codecombat/app/lib/surface/Surface.coffee
Scott Erickson 4dda1b67dd Refactor ThangsTabView to use GameUIState for managing all Surface mouse events
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.
2016-06-28 09:19:38 -07:00

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()