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' 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-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() $(window).on('keydown', @onKeyEvent) $(window).on('keyup', @onKeyEvent) @normalLayers = [] @options = _.clone(@defaults) @options = _.extend(@options, givenOptions) if givenOptions @handleEvents = @options.handleEvents ? true @gameUIState = @options.gameUIState or new GameUIState({ canDragCamera: true }) @realTimeInputEvents = @gameUIState.get('realTimeInputEvents') @listenTo(@gameUIState, 'sprite:mouse-down', @onSpriteMouseDown) @onResize = _.debounce @onResize, resizeDelay @initEasel() @initAudio() $(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 unless @options.levelType is 'game-dev' @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. @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()) return unless frame 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 (not @world.indefiniteLength) and @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 onSpriteMouseDown: (e) => return unless @realTime @realTimeInputEvents.add({ type: 'mousedown' pos: @camera.screenToWorld x: e.originalEvent.stageX, y: e.originalEvent.stageY time: @world.dt * @world.frames.length thangID: e.sprite.thang.id }) 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) #- Keyboard callbacks onKeyEvent: (e) => return unless @realTime @realTimeInputEvents.add(_.pick(e, 'type', 'keyCode', 'ctrlKey', 'metaKey', 'shiftKey')) #- 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 @options.resizeStrategy is 'wrapper-size' newWidth = $('#canvas-wrapper').width() 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 @trigger 'resize', { 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 onRealTimePlaybackStarted: (e) -> return if @realTime @realTimeInputEvents.reset() @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() @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 $(window).off('keydown', @onKeyEvent) $(window).off('keyup', @onKeyEvent) clearTimeout @surfacePauseTimeout if @surfacePauseTimeout clearTimeout @surfaceZoomPauseTimeout if @surfaceZoomPauseTimeout super()