2014-11-28 20:49:41 -05:00
CocoClass = require ' core/CocoClass '
2014-11-18 14:46:42 -05:00
TrailMaster = require ' ./TrailMaster '
2014-01-03 13:32:13 -05:00
Dropper = require ' ./Dropper '
AudioPlayer = require ' lib/AudioPlayer '
2014-11-28 20:49:41 -05:00
{ me } = require ' core/auth '
2014-01-03 13:32:13 -05:00
Camera = require ' ./Camera '
CameraBorder = require ' ./CameraBorder '
2014-09-19 18:46:37 -04:00
Layer = require ( ' ./LayerAdapter ' )
2014-01-03 13:32:13 -05:00
Letterbox = require ' ./Letterbox '
Dimmer = require ' ./Dimmer '
2014-08-26 01:05:24 -04:00
CountdownScreen = require ' ./CountdownScreen '
2014-02-25 20:14:39 -05:00
PlaybackOverScreen = require ' ./PlaybackOverScreen '
2014-08-30 00:46:26 -04:00
WaitingScreen = require ' ./WaitingScreen '
2014-01-03 13:32:13 -05:00
DebugDisplay = require ' ./DebugDisplay '
CoordinateDisplay = require ' ./CoordinateDisplay '
2014-08-26 16:56:57 -04:00
CoordinateGrid = require ' ./CoordinateGrid '
2014-09-28 17:00:48 -04:00
LankBoss = require ' ./LankBoss '
2014-01-03 13:32:13 -05:00
PointChooser = require ' ./PointChooser '
RegionChooser = require ' ./RegionChooser '
MusicPlayer = require ' ./MusicPlayer '
2014-09-25 12:48:14 -04:00
resizeDelay = 500 # At least as much as $level-resize-transition-time.
2014-01-03 13:32:13 -05:00
module.exports = Surface = class Surface extends CocoClass
stage: null
2014-09-25 13:47:53 -04:00
normalLayers: null
2014-01-03 13:32:13 -05:00
surfaceLayer: null
surfaceTextLayer: null
screenLayer: null
2014-08-26 16:56:57 -04:00
gridLayer: null
2014-01-03 13:32:13 -05:00
2014-09-28 17:00:48 -04:00
lankBoss: null
2014-01-03 13:32:13 -05:00
debugDisplay: null
currentFrame: 0
lastFrame: null
totalFramesDrawn: 0
2014-09-22 01:10:52 -04:00
playing: false # play vs. pause -- match default button state in playback.jade
2014-01-03 13:32:13 -05:00
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'
2014-09-23 21:59:08 -04:00
coords: null # use world defaults, or set to false/true to override
2014-01-03 13:32:13 -05:00
showInvisible: false
2014-04-28 19:41:18 -04:00
frameRate: 30 # Best as a divisor of 60, like 15, 30, 60, with RAF_SYNCHED timing.
2015-11-30 16:05:16 -05:00
levelType: ' hero '
2014-01-03 13:32:13 -05:00
subscriptions:
2014-08-27 15:24:03 -04:00
' 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 '
2014-01-03 13:32:13 -05:00
' level:restarted ' : ' onLevelRestarted '
' god:new-world-created ' : ' onNewWorld '
2014-08-21 19:27:52 -04:00
' god:streaming-world-updated ' : ' onNewWorld '
2014-01-03 13:32:13 -05:00
' tome:cast-spells ' : ' onCastSpells '
2014-08-27 15:24:03 -04:00
' level:set-letterbox ' : ' onSetLetterbox '
2014-05-09 18:07:30 -04:00
' application:idle-changed ' : ' onIdleChanged '
2014-05-12 18:54:07 -04:00
' camera:zoom-updated ' : ' onZoomUpdated '
2014-08-30 00:46:26 -04:00
' playback:real-time-playback-waiting ' : ' onRealTimePlaybackWaiting '
2014-08-23 16:54:52 -04:00
' playback:real-time-playback-started ' : ' onRealTimePlaybackStarted '
' playback:real-time-playback-ended ' : ' onRealTimePlaybackEnded '
2014-08-24 19:09:06 -04:00
' level:flag-color-selected ' : ' onFlagColorSelected '
2014-01-03 13:32:13 -05:00
shortcuts:
2014-01-31 19:32:46 -05:00
' ctrl+ \\ , ⌘+ \\ ' : ' onToggleDebug '
' ctrl+o, ⌘+o ' : ' onTogglePathFinding '
2014-01-03 13:32:13 -05:00
2014-09-06 22:50:31 -04:00
2014-09-05 12:20:29 -04:00
#- Initialization
2014-01-03 13:32:13 -05:00
2014-09-25 13:47:53 -04:00
constructor: (@world, @normalCanvas, @webGLCanvas, givenOptions) ->
2014-01-03 13:32:13 -05:00
super ( )
2014-09-25 13:47:53 -04:00
@normalLayers = [ ]
2014-01-03 13:32:13 -05:00
@options = _ . clone ( @ defaults )
@options = _ . extend ( @ options , givenOptions ) if givenOptions
@ initEasel ( )
@ initAudio ( )
2014-09-25 12:48:14 -04:00
@onResize = _ . debounce @ onResize , resizeDelay
2014-05-12 16:28:46 -04:00
$ ( window ) . on ' resize ' , @ onResize
2014-05-14 13:35:16 -04:00
if @ world . ended
_ . defer => @ setWorld @ world
2014-01-03 13:32:13 -05:00
2014-09-05 12:20:29 -04:00
initEasel: ->
2014-09-25 13:47:53 -04:00
@normalStage = new createjs . Stage ( @ normalCanvas [ 0 ] )
@webGLStage = new createjs . SpriteStage ( @ webGLCanvas [ 0 ] )
2014-10-01 18:02:14 -04:00
@normalStage.nextStage = @ webGLStage
2014-10-25 14:47:04 -04:00
@camera = new Camera @ webGLCanvas
AudioPlayer.camera = @ camera unless @ options . choosing
2014-09-25 13:47:53 -04:00
@ 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
2014-09-29 13:18:27 -04:00
# @normalLayers.push @cameraBorderLayer = new Layer name: 'Camera Border', layerPriority: 4, transform: Layer.TRANSFORM_SURFACE, camera: @camera
# @cameraBorderLayer.addChild @cameraBorder = new CameraBorder(bounds: @camera.bounds)
2014-09-26 15:45:27 -04:00
@ normalStage . addChild ( layer . container for layer in @ normalLayers ) . . .
2014-09-25 13:47:53 -04:00
canvasWidth = parseInt @ normalCanvas . attr ( ' width ' ) , 10
canvasHeight = parseInt @ normalCanvas . attr ( ' height ' ) , 10
2014-09-05 12:20:29 -04:00
@ screenLayer . addChild new Letterbox canvasWidth: canvasWidth , canvasHeight: canvasHeight
2014-09-25 13:47:53 -04:00
2015-11-30 16:05:16 -05:00
@lankBoss = new LankBoss camera: @ camera , webGLStage: @ webGLStage , surfaceTextLayer: @ surfaceTextLayer , world: @ 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
2014-10-30 19:21:08 -04:00
@countdownScreen = new CountdownScreen camera: @ camera , layer: @ screenLayer , showsCountdown: @ world . showsCountdown
2015-09-03 16:32:20 -04:00
@playbackOverScreen = new PlaybackOverScreen camera: @ camera , layer: @ screenLayer , playerNames: @ options . playerNames
2014-10-08 12:46:56 -04:00
@ normalStage . addChildAt @ playbackOverScreen . dimLayer , 0 # Put this below the other layers, actually, so we can more easily read text on the screen.
2014-09-05 12:20:29 -04:00
@waitingScreen = new WaitingScreen camera: @ camera , layer: @ screenLayer
@ initCoordinates ( )
2014-09-25 13:47:53 -04:00
@ webGLStage . enableMouseOver ( 10 )
@ webGLStage . addEventListener ' stagemousemove ' , @ onMouseMove
@ webGLStage . addEventListener ' stagemousedown ' , @ onMouseDown
@ webGLCanvas [ 0 ] . addEventListener ' mouseup ' , @ onMouseUp
2014-09-26 14:07:01 -04:00
@ webGLCanvas . on ' mousewheel ' , @ onMouseWheel
2014-09-25 13:47:53 -04:00
@ hookUpChooseControls ( ) if @ options . choosing # TODO: figure this stuff out
2014-11-18 18:57:06 -05:00
createjs.Ticker.timingMode = createjs . Ticker . RAF_SYNCHED
2014-09-05 12:20:29 -04:00
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
2014-09-23 21:39:52 -04:00
showCoordinates = if @ options . coords ? then @ options . coords else @ world . showCoordinates
@ coordinateDisplay ? = new CoordinateDisplay camera: @ camera , layer: @ surfaceTextLayer if showCoordinates
2014-09-05 12:20:29 -04:00
hookUpChooseControls: ->
2014-10-21 13:59:05 -04:00
chooserOptions = stage: @ webGLStage , surfaceLayer: @ surfaceTextLayer , camera: @ camera , restrictRatio: @ options . choosing is ' ratio-region '
2014-09-05 12:20:29 -04:00
klass = if @ options . choosing is ' point ' then PointChooser else RegionChooser
@chooser = new klass chooserOptions
initAudio: ->
@musicPlayer = new MusicPlayer ( )
2014-02-12 15:41:41 -05:00
2014-09-05 12:20:29 -04:00
#- Setting the world
2014-09-06 22:50:31 -04:00
2014-01-03 13:32:13 -05:00
setWorld: (@world) ->
@worldLoaded = true
2014-09-28 17:00:48 -04:00
@lankBoss.world = @ world
2014-09-25 17:59:08 -04:00
@ restoreWorldState ( ) unless @ options . choosing
2014-01-03 13:32:13 -05:00
@ showLevel ( )
@ updateState true if @ loaded
@ onFrameChanged ( )
2014-09-05 12:20:29 -04:00
showLevel: ->
return if @ destroyed
return if @ loaded
@loaded = true
2014-09-28 17:00:48 -04:00
@ lankBoss . createMarks ( )
2014-09-05 12:20:29 -04:00
@ updateState true
@ drawCurrentFrame ( )
createjs . Ticker . addEventListener ' tick ' , @ tick
Backbone . Mediator . publish ' level:started ' , { }
2014-01-31 13:21:32 -05:00
2014-09-05 12:20:29 -04:00
#- Update loop
2014-01-31 13:21:32 -05:00
2014-09-05 12:20:29 -04:00
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
2014-09-26 18:23:27 -04:00
framesDropped = 0
2014-09-05 12:20:29 -04:00
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
2014-09-26 18:23:27 -04:00
if Dropper . drop ( )
2014-10-08 15:38:23 -04:00
++ framesDropped
2014-09-26 18:23:27 -04:00
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
2014-09-05 12:20:29 -04:00
if frameAdvanced and not worldFrameAdvanced
# We didn't end the above loop on an integer frame, so do the world state update.
@ restoreWorldState ( )
2014-01-20 15:57:45 -05:00
2014-09-05 12:20:29 -04:00
# these are skipped for dropped frames
@ updateState @ currentFrame isnt oldFrame
@ drawCurrentFrame e
@ onFrameChanged ( )
Backbone . Mediator . publish ( ' surface:ticked ' , { dt: 1 / @ options . frameRate } )
2014-09-25 13:47:53 -04:00
mib = @ webGLStage . mouseInBounds
2014-09-05 12:20:29 -04:00
if @ mouseInBounds isnt mib
Backbone . Mediator . publish ( ' surface:mouse- ' + ( if mib then ' over ' else ' out ' ) , { } )
@mouseInBounds = mib
2014-09-21 23:49:45 -04:00
@mouseIsDown = false
2014-01-20 15:57:45 -05:00
2014-09-05 12:20:29 -04:00
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 )
2014-09-28 17:00:48 -04:00
@ lankBoss . updateSounds ( ) if parseInt ( @ currentFrame ) isnt parseInt ( @ lastFrame )
2014-09-05 12:20:29 -04:00
updateState: (frameChanged) ->
# world state must have been restored in @restoreWorldState
2014-10-06 18:04:33 -04:00
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
2014-09-30 16:41:42 -04:00
@ camera . zoomTo @ heroLank . sprite , @ camera . zoom , 750
2014-09-28 17:00:48 -04:00
@ lankBoss . update frameChanged
2014-10-08 15:38:23 -04:00
@ camera . updateZoom ( ) # Make sure to do this right after the LankBoss updates, not before, so it can properly target sprite positions.
2014-09-30 16:44:03 -04:00
@ dimmer ? . setSprites @ lankBoss . lanks
2014-01-20 15:57:45 -05:00
2014-09-05 12:20:29 -04:00
drawCurrentFrame: (e) ->
++ @ totalFramesDrawn
2014-09-30 16:45:15 -04:00
@ normalStage . update e
2014-09-25 13:47:53 -04:00
@ webGLStage . update e
2014-09-06 22:50:31 -04:00
2014-09-05 12:20:29 -04:00
#- Setting play/pause and progress
2014-09-06 22:50:31 -04:00
2014-01-03 13:32:13 -05:00
setProgress: (progress, scrubDuration=500) ->
2014-02-25 20:14:39 -05:00
progress = Math . max ( Math . min ( progress , 1 ) , 0.0 )
2014-01-03 13:32:13 -05:00
2014-08-23 12:52:05 -04:00
@fastForwardingToFrame = null
2014-01-03 13:32:13 -05:00
@scrubbing = true
onTweenEnd = =>
@scrubbingTo = null
@scrubbing = false
@scrubbingPlaybackSpeed = null
if @ scrubbingTo ?
# cut to the chase for existing tween
createjs . Tween . removeTweens ( @ )
@currentFrame = @ scrubbingTo
2014-11-19 18:46:50 -05:00
@scrubbingTo = Math . round ( progress * ( @ world . frames . length - 1 ) )
@scrubbingTo = Math . max @ scrubbingTo , 1
@scrubbingTo = Math . min @ scrubbingTo , @ world . frames . length - 1
2014-01-03 13:32:13 -05:00
@scrubbingPlaybackSpeed = Math . sqrt ( Math . abs ( @ scrubbingTo - @ currentFrame ) * @ world . dt / ( scrubDuration or 0.5 ) )
if scrubDuration
t = createjs . Tween
. get ( @ )
2014-08-23 12:52:05 -04:00
. to ( { currentFrame: @ scrubbingTo } , scrubDuration , createjs . Ease . sineInOut )
2014-01-03 13:32:13 -05:00
. call ( onTweenEnd )
2014-02-28 14:27:32 -05:00
t . addEventListener ( ' change ' , @ onFramesScrubbed )
2014-01-03 13:32:13 -05:00
else
@currentFrame = @ scrubbingTo
2014-02-28 14:27:32 -05:00
@ onFramesScrubbed ( ) # For performance, don't play these for instant transitions.
2014-01-03 13:32:13 -05:00
onTweenEnd ( )
2014-03-12 12:10:36 -04:00
return unless @ loaded
2014-01-03 13:32:13 -05:00
@ updateState true
@ onFrameChanged ( )
2014-02-28 14:27:32 -05:00
onFramesScrubbed: (e) =>
2014-03-12 12:10:36 -04:00
return unless @ loaded
2014-02-28 14:27:32 -05:00
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 ( )
2014-04-28 19:31:51 -04:00
volume = Math . max ( 0.05 , Math . min ( 1 , 1 / @ scrubbingPlaybackSpeed ) )
2014-09-30 16:44:03 -04:00
lank . playSounds false , volume for lank in @ lankBoss . lankArray
2014-02-28 14:27:32 -05:00
tempFrame += if rising then 1 else - 1
@currentFrame = actualCurrentFrame
@ restoreWorldState ( )
2014-09-28 17:00:48 -04:00
@ lankBoss . update true
2014-01-03 13:32:13 -05:00
@ onFrameChanged ( )
getCurrentFrame: ->
2014-07-17 18:50:29 -04:00
return Math . max ( 0 , Math . min ( Math . floor ( @ currentFrame ) , @ world . frames . length - 1 ) )
2014-01-03 13:32:13 -05:00
2014-09-05 12:20:29 -04:00
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
2014-09-28 17:00:48 -04:00
@ lankBoss . stop ( )
2014-11-18 19:03:47 -05:00
@ trailmaster ? . stop ( )
2014-09-05 12:20:29 -04:00
@ playbackOverScreen . show ( )
else
performToggle ( )
2014-09-28 17:00:48 -04:00
@ lankBoss . play ( )
2014-11-18 19:03:47 -05:00
@ trailmaster ? . play ( )
2014-09-05 12:20:29 -04:00
@ playbackOverScreen . hide ( )
#- Changes and events that only need to happen when the frame has changed
2014-09-06 22:50:31 -04:00
2014-09-05 12:20:29 -04:00
onFrameChanged: (force) ->
2014-09-25 01:07:55 -04:00
@currentFrame = Math . min ( @ currentFrame , @ world . frames . length - 1 )
2014-09-05 12:20:29 -04:00
@ debugDisplay ? . updateFrame @ currentFrame
return if @ currentFrame is @ lastFrame and not force
progress = @ getProgress ( )
Backbone . Mediator . publish ( ' surface:frame-changed ' ,
2014-09-30 16:44:03 -04:00
selectedThang: @ lankBoss . selectedLank ? . thang
2014-09-05 12:20:29 -04:00
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 ' , { }
2014-11-21 16:08:40 -05:00
@ updatePaths ( ) # TODO: this is a hack to make sure paths are on the first time the level loads
2014-09-05 12:20:29 -04:00
else if @ currentFrame < @ world . totalFrames and @ ended
@ended = false
@ setPaused false
Backbone . Mediator . publish ' surface:playback-restarted ' , { }
@lastFrame = @ currentFrame
2014-09-24 23:25:30 -04:00
getProgress: -> @ currentFrame / Math . max ( 1 , @ world . frames . length - 1 )
2014-01-03 13:32:13 -05:00
2014-09-06 22:50:31 -04:00
2014-09-05 12:20:29 -04:00
#- 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
2014-01-03 13:32:13 -05:00
onLevelRestarted: (e) ->
@ setProgress 0 , 0
onSetCamera: (e) ->
if e . thangID
2014-09-28 17:16:56 -04:00
return unless target = @ lankBoss . lankFor ( e . thangID ) ? . sprite
2014-01-03 13:32:13 -05:00
else if e . pos
target = @ camera . worldToSurface e . pos
else
target = null
@ camera . setBounds e . bounds if e . bounds
2014-09-29 13:18:27 -04:00
# @cameraBorder.updateBounds @camera.bounds
2014-02-19 15:43:25 -05:00
@ camera . zoomTo target , e . zoom , e . duration # TODO: SurfaceScriptModule perhaps shouldn't assign e.zoom if not set
2014-01-03 13:32:13 -05:00
2014-05-12 18:54:07 -04:00
onZoomUpdated: (e) ->
if @ ended
@ setPaused false
@surfaceZoomPauseTimeout = _ . delay ( => @ setPaused true ) , 3000
2014-11-23 00:22:46 -05:00
@zoomedIn = e . zoom > e . minZoom * 1.1
@ updateGrabbability ( )
updateGrabbability: ->
@ webGLCanvas . toggleClass ' grabbable ' , @ zoomedIn and not @ playing and not @ disabled
2014-05-12 18:54:07 -04:00
2014-01-03 13:32:13 -05:00
onDisableControls: (e) ->
return if e . controls and not ( ' surface ' in e . controls )
@ setDisabled true
@ dimmer ? = new Dimmer camera: @ camera , layer: @ screenLayer
2014-09-30 16:44:03 -04:00
@ dimmer . setSprites @ lankBoss . lanks
2014-01-03 13:32:13 -05:00
onEnableControls: (e) ->
return if e . controls and not ( ' surface ' in e . controls )
@ setDisabled false
onSetLetterbox: (e) ->
@ setDisabled e . on
2014-09-05 12:20:29 -04:00
setDisabled: (@disabled) ->
2014-09-28 17:00:48 -04:00
@lankBoss.disabled = @ disabled
2014-11-23 00:22:46 -05:00
@ updateGrabbability ( )
2014-09-05 12:20:29 -04:00
2014-02-11 15:54:08 -05:00
onSetPlaying: (e) ->
2014-01-03 13:32:13 -05:00
@playing = ( e ? { } ) . playing ? true
2014-05-20 15:21:43 -04:00
@setPlayingCalled = true
2014-01-03 13:32:13 -05:00
if @ playing and @ currentFrame >= ( @ world . totalFrames - 5 )
2014-11-19 18:46:50 -05:00
@currentFrame = 1 # Go back to the beginning (but not frame 0, that frame is weird)
2014-08-23 12:52:05 -04:00
if @ fastForwardingToFrame and not @ playing
@fastForwardingToFrame = null
2014-11-23 00:22:46 -05:00
@ updateGrabbability ( )
2014-01-03 13:32:13 -05:00
2014-02-11 15:54:08 -05:00
onSetTime: (e) ->
2014-01-03 13:32:13 -05:00
toFrame = @ currentFrame
if e . time ?
2014-08-22 20:11:40 -04:00
@worldLifespan = @ world . frames . length / @ world . frameRate
2014-01-03 13:32:13 -05:00
e.ratio = e . time / @ worldLifespan
if e . ratio ?
2014-08-22 20:11:40 -04:00
toFrame = @ world . frames . length * e . ratio
2014-01-03 13:32:13 -05:00
if e . frameOffset
toFrame += e . frameOffset
if e . ratioOffset
2014-08-22 20:11:40 -04:00
toFrame += @ world . frames . length * e . ratioOffset
2014-01-03 13:32:13 -05:00
unless _ . isNumber ( toFrame ) and not _ . isNaN ( toFrame )
return console . error ( ' set-time event ' , e , ' produced invalid target frame ' , toFrame )
2014-08-22 20:11:40 -04:00
@ setProgress ( toFrame / @ world . frames . length , e . scrubDuration )
2014-01-03 13:32:13 -05:00
2014-05-11 20:42:32 -04:00
onCastSpells: (e) ->
return if e . preload
2014-05-12 18:54:07 -04:00
@ setPaused false if @ ended
2014-02-24 17:40:28 -05:00
@casting = true
2014-08-22 00:23:45 -04:00
@setPlayingCalled = false # Don't overwrite playing settings if they changed by, say, scripts.
@frameBeforeCast = @ currentFrame
2014-09-25 01:07:55 -04:00
# 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
2014-01-03 13:32:13 -05:00
onNewWorld: (event) ->
return unless event . world . name is @ world . name
2014-08-26 16:56:57 -04:00
@ onStreamingWorldUpdated event
onStreamingWorldUpdated: (event) ->
2014-02-24 17:40:28 -05:00
@casting = false
2014-09-28 17:00:48 -04:00
@ lankBoss . play ( )
2014-02-28 14:27:32 -05:00
2014-02-25 13:50:12 -05:00
# 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.
2014-08-27 15:24:03 -04:00
Backbone . Mediator . publish ' level:set-playing ' , { playing: true } unless event . firstWorld or @ setPlayingCalled
2014-08-22 00:23:45 -04:00
@ setWorld event . world
@ onFrameChanged ( true )
2014-08-24 01:24:00 -04:00
fastForwardBuffer = 2
2014-09-25 00:01:58 -04:00
if @ playing and not @ realTime and ( ffToFrame = Math . min ( event . firstChangedFrame , @ frameBeforeCast , @ world . frames . length - 1 ) ) and ffToFrame > @ currentFrame + fastForwardBuffer * @ world . frameRate
2014-08-23 12:52:05 -04:00
@fastForwardingToFrame = ffToFrame
2014-09-25 01:07:55 -04:00
@fastForwardingSpeed = Math . max 3 , 3 * ( @ world . maxTotalFrames * @ world . dt ) / 60
2014-08-24 01:24:00 -04:00
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
2014-11-18 14:46:42 -05:00
# 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
2014-11-18 15:40:28 -05:00
if event . finished
@ updatePaths ( )
else
@ hidePaths ( )
2014-01-03 13:32:13 -05:00
2014-09-05 12:20:29 -04:00
onIdleChanged: (e) ->
@ setPaused e . idle unless @ ended
2014-01-03 13:32:13 -05:00
2014-09-05 12:20:29 -04:00
#- Mouse event callbacks
2014-01-03 13:32:13 -05:00
onMouseMove: (e) =>
2014-05-14 18:29:55 -04:00
@mouseScreenPos = { x: e . stageX , y: e . stageY }
2014-01-03 13:32:13 -05:00
return if @ disabled
Backbone . Mediator . publish ' surface:mouse-moved ' , x: e . stageX , y: e . stageY
onMouseDown: (e) =>
return if @ disabled
2014-09-11 16:05:20 -04:00
cap = @ camera . screenToCanvas ( { x: e . stageX , y: e . stageY } )
2014-09-02 19:41:31 -04:00
# getObject(s)UnderPoint is broken, so we have to use the private method to get what we want
2014-09-25 13:47:53 -04:00
onBackground = not @ webGLStage . _getObjectsUnderPoint ( e . stageX , e . stageY , null , true )
2014-09-03 21:35:14 -04:00
2014-09-11 16:05:20 -04:00
wop = @ camera . screenToWorld x: e . stageX , y: e . stageY
event = onBackground: onBackground , x: e . stageX , y: e . stageY , originalEvent: e , worldPos: wop
2014-08-23 20:26:56 -04:00
Backbone . Mediator . publish ' surface:stage-mouse-down ' , event
2014-08-28 17:00:54 -04:00
Backbone . Mediator . publish ' tome:focus-editor ' , { }
2014-09-21 23:49:45 -04:00
@mouseIsDown = true
2014-01-03 13:32:13 -05:00
2014-05-20 19:20:24 -04:00
onMouseUp: (e) =>
return if @ disabled
2014-09-25 13:47:53 -04:00
onBackground = not @ webGLStage . hitTest e . stageX , e . stageY
2014-05-20 19:20:24 -04:00
Backbone . Mediator . publish ' surface:stage-mouse-up ' , onBackground: onBackground , x: e . stageX , y: e . stageY , originalEvent: e
2014-08-28 17:00:54 -04:00
Backbone . Mediator . publish ' tome:focus-editor ' , { }
2014-09-21 23:49:45 -04:00
@mouseIsDown = false
2014-05-20 19:20:24 -04:00
2014-02-12 15:41:41 -05:00
onMouseWheel: (e) =>
# https://github.com/brandonaaron/jquery-mousewheel
e . preventDefault ( )
return if @ disabled
2014-02-15 16:45:16 -05:00
event =
deltaX: e . deltaX
deltaY: e . deltaY
2014-09-25 13:47:53 -04:00
canvas: @ webGLCanvas
2014-08-27 21:05:18 -04:00
event.screenPos = @ mouseScreenPos if @ mouseScreenPos
2014-02-15 16:45:16 -05:00
Backbone . Mediator . publish ' surface:mouse-scrolled ' , event unless @ disabled
2014-01-03 13:32:13 -05:00
2014-09-05 12:20:29 -04:00
#- Canvas callbacks
2014-01-03 13:32:13 -05:00
2014-09-05 12:20:29 -04:00
onResize: (e) =>
2014-09-15 18:38:07 -04:00
return if @ destroyed or @ options . choosing
2014-09-25 13:47:53 -04:00
oldWidth = parseInt @ normalCanvas . attr ( ' width ' ) , 10
oldHeight = parseInt @ normalCanvas . attr ( ' height ' ) , 10
2014-09-05 12:20:29 -04:00
aspectRatio = oldWidth / oldHeight
pageWidth = $ ( ' # page-container ' ) . width ( ) - 17 # 17px nano scroll bar
2014-10-01 15:08:14 -04:00
if application . isIPadApp
newWidth = 1024
newHeight = newWidth / aspectRatio
else if @ realTime or @ options . spectateGame
2014-09-05 12:20:29 -04:00
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
2014-10-17 11:47:53 -04:00
return if newWidth is oldWidth and newHeight is oldHeight and not @ options . spectateGame
2015-02-27 17:07:07 -05:00
return if newWidth < 200 or newHeight < 200
2014-10-02 01:02:52 -04:00
#scaleFactor = if application.isIPadApp then 2 else 1 # Retina
scaleFactor = 1
2014-09-25 13:47:53 -04:00
@ normalCanvas . add ( @ webGLCanvas ) . attr width: newWidth * scaleFactor , height: newHeight * scaleFactor
2014-10-01 15:08:14 -04:00
2014-09-26 14:07:01 -04:00
# Cannot do this to the webGLStage because it does not use scaleX/Y.
2014-10-01 15:08:14 -04:00
# Instead the LayerAdapter scales webGL-enabled layers.
2014-09-26 14:07:01 -04:00
@ webGLStage . updateViewport ( @ webGLCanvas [ 0 ] . width , @ webGLCanvas [ 0 ] . height )
@ normalStage . scaleX *= newWidth / oldWidth
@ normalStage . scaleY *= newHeight / oldHeight
2014-09-05 12:20:29 -04:00
@ camera . onResize newWidth , newHeight
2014-10-17 11:47:53 -04:00
if @ options . spectateGame
2015-02-12 22:47:57 -05:00
# 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
2014-01-03 13:32:13 -05:00
2014-09-21 23:49:45 -04:00
#- Camera focus on hero
focusOnHero: ->
2014-11-18 14:46:42 -05:00
hadHero = @ heroLank
2014-09-30 16:41:42 -04:00
@heroLank = @ lankBoss . lankFor ' Hero Placeholder '
2014-10-18 17:51:43 -04:00
if me . team is ' ogres '
# TODO: do this for real
@heroLank = @ lankBoss . lankFor ' Hero Placeholder 1 '
2014-11-18 14:46:42 -05:00
@ updatePaths ( ) if not hadHero
2014-01-03 13:32:13 -05:00
2014-09-05 12:20:29 -04:00
#- Real-time playback
2014-09-06 22:50:31 -04:00
2014-08-30 00:46:26 -04:00
onRealTimePlaybackWaiting: (e) ->
@ onRealTimePlaybackStarted e
2014-08-23 16:54:52 -04:00
onRealTimePlaybackStarted: (e) ->
2014-08-30 00:46:26 -04:00
return if @ realTime
2014-08-23 16:54:52 -04:00
@realTime = true
@ onResize ( )
2014-08-26 01:05:24 -04:00
@playing = false # Will start when countdown is done.
2014-09-30 16:41:42 -04:00
if @ heroLank
2014-09-24 01:10:18 -04:00
@previousCameraZoom = @ camera . zoom
2014-11-17 12:09:04 -05:00
#@camera.zoomTo @heroLank.sprite, 2, 3000 # This makes flag placement hard, now that we're only rarely using this as a coolcam.
2014-08-23 16:54:52 -04:00
onRealTimePlaybackEnded: (e) ->
2014-08-31 19:05:15 -04:00
return unless @ realTime
2014-08-23 16:54:52 -04:00
@realTime = false
@ onResize ( )
2014-09-25 12:48:14 -04:00
_ . delay @ onResize , resizeDelay + 100 # Do it again just to be double sure that we don't stay zoomed in due to timing problems.
2014-09-25 13:47:53 -04:00
@ normalCanvas . add ( @ webGLCanvas ) . removeClass ' flag-color-selected '
2014-09-24 01:10:18 -04:00
if @ previousCameraZoom
@ camera . zoomTo @ camera . newTarget or @ camera . target , @ previousCameraZoom , 3000
2014-08-23 20:26:56 -04:00
2014-08-24 19:09:06 -04:00
onFlagColorSelected: (e) ->
2014-09-25 13:47:53 -04:00
@ normalCanvas . add ( @ webGLCanvas ) . toggleClass ' flag-color-selected ' , Boolean ( e . color )
2014-08-24 19:09:06 -04:00
e.pos = @ camera . screenToWorld @ mouseScreenPos if @ mouseScreenPos
2014-08-23 16:54:52 -04:00
2014-09-06 22:50:31 -04:00
2014-01-03 13:32:13 -05:00
updatePaths: ->
2014-11-18 14:46:42 -05:00
return unless @ options . paths and @ heroLank
2014-01-03 13:32:13 -05:00
@ hidePaths ( )
return if @ world . showPaths is ' never '
2014-11-18 14:46:42 -05:00
layerAdapter = @ lankBoss . layerAdapters [ ' Path ' ]
@ trailmaster ? = new TrailMaster @ camera , layerAdapter
@paths = @ trailmaster . generatePaths @ world , @ heroLank . thang
2014-01-03 13:32:13 -05:00
@paths.name = ' paths '
2014-11-18 14:46:42 -05:00
layerAdapter . addChild @ paths
2014-01-03 13:32:13 -05:00
hidePaths: ->
return if not @ paths
2014-09-25 13:47:53 -04:00
if @ paths . parent
@ paths . parent . removeChild @ paths
2014-01-03 13:32:13 -05:00
@paths = null
2014-09-06 22:50:31 -04:00
2014-09-05 12:20:29 -04:00
#- Screenshot
2014-01-03 13:32:13 -05:00
screenshot: (scale=0.25, format='image/jpeg', quality=0.8, zoom=2) ->
2014-09-25 13:47:53 -04:00
# TODO: get screenshots working again
2014-01-03 13:32:13 -05:00
# Quality doesn't work with image/png, just image/jpeg and image/webp
2014-12-19 21:37:42 -05:00
[ w , h ] = [ @ camera . canvasWidth * @ camera . canvasScaleFactorX , @ camera . canvasHeight * @ camera . canvasScaleFactorY ]
2014-01-03 13:32:13 -05:00
margin = ( 1 - 1 / zoom ) / 2
2014-09-25 13:47:53 -04:00
@ webGLStage . cache margin * w , margin * h , w / zoom , h / zoom , scale * zoom
imageData = @ webGLStage . cacheCanvas . toDataURL ( format , quality )
2014-06-30 22:16:26 -04:00
#console.log 'Screenshot with scale', scale, 'format', format, 'quality', quality, 'was', Math.floor(imageData.length / 1024), 'kB'
screenshot = document . createElement ( ' img ' )
2014-01-03 13:32:13 -05:00
screenshot.src = imageData
2014-09-25 13:47:53 -04:00
@ webGLStage . uncache ( )
2014-01-03 13:32:13 -05:00
imageData
2014-09-05 12:20:29 -04:00
2014-09-06 22:50:31 -04:00
2014-09-05 12:20:29 -04:00
#- Path finding debugging
2014-09-06 22:50:31 -04:00
2014-09-05 12:20:29 -04:00
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
2014-09-06 22:50:31 -04:00
2014-09-05 12:20:29 -04:00
#- Teardown
2014-09-06 22:50:31 -04:00
2014-09-05 12:20:29 -04:00
destroy: ->
@ camera ? . destroy ( )
createjs . Ticker . removeEventListener ( ' tick ' , @ tick )
createjs . Sound . stop ( )
2014-09-25 13:47:53 -04:00
layer . destroy ( ) for layer in @ normalLayers
2014-09-28 17:00:48 -04:00
@ lankBoss . destroy ( )
2014-09-05 12:20:29 -04:00
@ chooser ? . destroy ( )
@ dimmer ? . destroy ( )
@ countdownScreen ? . destroy ( )
@ playbackOverScreen ? . destroy ( )
@ waitingScreen ? . destroy ( )
@ coordinateDisplay ? . destroy ( )
@ coordinateGrid ? . destroy ( )
2014-09-25 13:47:53 -04:00
@ normalStage . clear ( )
@ webGLStage . clear ( )
2014-09-05 12:20:29 -04:00
@ musicPlayer ? . destroy ( )
2014-11-18 14:46:42 -05:00
@ trailmaster ? . destroy ( )
2014-09-25 13:47:53 -04:00
@ 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
2014-09-26 14:07:01 -04:00
@ webGLCanvas . off ' mousewheel ' , @ onMouseWheel
2014-09-05 12:20:29 -04:00
$ ( window ) . off ' resize ' , @ onResize
clearTimeout @ surfacePauseTimeout if @ surfacePauseTimeout
clearTimeout @ surfaceZoomPauseTimeout if @ surfaceZoomPauseTimeout
super ( )