mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-12 08:41:46 -05:00
4dda1b67dd
Attempting to use a react-component-like system, where the Surface simply emits everything that happens through the shared GameUIState, and the parent (in this case the ThangsTabView, but theoretically anything that uses the surface) handles the events manually, to enforce desired behavior for that particular context. It's nice that all the event handling is centralized, but it's still a bit of a mess, and not thoroughly stateful. But it's a start. This is in preparation for allowing multi-thang selection and manipulation in the level editor.
752 lines
28 KiB
CoffeeScript
752 lines
28 KiB
CoffeeScript
CocoClass = require 'core/CocoClass'
|
|
TrailMaster = require './TrailMaster'
|
|
Dropper = require './Dropper'
|
|
AudioPlayer = require 'lib/AudioPlayer'
|
|
{me} = require 'core/auth'
|
|
Camera = require './Camera'
|
|
CameraBorder = require './CameraBorder'
|
|
Layer = require('./LayerAdapter')
|
|
Letterbox = require './Letterbox'
|
|
Dimmer = require './Dimmer'
|
|
CountdownScreen = require './CountdownScreen'
|
|
PlaybackOverScreen = require './PlaybackOverScreen'
|
|
WaitingScreen = require './WaitingScreen'
|
|
DebugDisplay = require './DebugDisplay'
|
|
CoordinateDisplay = require './CoordinateDisplay'
|
|
CoordinateGrid = require './CoordinateGrid'
|
|
LankBoss = require './LankBoss'
|
|
PointChooser = require './PointChooser'
|
|
RegionChooser = require './RegionChooser'
|
|
MusicPlayer = require './MusicPlayer'
|
|
GameUIState = require 'models/GameUIState'
|
|
|
|
resizeDelay = 500 # At least as much as $level-resize-transition-time.
|
|
|
|
module.exports = Surface = class Surface extends CocoClass
|
|
stage: null
|
|
|
|
normalLayers: null
|
|
surfaceLayer: null
|
|
surfaceTextLayer: null
|
|
screenLayer: null
|
|
gridLayer: null
|
|
|
|
lankBoss: null
|
|
|
|
debugDisplay: null
|
|
currentFrame: 0
|
|
lastFrame: null
|
|
totalFramesDrawn: 0
|
|
playing: false # play vs. pause -- match default button state in playback.jade
|
|
dead: false # if we kill it for some reason
|
|
imagesLoaded: false
|
|
worldLoaded: false
|
|
scrubbing: false
|
|
debug: false
|
|
|
|
defaults:
|
|
paths: true
|
|
grid: false
|
|
navigateToSelection: true
|
|
choosing: false # 'point', 'region', 'ratio-region'
|
|
coords: null # use world defaults, or set to false/true to override
|
|
showInvisible: false
|
|
frameRate: 30 # Best as a divisor of 60, like 15, 30, 60, with RAF_SYNCHED timing.
|
|
levelType: 'hero'
|
|
|
|
subscriptions:
|
|
'level:disable-controls': 'onDisableControls'
|
|
'level:enable-controls': 'onEnableControls'
|
|
'level:set-playing': 'onSetPlaying'
|
|
'level:set-debug': 'onSetDebug'
|
|
'level:toggle-debug': 'onToggleDebug'
|
|
'level:toggle-pathfinding': 'onTogglePathFinding'
|
|
'level:set-time': 'onSetTime'
|
|
'camera:set-camera': 'onSetCamera'
|
|
'level:restarted': 'onLevelRestarted'
|
|
'god:new-world-created': 'onNewWorld'
|
|
'god:streaming-world-updated': 'onNewWorld'
|
|
'tome:cast-spells': 'onCastSpells'
|
|
'level:set-letterbox': 'onSetLetterbox'
|
|
'application:idle-changed': 'onIdleChanged'
|
|
'camera:zoom-updated': 'onZoomUpdated'
|
|
'playback:real-time-playback-waiting': 'onRealTimePlaybackWaiting'
|
|
'playback:real-time-playback-started': 'onRealTimePlaybackStarted'
|
|
'playback:real-time-playback-ended': 'onRealTimePlaybackEnded'
|
|
'level:flag-color-selected': 'onFlagColorSelected'
|
|
|
|
shortcuts:
|
|
'ctrl+\\, ⌘+\\': 'onToggleDebug'
|
|
'ctrl+o, ⌘+o': 'onTogglePathFinding'
|
|
|
|
|
|
|
|
#- Initialization
|
|
|
|
constructor: (@world, @normalCanvas, @webGLCanvas, givenOptions) ->
|
|
super()
|
|
@normalLayers = []
|
|
@options = _.clone(@defaults)
|
|
@options = _.extend(@options, givenOptions) if givenOptions
|
|
@handleEvents = @options.handleEvents ? true
|
|
@gameUIState = @options.gameUIState or new GameUIState({
|
|
canDragCamera: true
|
|
})
|
|
@initEasel()
|
|
@initAudio()
|
|
@onResize = _.debounce @onResize, resizeDelay
|
|
$(window).on 'resize', @onResize
|
|
if @world.ended
|
|
_.defer => @setWorld @world
|
|
|
|
initEasel: ->
|
|
@normalStage = new createjs.Stage(@normalCanvas[0])
|
|
@webGLStage = new createjs.SpriteStage(@webGLCanvas[0])
|
|
@normalStage.nextStage = @webGLStage
|
|
@camera = new Camera(@webGLCanvas, { @gameUIState, @handleEvents })
|
|
AudioPlayer.camera = @camera unless @options.choosing
|
|
|
|
@normalLayers.push @surfaceTextLayer = new Layer name: 'Surface Text', layerPriority: 1, transform: Layer.TRANSFORM_SURFACE_TEXT, camera: @camera
|
|
@normalLayers.push @gridLayer = new Layer name: 'Grid', layerPriority: 2, transform: Layer.TRANSFORM_SURFACE, camera: @camera
|
|
@normalLayers.push @screenLayer = new Layer name: 'Screen', layerPriority: 3, transform: Layer.TRANSFORM_SCREEN, camera: @camera
|
|
# @normalLayers.push @cameraBorderLayer = new Layer name: 'Camera Border', layerPriority: 4, transform: Layer.TRANSFORM_SURFACE, camera: @camera
|
|
# @cameraBorderLayer.addChild @cameraBorder = new CameraBorder(bounds: @camera.bounds)
|
|
@normalStage.addChild (layer.container for layer in @normalLayers)...
|
|
|
|
canvasWidth = parseInt @normalCanvas.attr('width'), 10
|
|
canvasHeight = parseInt @normalCanvas.attr('height'), 10
|
|
@screenLayer.addChild new Letterbox canvasWidth: canvasWidth, canvasHeight: canvasHeight
|
|
|
|
@lankBoss = new LankBoss({
|
|
@camera
|
|
@webGLStage
|
|
@surfaceTextLayer
|
|
@world
|
|
thangTypes: @options.thangTypes
|
|
choosing: @options.choosing
|
|
navigateToSelection: @options.navigateToSelection
|
|
showInvisible: @options.showInvisible
|
|
playerNames: if @options.levelType is 'course-ladder' then @options.playerNames else null
|
|
@gameUIState
|
|
@handleEvents
|
|
})
|
|
@countdownScreen = new CountdownScreen camera: @camera, layer: @screenLayer, showsCountdown: @world.showsCountdown
|
|
@playbackOverScreen = new PlaybackOverScreen camera: @camera, layer: @screenLayer, playerNames: @options.playerNames
|
|
@normalStage.addChildAt @playbackOverScreen.dimLayer, 0 # Put this below the other layers, actually, so we can more easily read text on the screen.
|
|
@waitingScreen = new WaitingScreen camera: @camera, layer: @screenLayer
|
|
@initCoordinates()
|
|
@webGLStage.enableMouseOver(10)
|
|
@webGLStage.addEventListener 'stagemousemove', @onMouseMove
|
|
@webGLStage.addEventListener 'stagemousedown', @onMouseDown
|
|
@webGLStage.addEventListener 'stagemouseup', @onMouseUp
|
|
@webGLCanvas.on 'mousewheel', @onMouseWheel
|
|
@hookUpChooseControls() if @options.choosing # TODO: figure this stuff out
|
|
createjs.Ticker.timingMode = createjs.Ticker.RAF_SYNCHED
|
|
createjs.Ticker.setFPS @options.frameRate
|
|
@onResize()
|
|
|
|
initCoordinates: ->
|
|
@coordinateGrid ?= new CoordinateGrid {camera: @camera, layer: @gridLayer, textLayer: @surfaceTextLayer}, @world.size()
|
|
@coordinateGrid.showGrid() if @world.showGrid or @options.grid
|
|
showCoordinates = if @options.coords? then @options.coords else @world.showCoordinates
|
|
@coordinateDisplay ?= new CoordinateDisplay camera: @camera, layer: @surfaceTextLayer if showCoordinates
|
|
|
|
hookUpChooseControls: ->
|
|
chooserOptions = stage: @webGLStage, surfaceLayer: @surfaceTextLayer, camera: @camera, restrictRatio: @options.choosing is 'ratio-region'
|
|
klass = if @options.choosing is 'point' then PointChooser else RegionChooser
|
|
@chooser = new klass chooserOptions
|
|
|
|
initAudio: ->
|
|
@musicPlayer = new MusicPlayer()
|
|
|
|
|
|
|
|
#- Setting the world
|
|
|
|
setWorld: (@world) ->
|
|
@worldLoaded = true
|
|
@lankBoss.world = @world
|
|
@restoreWorldState() unless @options.choosing
|
|
@showLevel()
|
|
@updateState true if @loaded
|
|
@onFrameChanged()
|
|
|
|
showLevel: ->
|
|
return if @destroyed
|
|
return if @loaded
|
|
@loaded = true
|
|
@lankBoss.createMarks()
|
|
@updateState true
|
|
@drawCurrentFrame()
|
|
createjs.Ticker.addEventListener 'tick', @tick
|
|
Backbone.Mediator.publish 'level:started', {}
|
|
|
|
#- Update loop
|
|
|
|
tick: (e) =>
|
|
# seems to be a bug where only one object can register with the Ticker...
|
|
oldFrame = @currentFrame
|
|
oldWorldFrame = Math.floor oldFrame
|
|
lastFrame = @world.frames.length - 1
|
|
framesDropped = 0
|
|
while true
|
|
Dropper.tick()
|
|
# Skip some frame updates unless we're playing and not at end (or we haven't drawn much yet)
|
|
frameAdvanced = (@playing and @currentFrame < lastFrame) or @totalFramesDrawn < 2
|
|
if frameAdvanced and @playing
|
|
advanceBy = @world.frameRate / @options.frameRate
|
|
if @fastForwardingToFrame and @currentFrame < @fastForwardingToFrame - advanceBy
|
|
advanceBy = Math.min(@currentFrame + advanceBy * @fastForwardingSpeed, @fastForwardingToFrame) - @currentFrame
|
|
else if @fastForwardingToFrame
|
|
@fastForwardingToFrame = @fastForwardingSpeed = null
|
|
@currentFrame += advanceBy
|
|
@currentFrame = Math.min @currentFrame, lastFrame
|
|
newWorldFrame = Math.floor @currentFrame
|
|
if Dropper.drop()
|
|
++framesDropped
|
|
else
|
|
worldFrameAdvanced = newWorldFrame isnt oldWorldFrame
|
|
if worldFrameAdvanced
|
|
# Only restore world state when it will correspond to an integer WorldFrame, not interpolated frame.
|
|
@restoreWorldState()
|
|
oldWorldFrame = newWorldFrame
|
|
break
|
|
if frameAdvanced and not worldFrameAdvanced
|
|
# We didn't end the above loop on an integer frame, so do the world state update.
|
|
@restoreWorldState()
|
|
|
|
# these are skipped for dropped frames
|
|
@updateState @currentFrame isnt oldFrame
|
|
@drawCurrentFrame e
|
|
@onFrameChanged()
|
|
Backbone.Mediator.publish('surface:ticked', {dt: 1 / @options.frameRate})
|
|
mib = @webGLStage.mouseInBounds
|
|
if @mouseInBounds isnt mib
|
|
Backbone.Mediator.publish('surface:mouse-' + (if mib then 'over' else 'out'), {})
|
|
@mouseInBounds = mib
|
|
@mouseIsDown = false
|
|
|
|
restoreWorldState: ->
|
|
frame = @world.getFrame(@getCurrentFrame())
|
|
frame.restoreState()
|
|
current = Math.max(0, Math.min(@currentFrame, @world.frames.length - 1))
|
|
if current - Math.floor(current) > 0.01 and Math.ceil(current) < @world.frames.length - 1
|
|
next = Math.ceil current
|
|
ratio = current % 1
|
|
@world.frames[next].restorePartialState ratio if next > 1
|
|
frame.clearEvents() if parseInt(@currentFrame) is parseInt(@lastFrame)
|
|
@lankBoss.updateSounds() if parseInt(@currentFrame) isnt parseInt(@lastFrame)
|
|
|
|
updateState: (frameChanged) ->
|
|
# world state must have been restored in @restoreWorldState
|
|
if @handleEvents
|
|
if @playing and @currentFrame < @world.frames.length - 1 and @heroLank and not @mouseIsDown and @camera.newTarget isnt @heroLank.sprite and @camera.target isnt @heroLank.sprite
|
|
@camera.zoomTo @heroLank.sprite, @camera.zoom, 750
|
|
@lankBoss.update frameChanged
|
|
@camera.updateZoom() # Make sure to do this right after the LankBoss updates, not before, so it can properly target sprite positions.
|
|
@dimmer?.setSprites @lankBoss.lanks
|
|
|
|
drawCurrentFrame: (e) ->
|
|
++@totalFramesDrawn
|
|
@normalStage.update e
|
|
@webGLStage.update e
|
|
|
|
|
|
#- Setting play/pause and progress
|
|
|
|
setProgress: (progress, scrubDuration=500) ->
|
|
progress = Math.max(Math.min(progress, 1), 0.0)
|
|
|
|
@fastForwardingToFrame = null
|
|
@scrubbing = true
|
|
onTweenEnd = =>
|
|
@scrubbingTo = null
|
|
@scrubbing = false
|
|
@scrubbingPlaybackSpeed = null
|
|
|
|
if @scrubbingTo?
|
|
# cut to the chase for existing tween
|
|
createjs.Tween.removeTweens(@)
|
|
@currentFrame = @scrubbingTo
|
|
|
|
@scrubbingTo = Math.round(progress * (@world.frames.length - 1))
|
|
@scrubbingTo = Math.max @scrubbingTo, 1
|
|
@scrubbingTo = Math.min @scrubbingTo, @world.frames.length - 1
|
|
@scrubbingPlaybackSpeed = Math.sqrt(Math.abs(@scrubbingTo - @currentFrame) * @world.dt / (scrubDuration or 0.5))
|
|
if scrubDuration
|
|
t = createjs.Tween
|
|
.get(@)
|
|
.to({currentFrame: @scrubbingTo}, scrubDuration, createjs.Ease.sineInOut)
|
|
.call(onTweenEnd)
|
|
t.addEventListener('change', @onFramesScrubbed)
|
|
else
|
|
@currentFrame = @scrubbingTo
|
|
@onFramesScrubbed() # For performance, don't play these for instant transitions.
|
|
onTweenEnd()
|
|
|
|
return unless @loaded
|
|
@updateState true
|
|
@onFrameChanged()
|
|
|
|
onFramesScrubbed: (e) =>
|
|
return unless @loaded
|
|
if e
|
|
# Gotta play all the sounds when scrubbing (but not when doing an immediate transition).
|
|
rising = @currentFrame > @lastFrame
|
|
actualCurrentFrame = @currentFrame
|
|
tempFrame = if rising then Math.ceil(@lastFrame) else Math.floor(@lastFrame)
|
|
while true # temporary fix to stop cacophony
|
|
break if rising and tempFrame > actualCurrentFrame
|
|
break if (not rising) and tempFrame < actualCurrentFrame
|
|
@currentFrame = tempFrame
|
|
frame = @world.getFrame(@getCurrentFrame())
|
|
frame.restoreState()
|
|
volume = Math.max(0.05, Math.min(1, 1 / @scrubbingPlaybackSpeed))
|
|
lank.playSounds false, volume for lank in @lankBoss.lankArray
|
|
tempFrame += if rising then 1 else -1
|
|
@currentFrame = actualCurrentFrame
|
|
|
|
@restoreWorldState()
|
|
@lankBoss.update true
|
|
@onFrameChanged()
|
|
|
|
getCurrentFrame: ->
|
|
return Math.max(0, Math.min(Math.floor(@currentFrame), @world.frames.length - 1))
|
|
|
|
setPaused: (paused) ->
|
|
# We want to be able to essentially stop rendering the surface if it doesn't need to animate anything.
|
|
# If pausing, though, we want to give it enough time to finish any tweens.
|
|
performToggle = =>
|
|
createjs.Ticker.setFPS if paused then 1 else @options.frameRate
|
|
@surfacePauseTimeout = null
|
|
clearTimeout @surfacePauseTimeout if @surfacePauseTimeout
|
|
clearTimeout @surfaceZoomPauseTimeout if @surfaceZoomPauseTimeout
|
|
@surfacePauseTimeout = @surfaceZoomPauseTimeout = null
|
|
if paused
|
|
@surfacePauseTimeout = _.delay performToggle, 2000
|
|
@lankBoss.stop()
|
|
@trailmaster?.stop()
|
|
@playbackOverScreen.show()
|
|
else
|
|
performToggle()
|
|
@lankBoss.play()
|
|
@trailmaster?.play()
|
|
@playbackOverScreen.hide()
|
|
|
|
|
|
|
|
#- Changes and events that only need to happen when the frame has changed
|
|
|
|
onFrameChanged: (force) ->
|
|
@currentFrame = Math.min(@currentFrame, @world.frames.length - 1)
|
|
@debugDisplay?.updateFrame @currentFrame
|
|
return if @currentFrame is @lastFrame and not force
|
|
progress = @getProgress()
|
|
Backbone.Mediator.publish('surface:frame-changed',
|
|
selectedThang: @lankBoss.selectedLank?.thang
|
|
progress: progress
|
|
frame: @currentFrame
|
|
world: @world
|
|
)
|
|
|
|
if @lastFrame < @world.frames.length and @currentFrame >= @world.totalFrames - 1
|
|
@ended = true
|
|
@setPaused true
|
|
Backbone.Mediator.publish 'surface:playback-ended', {}
|
|
@updatePaths() # TODO: this is a hack to make sure paths are on the first time the level loads
|
|
else if @currentFrame < @world.totalFrames and @ended
|
|
@ended = false
|
|
@setPaused false
|
|
Backbone.Mediator.publish 'surface:playback-restarted', {}
|
|
|
|
@lastFrame = @currentFrame
|
|
|
|
getProgress: -> @currentFrame / Math.max(1, @world.frames.length - 1)
|
|
|
|
|
|
|
|
#- Subscription callbacks
|
|
|
|
onToggleDebug: (e) ->
|
|
e?.preventDefault?()
|
|
Backbone.Mediator.publish 'level:set-debug', {debug: not @debug}
|
|
|
|
onSetDebug: (e) ->
|
|
return if e.debug is @debug
|
|
@debug = e.debug
|
|
if @debug and not @debugDisplay
|
|
@screenLayer.addChild @debugDisplay = new DebugDisplay canvasWidth: @camera.canvasWidth, canvasHeight: @camera.canvasHeight
|
|
|
|
onLevelRestarted: (e) ->
|
|
@setProgress 0, 0
|
|
|
|
onSetCamera: (e) ->
|
|
if e.thangID
|
|
return unless target = @lankBoss.lankFor(e.thangID)?.sprite
|
|
else if e.pos
|
|
target = @camera.worldToSurface e.pos
|
|
else
|
|
target = null
|
|
@camera.setBounds e.bounds if e.bounds
|
|
# @cameraBorder.updateBounds @camera.bounds
|
|
if @handleEvents
|
|
@camera.zoomTo target, e.zoom, e.duration # TODO: SurfaceScriptModule perhaps shouldn't assign e.zoom if not set
|
|
|
|
onZoomUpdated: (e) ->
|
|
if @ended
|
|
@setPaused false
|
|
@surfaceZoomPauseTimeout = _.delay (=> @setPaused true), 3000
|
|
@zoomedIn = e.zoom > e.minZoom * 1.1
|
|
@updateGrabbability()
|
|
|
|
updateGrabbability: ->
|
|
@webGLCanvas.toggleClass 'grabbable', @zoomedIn and not @playing and not @disabled
|
|
|
|
onDisableControls: (e) ->
|
|
return if e.controls and not ('surface' in e.controls)
|
|
@setDisabled true
|
|
@dimmer ?= new Dimmer camera: @camera, layer: @screenLayer
|
|
@dimmer.setSprites @lankBoss.lanks
|
|
|
|
onEnableControls: (e) ->
|
|
return if e.controls and not ('surface' in e.controls)
|
|
@setDisabled false
|
|
|
|
onSetLetterbox: (e) ->
|
|
@setDisabled e.on
|
|
|
|
setDisabled: (@disabled) ->
|
|
@lankBoss.disabled = @disabled
|
|
@updateGrabbability()
|
|
|
|
onSetPlaying: (e) ->
|
|
@playing = (e ? {}).playing ? true
|
|
@setPlayingCalled = true
|
|
if @playing and @currentFrame >= (@world.totalFrames - 5)
|
|
@currentFrame = 1 # Go back to the beginning (but not frame 0, that frame is weird)
|
|
if @fastForwardingToFrame and not @playing
|
|
@fastForwardingToFrame = null
|
|
@updateGrabbability()
|
|
|
|
onSetTime: (e) ->
|
|
toFrame = @currentFrame
|
|
if e.time?
|
|
@worldLifespan = @world.frames.length / @world.frameRate
|
|
e.ratio = e.time / @worldLifespan
|
|
if e.ratio?
|
|
toFrame = @world.frames.length * e.ratio
|
|
if e.frameOffset
|
|
toFrame += e.frameOffset
|
|
if e.ratioOffset
|
|
toFrame += @world.frames.length * e.ratioOffset
|
|
unless _.isNumber(toFrame) and not _.isNaN(toFrame)
|
|
return console.error('set-time event', e, 'produced invalid target frame', toFrame)
|
|
@setProgress(toFrame / @world.frames.length, e.scrubDuration)
|
|
|
|
onCastSpells: (e) ->
|
|
return if e.preload
|
|
@setPaused false if @ended
|
|
@casting = true
|
|
@setPlayingCalled = false # Don't overwrite playing settings if they changed by, say, scripts.
|
|
@frameBeforeCast = @currentFrame
|
|
# This is where I wanted to trigger a rewind, but it turned out to be pretty complicated, since the new world gets updated everywhere, and you don't want to rewind through that.
|
|
@setProgress 0, 0
|
|
|
|
onNewWorld: (event) ->
|
|
return unless event.world.name is @world.name
|
|
@onStreamingWorldUpdated event
|
|
|
|
onStreamingWorldUpdated: (event) ->
|
|
@casting = false
|
|
@lankBoss.play()
|
|
|
|
# This has a tendency to break scripts that are waiting for playback to change when the level is loaded
|
|
# so only run it after the first world is created.
|
|
Backbone.Mediator.publish 'level:set-playing', {playing: true} unless event.firstWorld or @setPlayingCalled
|
|
|
|
@setWorld event.world
|
|
@onFrameChanged(true)
|
|
fastForwardBuffer = 2
|
|
if @playing and not @realTime and (ffToFrame = Math.min(event.firstChangedFrame, @frameBeforeCast, @world.frames.length - 1)) and ffToFrame > @currentFrame + fastForwardBuffer * @world.frameRate
|
|
@fastForwardingToFrame = ffToFrame
|
|
@fastForwardingSpeed = Math.max 3, 3 * (@world.maxTotalFrames * @world.dt) / 60
|
|
else if @realTime
|
|
lag = (@world.frames.length - 1) * @world.dt - @world.age
|
|
intendedLag = @world.realTimeBufferMax + @world.dt
|
|
if lag > intendedLag * 1.2
|
|
@fastForwardingToFrame = @world.frames.length - @world.realTimeBufferMax * @world.frameRate
|
|
@fastForwardingSpeed = lag / intendedLag
|
|
else
|
|
@fastForwardingToFrame = @fastForwardingSpeed = null
|
|
# console.log "on new world, lag", lag, "intended lag", intendedLag, "fastForwardingToFrame", @fastForwardingToFrame, "speed", @fastForwardingSpeed, "cause we are at", @world.age, "of", @world.frames.length * @world.dt
|
|
if event.finished
|
|
@updatePaths()
|
|
else
|
|
@hidePaths()
|
|
|
|
onIdleChanged: (e) ->
|
|
@setPaused e.idle unless @ended
|
|
|
|
|
|
|
|
#- Mouse event callbacks
|
|
|
|
onMouseMove: (e) =>
|
|
@mouseScreenPos = {x: e.stageX, y: e.stageY}
|
|
return if @disabled
|
|
Backbone.Mediator.publish 'surface:mouse-moved', x: e.stageX, y: e.stageY
|
|
@gameUIState.trigger('surface:stage-mouse-move', { originalEvent: e })
|
|
|
|
onMouseDown: (e) =>
|
|
return if @disabled
|
|
cap = @camera.screenToCanvas({x: e.stageX, y: e.stageY})
|
|
# getObject(s)UnderPoint is broken, so we have to use the private method to get what we want
|
|
onBackground = not @webGLStage._getObjectsUnderPoint(e.stageX, e.stageY, null, true)
|
|
|
|
wop = @camera.screenToWorld x: e.stageX, y: e.stageY
|
|
event = { onBackground: onBackground, x: e.stageX, y: e.stageY, originalEvent: e, worldPos: wop }
|
|
Backbone.Mediator.publish 'surface:stage-mouse-down', event
|
|
Backbone.Mediator.publish 'tome:focus-editor', {}
|
|
@gameUIState.trigger('surface:stage-mouse-down', event)
|
|
@mouseIsDown = true
|
|
|
|
onMouseUp: (e) =>
|
|
return if @disabled
|
|
onBackground = not @webGLStage.hitTest e.stageX, e.stageY
|
|
event = { onBackground: onBackground, x: e.stageX, y: e.stageY, originalEvent: e }
|
|
Backbone.Mediator.publish 'surface:stage-mouse-up', event
|
|
Backbone.Mediator.publish 'tome:focus-editor', {}
|
|
@gameUIState.trigger('surface:stage-mouse-up', event)
|
|
@mouseIsDown = false
|
|
|
|
onMouseWheel: (e) =>
|
|
# https://github.com/brandonaaron/jquery-mousewheel
|
|
e.preventDefault()
|
|
return if @disabled
|
|
event =
|
|
deltaX: e.deltaX
|
|
deltaY: e.deltaY
|
|
canvas: @webGLCanvas
|
|
event.screenPos = @mouseScreenPos if @mouseScreenPos
|
|
Backbone.Mediator.publish 'surface:mouse-scrolled', event unless @disabled
|
|
@gameUIState.trigger('surface:mouse-scrolled', event)
|
|
|
|
|
|
#- Canvas callbacks
|
|
|
|
onResize: (e) =>
|
|
return if @destroyed or @options.choosing
|
|
oldWidth = parseInt @normalCanvas.attr('width'), 10
|
|
oldHeight = parseInt @normalCanvas.attr('height'), 10
|
|
aspectRatio = oldWidth / oldHeight
|
|
pageWidth = $('#page-container').width() - 17 # 17px nano scroll bar
|
|
if application.isIPadApp
|
|
newWidth = 1024
|
|
newHeight = newWidth / aspectRatio
|
|
else if @realTime or @options.spectateGame
|
|
pageHeight = $('#page-container').height() - $('#control-bar-view').outerHeight() - $('#playback-view').outerHeight()
|
|
newWidth = Math.min pageWidth, pageHeight * aspectRatio
|
|
newHeight = newWidth / aspectRatio
|
|
else if $('#thangs-tab-view')
|
|
newWidth = $('#canvas-wrapper').width()
|
|
newHeight = newWidth / aspectRatio
|
|
else
|
|
newWidth = 0.55 * pageWidth
|
|
newHeight = newWidth / aspectRatio
|
|
return unless newWidth > 0 and newHeight > 0
|
|
|
|
#scaleFactor = if application.isIPadApp then 2 else 1 # Retina
|
|
scaleFactor = 1
|
|
if @options.stayVisible
|
|
availableHeight = window.innerHeight
|
|
availableHeight -= $('.ad-container').outerHeight()
|
|
availableHeight -= $('#game-area').outerHeight() - $('#canvas-wrapper').outerHeight()
|
|
scaleFactor = availableHeight / newHeight if availableHeight < newHeight
|
|
newWidth *= scaleFactor
|
|
newHeight *= scaleFactor
|
|
|
|
return if newWidth is oldWidth and newHeight is oldHeight and not @options.spectateGame
|
|
return if newWidth < 200 or newHeight < 200
|
|
@normalCanvas.add(@webGLCanvas).attr width: newWidth, height: newHeight
|
|
|
|
# Cannot do this to the webGLStage because it does not use scaleX/Y.
|
|
# Instead the LayerAdapter scales webGL-enabled layers.
|
|
@webGLStage.updateViewport(@webGLCanvas[0].width, @webGLCanvas[0].height)
|
|
@normalStage.scaleX *= newWidth / oldWidth
|
|
@normalStage.scaleY *= newHeight / oldHeight
|
|
@camera.onResize newWidth, newHeight
|
|
if @options.spectateGame
|
|
# Since normalCanvas is absolutely positioned, it needs help aligning with webGLCanvas.
|
|
offset = @webGLCanvas.offset().left - ($('#page-container').innerWidth() - $('#canvas-wrapper').innerWidth()) / 2
|
|
@normalCanvas.css 'left', offset
|
|
|
|
#- Camera focus on hero
|
|
focusOnHero: ->
|
|
hadHero = @heroLank
|
|
@heroLank = @lankBoss.lankFor 'Hero Placeholder'
|
|
if me.team is 'ogres'
|
|
# TODO: do this for real
|
|
@heroLank = @lankBoss.lankFor 'Hero Placeholder 1'
|
|
@updatePaths() if not hadHero
|
|
|
|
#- Real-time playback
|
|
|
|
onRealTimePlaybackWaiting: (e) ->
|
|
@onRealTimePlaybackStarted e
|
|
|
|
onRealTimePlaybackStarted: (e) ->
|
|
return if @realTime
|
|
@realTime = true
|
|
@onResize()
|
|
@playing = false # Will start when countdown is done.
|
|
if @heroLank
|
|
@previousCameraZoom = @camera.zoom
|
|
#@camera.zoomTo @heroLank.sprite, 2, 3000 # This makes flag placement hard, now that we're only rarely using this as a coolcam.
|
|
|
|
onRealTimePlaybackEnded: (e) ->
|
|
return unless @realTime
|
|
@realTime = false
|
|
@onResize()
|
|
_.delay @onResize, resizeDelay + 100 # Do it again just to be double sure that we don't stay zoomed in due to timing problems.
|
|
@normalCanvas.add(@webGLCanvas).removeClass 'flag-color-selected'
|
|
if @handleEvents
|
|
if @previousCameraZoom
|
|
@camera.zoomTo @camera.newTarget or @camera.target, @previousCameraZoom, 3000
|
|
|
|
onFlagColorSelected: (e) ->
|
|
@normalCanvas.add(@webGLCanvas).toggleClass 'flag-color-selected', Boolean(e.color)
|
|
e.pos = @camera.screenToWorld @mouseScreenPos if @mouseScreenPos
|
|
|
|
|
|
|
|
updatePaths: ->
|
|
return unless @options.paths and @heroLank
|
|
@hidePaths()
|
|
return if @world.showPaths is 'never'
|
|
layerAdapter = @lankBoss.layerAdapters['Path']
|
|
@trailmaster ?= new TrailMaster @camera, layerAdapter
|
|
@paths = @trailmaster.generatePaths @world, @heroLank.thang
|
|
@paths.name = 'paths'
|
|
layerAdapter.addChild @paths
|
|
|
|
hidePaths: ->
|
|
return if not @paths
|
|
if @paths.parent
|
|
@paths.parent.removeChild @paths
|
|
@paths = null
|
|
|
|
|
|
|
|
#- Screenshot
|
|
|
|
screenshot: (scale=0.25, format='image/jpeg', quality=0.8, zoom=2) ->
|
|
# TODO: get screenshots working again
|
|
# Quality doesn't work with image/png, just image/jpeg and image/webp
|
|
[w, h] = [@camera.canvasWidth * @camera.canvasScaleFactorX, @camera.canvasHeight * @camera.canvasScaleFactorY]
|
|
margin = (1 - 1 / zoom) / 2
|
|
@webGLStage.cache margin * w, margin * h, w / zoom, h / zoom, scale * zoom
|
|
imageData = @webGLStage.cacheCanvas.toDataURL(format, quality)
|
|
#console.log 'Screenshot with scale', scale, 'format', format, 'quality', quality, 'was', Math.floor(imageData.length / 1024), 'kB'
|
|
screenshot = document.createElement('img')
|
|
screenshot.src = imageData
|
|
@webGLStage.uncache()
|
|
imageData
|
|
|
|
|
|
|
|
#- Path finding debugging
|
|
|
|
onTogglePathFinding: (e) ->
|
|
e?.preventDefault?()
|
|
@hidePathFinding()
|
|
@showingPathFinding = not @showingPathFinding
|
|
if @showingPathFinding then @showPathFinding() else @hidePathFinding()
|
|
|
|
hidePathFinding: ->
|
|
@surfaceLayer.removeChild @navRectangles if @navRectangles
|
|
@surfaceLayer.removeChild @navPaths if @navPaths
|
|
@navRectangles = @navPaths = null
|
|
|
|
showPathFinding: ->
|
|
@hidePathFinding()
|
|
|
|
mesh = _.values(@world.navMeshes or {})[0]
|
|
return unless mesh
|
|
@navRectangles = new createjs.Container()
|
|
@navRectangles.layerPriority = -1
|
|
@addMeshRectanglesToContainer mesh, @navRectangles
|
|
@surfaceLayer.addChild @navRectangles
|
|
@surfaceLayer.updateLayerOrder()
|
|
|
|
graph = _.values(@world.graphs or {})[0]
|
|
return @surfaceLayer.updateLayerOrder() unless graph
|
|
@navPaths = new createjs.Container()
|
|
@navPaths.layerPriority = -1
|
|
@addNavPathsToContainer graph, @navPaths
|
|
@surfaceLayer.addChild @navPaths
|
|
@surfaceLayer.updateLayerOrder()
|
|
|
|
addMeshRectanglesToContainer: (mesh, container) ->
|
|
for rect in mesh
|
|
shape = new createjs.Shape()
|
|
pos = @camera.worldToSurface {x: rect.x, y: rect.y}
|
|
dim = @camera.worldToSurface {x: rect.width, y: rect.height}
|
|
shape.graphics
|
|
.setStrokeStyle(3)
|
|
.beginFill('rgba(0,0,128,0.3)')
|
|
.beginStroke('rgba(0,0,128,0.7)')
|
|
.drawRect(pos.x - dim.x/2, pos.y - dim.y/2, dim.x, dim.y)
|
|
container.addChild shape
|
|
|
|
addNavPathsToContainer: (graph, container) ->
|
|
for node in _.values graph
|
|
for edgeVertex in node.edges
|
|
@drawLine node.vertex, edgeVertex, container
|
|
|
|
drawLine: (v1, v2, container) ->
|
|
shape = new createjs.Shape()
|
|
v1 = @camera.worldToSurface v1
|
|
v2 = @camera.worldToSurface v2
|
|
shape.graphics
|
|
.setStrokeStyle(1)
|
|
.moveTo(v1.x, v1.y)
|
|
.beginStroke('rgba(128,0,0,0.4)')
|
|
.lineTo(v2.x, v2.y)
|
|
.endStroke()
|
|
container.addChild shape
|
|
|
|
|
|
|
|
#- Teardown
|
|
|
|
destroy: ->
|
|
@camera?.destroy()
|
|
createjs.Ticker.removeEventListener('tick', @tick)
|
|
createjs.Sound.stop()
|
|
layer.destroy() for layer in @normalLayers
|
|
@lankBoss.destroy()
|
|
@chooser?.destroy()
|
|
@dimmer?.destroy()
|
|
@countdownScreen?.destroy()
|
|
@playbackOverScreen?.destroy()
|
|
@waitingScreen?.destroy()
|
|
@coordinateDisplay?.destroy()
|
|
@coordinateGrid?.destroy()
|
|
@normalStage.clear()
|
|
@webGLStage.clear()
|
|
@musicPlayer?.destroy()
|
|
@trailmaster?.destroy()
|
|
@normalStage.removeAllChildren()
|
|
@webGLStage.removeAllChildren()
|
|
@webGLStage.removeEventListener 'stagemousemove', @onMouseMove
|
|
@webGLStage.removeEventListener 'stagemousedown', @onMouseDown
|
|
@webGLStage.removeEventListener 'stagemouseup', @onMouseUp
|
|
@webGLStage.removeAllEventListeners()
|
|
@normalStage.enableDOMEvents false
|
|
@webGLStage.enableDOMEvents false
|
|
@webGLStage.enableMouseOver 0
|
|
@webGLCanvas.off 'mousewheel', @onMouseWheel
|
|
$(window).off 'resize', @onResize
|
|
clearTimeout @surfacePauseTimeout if @surfacePauseTimeout
|
|
clearTimeout @surfaceZoomPauseTimeout if @surfaceZoomPauseTimeout
|
|
super()
|