mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-17 19:12:33 -05:00
773 lines
29 KiB
CoffeeScript
773 lines
29 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'
|
|
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()
|