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.
This commit is contained in:
Scott Erickson 2016-06-22 11:20:21 -07:00
parent 20ec35b85f
commit 4dda1b67dd
6 changed files with 208 additions and 117 deletions

View file

@ -1,4 +1,5 @@
CocoClass = require 'core/CocoClass' CocoClass = require 'core/CocoClass'
GameUIState = require 'models/GameUIState'
# If I were the kind of math major who remembered his math, this would all be done with matrix transforms. # If I were the kind of math major who remembered his math, this would all be done with matrix transforms.
@ -44,12 +45,17 @@ module.exports = class Camera extends CocoClass
'camera:zoom-out': 'onZoomOut' 'camera:zoom-out': 'onZoomOut'
'camera:zoom-to': 'onZoomTo' 'camera:zoom-to': 'onZoomTo'
'level:restarted': 'onLevelRestarted' 'level:restarted': 'onLevelRestarted'
'surface:mouse-scrolled': 'onMouseScrolled'
'sprite:mouse-down': 'onMouseDown'
'sprite:dragged': 'onMouseDragged'
constructor: (@canvas, angle=Math.asin(0.75), hFOV=d2r(30)) -> constructor: (@canvas, @options={}) ->
angle=Math.asin(0.75)
hFOV=d2r(30)
super() super()
@gameUIState = @options.gameUIState or new GameUIState()
@listenTo @gameUIState, 'surface:stage-mouse-move', @onMouseMove
@listenTo @gameUIState, 'surface:stage-mouse-down', @onMouseDown
@listenTo @gameUIState, 'surface:stage-mouse-up', @onMouseUp
@listenTo @gameUIState, 'surface:mouse-scrolled', @onMouseScrolled
@handleEvents = @options.handleEvents ? true
@canvasWidth = parseInt(@canvas.attr('width'), 10) @canvasWidth = parseInt(@canvas.attr('width'), 10)
@canvasHeight = parseInt(@canvas.attr('height'), 10) @canvasHeight = parseInt(@canvas.attr('height'), 10)
@offset = {x: 0, y: 0} @offset = {x: 0, y: 0}
@ -155,8 +161,27 @@ module.exports = class Camera extends CocoClass
onZoomIn: (e) -> @zoomTo @target, @zoom * 1.15, 300 onZoomIn: (e) -> @zoomTo @target, @zoom * 1.15, 300
onZoomOut: (e) -> @zoomTo @target, @zoom / 1.15, 300 onZoomOut: (e) -> @zoomTo @target, @zoom / 1.15, 300
onMouseDown: (e) ->
return if @dragDisabled
@lastPos = {x: e.originalEvent.rawX, y: e.originalEvent.rawY}
@mousePressed = true
onMouseMove: (e) ->
return unless @mousePressed and @gameUIState.get('canDragCamera')
return if @dragDisabled
target = @boundTarget(@target, @zoom)
newPos =
x: target.x + (@lastPos.x - e.originalEvent.rawX) / @zoom
y: target.y + (@lastPos.y - e.originalEvent.rawY) / @zoom
@zoomTo newPos, @zoom, 0
@lastPos = {x: e.originalEvent.rawX, y: e.originalEvent.rawY}
Backbone.Mediator.publish 'camera:dragged', {}
onMouseUp: (e) ->
@mousePressed = false
onMouseScrolled: (e) -> onMouseScrolled: (e) ->
return unless e.canvas is @canvas
ratio = 1 + 0.05 * Math.sqrt(Math.abs(e.deltaY)) ratio = 1 + 0.05 * Math.sqrt(Math.abs(e.deltaY))
ratio = 1 / ratio if e.deltaY > 0 ratio = 1 / ratio if e.deltaY > 0
newZoom = @zoom * ratio newZoom = @zoom * ratio
@ -174,22 +199,6 @@ module.exports = class Camera extends CocoClass
target = @target target = @target
@zoomTo target, newZoom, 0 @zoomTo target, newZoom, 0
onMouseDown: (e) ->
return unless e.canvas is @canvas[0]
return if @dragDisabled
@lastPos = {x: e.originalEvent.rawX, y: e.originalEvent.rawY}
onMouseDragged: (e) ->
return unless e.canvas is @canvas[0]
return if @dragDisabled
target = @boundTarget(@target, @zoom)
newPos =
x: target.x + (@lastPos.x - e.originalEvent.rawX) / @zoom
y: target.y + (@lastPos.y - e.originalEvent.rawY) / @zoom
@zoomTo newPos, @zoom, 0
@lastPos = {x: e.originalEvent.rawX, y: e.originalEvent.rawY}
Backbone.Mediator.publish 'camera:dragged', {}
onLevelRestarted: -> onLevelRestarted: ->
@setBounds(@firstBounds, false) @setBounds(@firstBounds, false)

View file

@ -57,12 +57,14 @@ module.exports = Lank = class Lank extends CocoClass
'level:set-letterbox': 'onSetLetterbox' 'level:set-letterbox': 'onSetLetterbox'
'surface:ticked': 'onSurfaceTicked' 'surface:ticked': 'onSurfaceTicked'
'sprite:move': 'onMove' 'sprite:move': 'onMove'
constructor: (@thangType, options) -> constructor: (@thangType, options={}) ->
super() super()
spriteName = @thangType.get('name') spriteName = @thangType.get('name')
@isMissile = /(Missile|Arrow|Spear|Bolt)/.test(spriteName) and not /(Tower|Charge)/.test(spriteName) @isMissile = /(Missile|Arrow|Spear|Bolt)/.test(spriteName) and not /(Tower|Charge)/.test(spriteName)
@options = _.extend($.extend(true, {}, @options), options) @options = _.extend($.extend(true, {}, @options), options)
@gameUIState = @options.gameUIState
@handleEvents = @options.handleEvents
@setThang @options.thang @setThang @options.thang
if @thang? if @thang?
options = @thang?.getLankOptions?() options = @thang?.getLankOptions?()
@ -496,6 +498,7 @@ module.exports = Lank = class Lank extends CocoClass
newEvent = sprite: @, thang: @thang, originalEvent: e, canvas: p.canvas newEvent = sprite: @, thang: @thang, originalEvent: e, canvas: p.canvas
@trigger ourEventName, newEvent @trigger ourEventName, newEvent
Backbone.Mediator.publish ourEventName, newEvent Backbone.Mediator.publish ourEventName, newEvent
@gameUIState.trigger(ourEventName, newEvent)
addHealthBar: -> addHealthBar: ->
return unless @thang?.health? and 'health' in (@thang?.hudProperties ? []) and @options.floatingLayer return unless @thang?.health? and 'health' in (@thang?.hudProperties ? []) and @options.floatingLayer

View file

@ -25,10 +25,11 @@ module.exports = class LankBoss extends CocoClass
'surface:flag-appeared': 'onFlagAppeared' 'surface:flag-appeared': 'onFlagAppeared'
'surface:remove-selected-flag': 'onRemoveSelectedFlag' 'surface:remove-selected-flag': 'onRemoveSelectedFlag'
constructor: (@options) -> constructor: (@options={}) ->
super() super()
@handleEvents = @options.handleEvents
@gameUIState = @options.gameUIState
@dragged = 0 @dragged = 0
@options ?= {}
@camera = @options.camera @camera = @options.camera
@webGLStage = @options.webGLStage @webGLStage = @options.webGLStage
@surfaceTextLayer = @options.surfaceTextLayer @surfaceTextLayer = @options.surfaceTextLayer
@ -38,6 +39,8 @@ module.exports = class LankBoss extends CocoClass
@lankArray = [] # Mirror @lanks, but faster for when we just need to iterate @lankArray = [] # Mirror @lanks, but faster for when we just need to iterate
@createLayers() @createLayers()
@pendingFlags = [] @pendingFlags = []
if not @handleEvents
@listenTo @gameUIState, 'change:selected', @onChangeSelected
destroy: -> destroy: ->
@removeLank lank for thangID, lank of @lanks @removeLank lank for thangID, lank of @lanks
@ -93,7 +96,16 @@ module.exports = class LankBoss extends CocoClass
@selectionMark = new Mark name: 'selection', camera: @camera, layer: @layerAdapters['Ground'], thangType: 'selection' @selectionMark = new Mark name: 'selection', camera: @camera, layer: @layerAdapters['Ground'], thangType: 'selection'
createLankOptions: (options) -> createLankOptions: (options) ->
_.extend options, camera: @camera, resolutionFactor: SPRITE_RESOLUTION_FACTOR, groundLayer: @layerAdapters['Ground'], textLayer: @surfaceTextLayer, floatingLayer: @layerAdapters['Floating'], showInvisible: @options.showInvisible _.extend options, {
@camera
resolutionFactor: SPRITE_RESOLUTION_FACTOR
groundLayer: @layerAdapters['Ground']
textLayer: @surfaceTextLayer
floatingLayer: @layerAdapters['Floating']
showInvisible: @options.showInvisible
@gameUIState
@handleEvents
}
onSetDebug: (e) -> onSetDebug: (e) ->
return if e.debug is @debug return if e.debug is @debug
@ -256,6 +268,7 @@ module.exports = class LankBoss extends CocoClass
@dragged += 1 @dragged += 1
onLankMouseUp: (e) -> onLankMouseUp: (e) ->
return unless @handleEvents
return if key.shift #and @options.choosing return if key.shift #and @options.choosing
return @dragged = 0 if @dragged > 3 return @dragged = 0 if @dragged > 3
@dragged = 0 @dragged = 0
@ -264,9 +277,17 @@ module.exports = class LankBoss extends CocoClass
@selectLank e, lank @selectLank e, lank
onStageMouseDown: (e) -> onStageMouseDown: (e) ->
return unless @handleEvents
return if key.shift #and @options.choosing return if key.shift #and @options.choosing
@selectLank e if e.onBackground @selectLank e if e.onBackground
onChangeSelected: (gameUIState, selected) ->
if selected
@selectThang(selected.thang.id, selected.spellName)
else
@selectedLank?.selected = false
@selectedLank = null
selectThang: (thangID, spellName=null, treemaThangSelected = null) -> selectThang: (thangID, spellName=null, treemaThangSelected = null) ->
return @willSelectThang = [thangID, spellName] unless @lanks[thangID] return @willSelectThang = [thangID, spellName] unless @lanks[thangID]
@selectLank null, @lanks[thangID], spellName, treemaThangSelected @selectLank null, @lanks[thangID], spellName, treemaThangSelected
@ -275,8 +296,9 @@ module.exports = class LankBoss extends CocoClass
return if e and (@disabled or @selectLocked) # Ignore clicks for selection/panning/wizard movement while disabled or select is locked return if e and (@disabled or @selectLocked) # Ignore clicks for selection/panning/wizard movement while disabled or select is locked
worldPos = lank?.thang?.pos worldPos = lank?.thang?.pos
worldPos ?= @camera.screenToWorld {x: e.originalEvent.rawX, y: e.originalEvent.rawY} if e?.originalEvent worldPos ?= @camera.screenToWorld {x: e.originalEvent.rawX, y: e.originalEvent.rawY} if e?.originalEvent
if (not @reallyStopMoving) and worldPos and (@options.navigateToSelection or not lank or treemaThangSelected) and e?.originalEvent?.nativeEvent?.which isnt 3 if @handleEvents
@camera.zoomTo(lank?.sprite or @camera.worldToSurface(worldPos), @camera.zoom, 1000, true) if (not @reallyStopMoving) and worldPos and (@options.navigateToSelection or not lank or treemaThangSelected) and e?.originalEvent?.nativeEvent?.which isnt 3
@camera.zoomTo(lank?.sprite or @camera.worldToSurface(worldPos), @camera.zoom, 1000, true)
lank = null if @options.choosing # Don't select lanks while choosing lank = null if @options.choosing # Don't select lanks while choosing
if lank isnt @selectedLank if lank isnt @selectedLank
@selectedLank?.selected = false @selectedLank?.selected = false

View file

@ -18,6 +18,7 @@ LankBoss = require './LankBoss'
PointChooser = require './PointChooser' PointChooser = require './PointChooser'
RegionChooser = require './RegionChooser' RegionChooser = require './RegionChooser'
MusicPlayer = require './MusicPlayer' MusicPlayer = require './MusicPlayer'
GameUIState = require 'models/GameUIState'
resizeDelay = 500 # At least as much as $level-resize-transition-time. resizeDelay = 500 # At least as much as $level-resize-transition-time.
@ -87,6 +88,10 @@ module.exports = Surface = class Surface extends CocoClass
@normalLayers = [] @normalLayers = []
@options = _.clone(@defaults) @options = _.clone(@defaults)
@options = _.extend(@options, givenOptions) if givenOptions @options = _.extend(@options, givenOptions) if givenOptions
@handleEvents = @options.handleEvents ? true
@gameUIState = @options.gameUIState or new GameUIState({
canDragCamera: true
})
@initEasel() @initEasel()
@initAudio() @initAudio()
@onResize = _.debounce @onResize, resizeDelay @onResize = _.debounce @onResize, resizeDelay
@ -98,7 +103,7 @@ module.exports = Surface = class Surface extends CocoClass
@normalStage = new createjs.Stage(@normalCanvas[0]) @normalStage = new createjs.Stage(@normalCanvas[0])
@webGLStage = new createjs.SpriteStage(@webGLCanvas[0]) @webGLStage = new createjs.SpriteStage(@webGLCanvas[0])
@normalStage.nextStage = @webGLStage @normalStage.nextStage = @webGLStage
@camera = new Camera @webGLCanvas @camera = new Camera(@webGLCanvas, { @gameUIState, @handleEvents })
AudioPlayer.camera = @camera unless @options.choosing 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 @surfaceTextLayer = new Layer name: 'Surface Text', layerPriority: 1, transform: Layer.TRANSFORM_SURFACE_TEXT, camera: @camera
@ -112,7 +117,19 @@ module.exports = Surface = class Surface extends CocoClass
canvasHeight = parseInt @normalCanvas.attr('height'), 10 canvasHeight = parseInt @normalCanvas.attr('height'), 10
@screenLayer.addChild new Letterbox canvasWidth: canvasWidth, canvasHeight: canvasHeight @screenLayer.addChild new Letterbox canvasWidth: canvasWidth, canvasHeight: canvasHeight
@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 @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 @countdownScreen = new CountdownScreen camera: @camera, layer: @screenLayer, showsCountdown: @world.showsCountdown
@playbackOverScreen = new PlaybackOverScreen camera: @camera, layer: @screenLayer, playerNames: @options.playerNames @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. @normalStage.addChildAt @playbackOverScreen.dimLayer, 0 # Put this below the other layers, actually, so we can more easily read text on the screen.
@ -121,7 +138,7 @@ module.exports = Surface = class Surface extends CocoClass
@webGLStage.enableMouseOver(10) @webGLStage.enableMouseOver(10)
@webGLStage.addEventListener 'stagemousemove', @onMouseMove @webGLStage.addEventListener 'stagemousemove', @onMouseMove
@webGLStage.addEventListener 'stagemousedown', @onMouseDown @webGLStage.addEventListener 'stagemousedown', @onMouseDown
@webGLCanvas[0].addEventListener 'mouseup', @onMouseUp @webGLStage.addEventListener 'stagemouseup', @onMouseUp
@webGLCanvas.on 'mousewheel', @onMouseWheel @webGLCanvas.on 'mousewheel', @onMouseWheel
@hookUpChooseControls() if @options.choosing # TODO: figure this stuff out @hookUpChooseControls() if @options.choosing # TODO: figure this stuff out
createjs.Ticker.timingMode = createjs.Ticker.RAF_SYNCHED createjs.Ticker.timingMode = createjs.Ticker.RAF_SYNCHED
@ -222,8 +239,9 @@ module.exports = Surface = class Surface extends CocoClass
updateState: (frameChanged) -> updateState: (frameChanged) ->
# world state must have been restored in @restoreWorldState # world state must have been restored in @restoreWorldState
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 if @handleEvents
@camera.zoomTo @heroLank.sprite, @camera.zoom, 750 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 @lankBoss.update frameChanged
@camera.updateZoom() # Make sure to do this right after the LankBoss updates, not before, so it can properly target sprite positions. @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 @dimmer?.setSprites @lankBoss.lanks
@ -371,7 +389,8 @@ module.exports = Surface = class Surface extends CocoClass
target = null target = null
@camera.setBounds e.bounds if e.bounds @camera.setBounds e.bounds if e.bounds
# @cameraBorder.updateBounds @camera.bounds # @cameraBorder.updateBounds @camera.bounds
@camera.zoomTo target, e.zoom, e.duration # TODO: SurfaceScriptModule perhaps shouldn't assign e.zoom if not set if @handleEvents
@camera.zoomTo target, e.zoom, e.duration # TODO: SurfaceScriptModule perhaps shouldn't assign e.zoom if not set
onZoomUpdated: (e) -> onZoomUpdated: (e) ->
if @ended if @ended
@ -476,6 +495,7 @@ module.exports = Surface = class Surface extends CocoClass
@mouseScreenPos = {x: e.stageX, y: e.stageY} @mouseScreenPos = {x: e.stageX, y: e.stageY}
return if @disabled return if @disabled
Backbone.Mediator.publish 'surface:mouse-moved', x: e.stageX, y: e.stageY Backbone.Mediator.publish 'surface:mouse-moved', x: e.stageX, y: e.stageY
@gameUIState.trigger('surface:stage-mouse-move', { originalEvent: e })
onMouseDown: (e) => onMouseDown: (e) =>
return if @disabled return if @disabled
@ -484,16 +504,19 @@ module.exports = Surface = class Surface extends CocoClass
onBackground = not @webGLStage._getObjectsUnderPoint(e.stageX, e.stageY, null, true) onBackground = not @webGLStage._getObjectsUnderPoint(e.stageX, e.stageY, null, true)
wop = @camera.screenToWorld x: e.stageX, y: e.stageY wop = @camera.screenToWorld x: e.stageX, y: e.stageY
event = onBackground: onBackground, x: e.stageX, y: e.stageY, originalEvent: e, worldPos: wop event = { onBackground: onBackground, x: e.stageX, y: e.stageY, originalEvent: e, worldPos: wop }
Backbone.Mediator.publish 'surface:stage-mouse-down', event Backbone.Mediator.publish 'surface:stage-mouse-down', event
Backbone.Mediator.publish 'tome:focus-editor', {} Backbone.Mediator.publish 'tome:focus-editor', {}
@gameUIState.trigger('surface:stage-mouse-down', event)
@mouseIsDown = true @mouseIsDown = true
onMouseUp: (e) => onMouseUp: (e) =>
return if @disabled return if @disabled
onBackground = not @webGLStage.hitTest e.stageX, e.stageY onBackground = not @webGLStage.hitTest e.stageX, e.stageY
Backbone.Mediator.publish 'surface:stage-mouse-up', onBackground: onBackground, x: e.stageX, y: e.stageY, originalEvent: e 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', {} Backbone.Mediator.publish 'tome:focus-editor', {}
@gameUIState.trigger('surface:stage-mouse-up', event)
@mouseIsDown = false @mouseIsDown = false
onMouseWheel: (e) => onMouseWheel: (e) =>
@ -506,6 +529,7 @@ module.exports = Surface = class Surface extends CocoClass
canvas: @webGLCanvas canvas: @webGLCanvas
event.screenPos = @mouseScreenPos if @mouseScreenPos event.screenPos = @mouseScreenPos if @mouseScreenPos
Backbone.Mediator.publish 'surface:mouse-scrolled', event unless @disabled Backbone.Mediator.publish 'surface:mouse-scrolled', event unless @disabled
@gameUIState.trigger('surface:mouse-scrolled', event)
#- Canvas callbacks #- Canvas callbacks
@ -585,8 +609,9 @@ module.exports = Surface = class Surface extends CocoClass
@onResize() @onResize()
_.delay @onResize, resizeDelay + 100 # Do it again just to be double sure that we don't stay zoomed in due to timing problems. _.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' @normalCanvas.add(@webGLCanvas).removeClass 'flag-color-selected'
if @previousCameraZoom if @handleEvents
@camera.zoomTo @camera.newTarget or @camera.target, @previousCameraZoom, 3000 if @previousCameraZoom
@camera.zoomTo @camera.newTarget or @camera.target, @previousCameraZoom, 3000
onFlagColorSelected: (e) -> onFlagColorSelected: (e) ->
@normalCanvas.add(@webGLCanvas).toggleClass 'flag-color-selected', Boolean(e.color) @normalCanvas.add(@webGLCanvas).toggleClass 'flag-color-selected', Boolean(e.color)

View file

@ -0,0 +1,5 @@
CocoModel = require './CocoModel'
module.exports = class GameUIState extends CocoModel
@className: 'GameUIState'

View file

@ -11,6 +11,7 @@ Thang = require 'lib/world/thang'
LevelThangEditView = require './LevelThangEditView' LevelThangEditView = require './LevelThangEditView'
ComponentsCollection = require 'collections/ComponentsCollection' ComponentsCollection = require 'collections/ComponentsCollection'
require 'vendor/treema' require 'vendor/treema'
GameUIState = require 'models/GameUIState'
# Moving the screen while dragging thangs constants # Moving the screen while dragging thangs constants
MOVE_MARGIN = 0.15 MOVE_MARGIN = 0.15
@ -29,7 +30,6 @@ module.exports = class ThangsTabView extends CocoView
template: thangs_template template: thangs_template
subscriptions: subscriptions:
'surface:sprite-selected': 'onExtantThangSelected'
'surface:mouse-moved': 'onSurfaceMouseMoved' 'surface:mouse-moved': 'onSurfaceMouseMoved'
'surface:mouse-over': 'onSurfaceMouseOver' 'surface:mouse-over': 'onSurfaceMouseOver'
'surface:mouse-out': 'onSurfaceMouseOut' 'surface:mouse-out': 'onSurfaceMouseOut'
@ -39,7 +39,6 @@ module.exports = class ThangsTabView extends CocoView
'editor:view-switched': 'onViewSwitched' 'editor:view-switched': 'onViewSwitched'
'sprite:dragged': 'onSpriteDragged' 'sprite:dragged': 'onSpriteDragged'
'sprite:mouse-up': 'onSpriteMouseUp' 'sprite:mouse-up': 'onSpriteMouseUp'
'sprite:mouse-down': 'onSpriteMouseDown'
'sprite:double-clicked': 'onSpriteDoubleClicked' 'sprite:double-clicked': 'onSpriteDoubleClicked'
'surface:stage-mouse-down': 'onStageMouseDown' 'surface:stage-mouse-down': 'onStageMouseDown'
'surface:stage-mouse-up': 'onStageMouseUp' 'surface:stage-mouse-up': 'onStageMouseUp'
@ -78,6 +77,11 @@ module.exports = class ThangsTabView extends CocoView
constructor: (options) -> constructor: (options) ->
super options super options
@world = options.world @world = options.world
@gameUIState = new GameUIState()
@listenTo(@gameUIState, 'sprite:mouse-down', @onSpriteMouseDown)
@listenTo(@gameUIState, 'surface:stage-mouse-move', @onStageMouseMove)
@listenTo(@gameUIState, 'change:selected', @onChangeSelected)
@willRepositionCamera =true
# should load depended-on Components, too # should load depended-on Components, too
@thangTypes = @supermodel.loadCollection(new ThangTypeSearchCollection(), 'thangs').model @thangTypes = @supermodel.loadCollection(new ThangTypeSearchCollection(), 'thangs').model
@ -203,7 +207,7 @@ module.exports = class ThangsTabView extends CocoView
initSurface: -> initSurface: ->
webGLCanvas = $('canvas#webgl-surface', @$el) webGLCanvas = $('canvas#webgl-surface', @$el)
normalCanvas = $('canvas#normal-surface', @$el) normalCanvas = $('canvas#normal-surface', @$el)
@surface = new Surface @world, normalCanvas, webGLCanvas, { @surface = new Surface(@world, normalCanvas, webGLCanvas, {
paths: false paths: false
coords: true coords: true
grid: true grid: true
@ -212,7 +216,9 @@ module.exports = class ThangsTabView extends CocoView
showInvisible: true showInvisible: true
frameRate: 15 frameRate: 15
levelType: @level.get 'type', true levelType: @level.get 'type', true
} @gameUIState
handleEvents: false
})
@surface.playing = false @surface.playing = false
@surface.setWorld @world @surface.setWorld @world
@surface.lankBoss.suppressSelectionSounds = true @surface.lankBoss.suppressSelectionSounds = true
@ -240,38 +246,60 @@ module.exports = class ThangsTabView extends CocoView
@selectAddThang null, true @selectAddThang null, true
@surface?.lankBoss?.selectLank null, null @surface?.lankBoss?.selectLank null, null
onSpriteMouseDown: (e) ->
@dragged = false
# Sprite clicks happen after stage clicks, but we need to know whether a sprite is being clicked.
# clearTimeout @backgroundAddClickTimeout
# if e.originalEvent.nativeEvent.button == 2
# @onSpriteContextMenu e
onStageMouseDown: (e) -> onStageMouseDown: (e) ->
return unless @addThangLank?.thangType.get('kind') is 'Wall' @dragged = false
@surface.camera.dragDisabled = true @willRepositionCamera = true
@paintingWalls = true @gameUIState.set('canDragCamera', true)
if @addThangLank?.thangType.get('kind') is 'Wall'
@paintingWalls = true
@gameUIState.set('canDragCamera', false)
else if @addThangLank
# We clicked on the background when we had an add Thang selected, so add it
@addThang @addThangType, @addThangLank.thang.pos
@willRepositionCamera = false
else if e.onBackground
@gameUIState.set('selected', null)
onStageMouseMove: (e) ->
@willRepositionCamera = false
onStageMouseUp: (e) -> onStageMouseUp: (e) ->
if @paintingWalls if @willRepositionCamera
# We need to stop painting walls, but we may also stop in onExtantThangSelected. worldPos = @surface.camera.screenToWorld {x: e.originalEvent.rawX, y: e.originalEvent.rawY}
_.defer => @surface.camera.zoomTo(@surface.camera.worldToSurface(worldPos), @surface.camera.zoom, 1000)
@paintingWalls = @paintedWalls = @surface.camera.dragDisabled = false
else if @addThangLank @paintingWalls = false
@surface.camera.lock()
# If we click on the background, we need to add @addThangLank, but not if onSpriteMouseUp will fire.
@backgroundAddClickTimeout = _.defer => @onExtantThangSelected {}
$('#contextmenu').hide() $('#contextmenu').hide()
onSpriteMouseDown: (e) ->
# update selection
# TODO: Handle key.shift, lankBoss.dragged property
selected = null
if e.thang?.isSelectable
selected = { thang: e.thang, sprite: e.sprite, spellName: e.spellName }
if selected and (key.alt or key.meta)
# Clone selected thang instead of selecting it
@willRepositionCamera = false
@selectAddThangType selected.thang.spriteName, selected.thang
selected = null
@gameUIState.set('selected', selected)
if selected
@willRepositionCamera = false
@gameUIState.set('canDragCamera', false)
onSpriteDragged: (e) -> onSpriteDragged: (e) ->
return unless @selectedExtantThang and e.thang?.id is @selectedExtantThang?.id selected = @gameUIState.get('selected')
return unless selected and e.thang?.id is selected.thang.id
@dragged = true @dragged = true
@surface.camera.dragDisabled = true @willRepositionCamera = false
{stageX, stageY} = e.originalEvent {stageX, stageY} = e.originalEvent
cap = @surface.camera.screenToCanvas x: stageX, y: stageY cap = @surface.camera.screenToCanvas x: stageX, y: stageY
wop = @surface.camera.canvasToWorld cap wop = @surface.camera.canvasToWorld cap
wop.z = @selectedExtantThang.depth / 2 wop.z = selected.thang.depth / 2
@adjustThangPos @selectedExtantLank, @selectedExtantThang, wop @adjustThangPos selected.sprite, selected.thang, wop
[w, h] = [@surface.camera.canvasWidth, @surface.camera.canvasHeight] [w, h] = [@surface.camera.canvasWidth, @surface.camera.canvasHeight]
sidebarWidths = ((if @$el.find(id).hasClass('hide') then 0 else (@$el.find(id).outerWidth() / @surface.camera.canvasScaleFactorX)) for id in ['#all-thangs', '#add-thangs-view']) sidebarWidths = ((if @$el.find(id).hasClass('hide') then 0 else (@$el.find(id).outerWidth() / @surface.camera.canvasScaleFactorX)) for id in ['#all-thangs', '#add-thangs-view'])
w -= sidebarWidth for sidebarWidth in sidebarWidths w -= sidebarWidth for sidebarWidth in sidebarWidths
@ -279,24 +307,23 @@ module.exports = class ThangsTabView extends CocoView
@calculateMovement(cap.x / w, cap.y / h, w / h) @calculateMovement(cap.x / w, cap.y / h, w / h)
onSpriteMouseUp: (e) -> onSpriteMouseUp: (e) ->
clearTimeout @backgroundAddClickTimeout selected = @gameUIState.get('selected')
@surface.camera.unlock() if e.originalEvent.nativeEvent.button == 2 and selected
if e.originalEvent.nativeEvent.button == 2 and @selectedExtantThang
@onSpriteContextMenu e @onSpriteContextMenu e
clearInterval(@movementInterval) if @movementInterval? clearInterval(@movementInterval) if @movementInterval?
@movementInterval = null @movementInterval = null
@surface.camera.dragDisabled = false
return unless @selectedExtantThang and e.thang?.id is @selectedExtantThang?.id return unless selected and e.thang?.id is selected.thang.id
pos = @selectedExtantThang.pos pos = selected.thang.pos
thang = _.find(@level.get('thangs') ? [], {id: @selectedExtantThang.id}) thang = _.find(@level.get('thangs') ? [], {id: selected.thang.id})
path = "#{@pathForThang(thang)}/components/original=#{LevelComponent.PhysicalID}" path = "#{@pathForThang(thang)}/components/original=#{LevelComponent.PhysicalID}"
physical = @thangsTreema.get path physical = @thangsTreema.get path
return if not physical or (physical.config.pos.x is pos.x and physical.config.pos.y is pos.y) return if not physical or (physical.config.pos.x is pos.x and physical.config.pos.y is pos.y)
@thangsTreema.set path + '/config/pos', x: pos.x, y: pos.y, z: pos.z @thangsTreema.set path + '/config/pos', x: pos.x, y: pos.y, z: pos.z
onSpriteDoubleClicked: (e) -> onSpriteDoubleClicked: (e) ->
return unless e.thang and not @dragged return unless e.thang
@editThang thangID: e.thang.id @editThang thangID: e.thang.id
onRandomTerrainGenerated: (e) -> onRandomTerrainGenerated: (e) ->
@ -320,35 +347,21 @@ module.exports = class ThangsTabView extends CocoView
@onThangsChanged() @onThangsChanged()
@selectAddThangType null @selectAddThangType null
onChangeSelected: (gameUIState, selected) ->
previousSprite = gameUIState.previousAttributes()?.selected?.sprite
sprite = selected?.sprite
thang = selected?.thang
# TODO: figure out a good way to have all Surface clicks and Treema clicks just proxy in one direction, so we can maintain only one way of handling selection and deletion previousSprite?.setNameLabel?(null) unless previousSprite is sprite
onExtantThangSelected: (e) ->
@selectedExtantLank?.setNameLabel? null unless @selectedExtantLank is e.sprite if thang and not (@addThangLank and @addThangType.get('name') in overlappableThangTypeNames)
@selectedExtantThang = e.thang
@selectedExtantLank = e.sprite
paintedAWall = @paintedWalls
@paintingWalls = @paintedWalls = @surface.camera.dragDisabled = false
if paintedAWall
# Skip adding a wall now, because we already dragged to add one
null
else if e.thang and (key.alt or key.meta)
# We alt-clicked, so create a clone addThang
@selectAddThangType e.thang.spriteName, @selectedExtantThang
else if @justAdded()
# Skip double insert due to extra selection event
null
else if e.thang and not (@addThangLank and @addThangType.get('name') in overlappableThangTypeNames)
# We clicked on a Thang (or its Treema), so select the Thang # We clicked on a Thang (or its Treema), so select the Thang
@selectAddThang null, true @selectAddThang(null, true)
@selectedExtantThangClickTime = new Date() @selectedExtantThangClickTime = new Date()
# Show the label above selected thang, notice that we may get here from thang-edit-view, so it will be selected but no label # Show the label above selected thang, notice that we may get here from thang-edit-view, so it will be selected but no label
@selectedExtantLank.setNameLabel @selectedExtantLank.thangType.get('name') + ': ' + @selectedExtantThang.id sprite.setNameLabel(sprite.thangType.get('name') + ': ' + thang.id)
@selectedExtantLank.updateLabels() sprite.updateLabels()
@selectedExtantLank.updateMarks() sprite.updateMarks()
else if @addThangLank
# We clicked on the background when we had an add Thang selected, so add it
@addThang @addThangType, @addThangLank.thang.pos
@lastAddTime = new Date()
justAdded: -> @lastAddTime and (new Date() - @lastAddTime) < 150 justAdded: -> @lastAddTime and (new Date() - @lastAddTime) < 150
@ -430,6 +443,7 @@ module.exports = class ThangsTabView extends CocoView
@surface.lankBoss.update true # Make sure Obstacle layer resets cache @surface.lankBoss.update true # Make sure Obstacle layer resets cache
onSurfaceMouseMoved: (e) -> onSurfaceMouseMoved: (e) ->
@dragged = true
return unless @addThangLank return unless @addThangLank
wop = @surface.camera.screenToWorld x: e.x, y: e.y wop = @surface.camera.screenToWorld x: e.x, y: e.y
wop.z = 0.5 wop.z = 0.5
@ -482,8 +496,9 @@ module.exports = class ThangsTabView extends CocoView
deleteSelectedExtantThang: (e) => deleteSelectedExtantThang: (e) =>
return if $(e.target).hasClass 'treema-node' return if $(e.target).hasClass 'treema-node'
return unless @selectedExtantThang selected = @gameUIState.get('selected')
thang = @getThangByID(@selectedExtantThang.id) return unless selected
thang = @getThangByID(selected.thang.id)
@thangsTreema.delete(@pathForThang(thang)) @thangsTreema.delete(@pathForThang(thang))
@deleteEmptyTreema(thang) @deleteEmptyTreema(thang)
Thang.resetThangIDs() # TODO: find some way to do this when we delete from treema, too Thang.resetThangIDs() # TODO: find some way to do this when we delete from treema, too
@ -564,14 +579,19 @@ module.exports = class ThangsTabView extends CocoView
@selectAddThangType @addThangType, @cloneSourceThang if @addThangType # make another addThang sprite, since the World just refreshed @selectAddThangType @addThangType, @cloneSourceThang if @addThangType # make another addThang sprite, since the World just refreshed
# update selection, since the thangs have been remade # update selection, since the thangs have been remade
if @selectedExtantThang selected = @gameUIState.get('selected')
@selectedExtantLank = @surface.lankBoss.lanks[@selectedExtantThang.id] if selected
@selectedExtantThang = @selectedExtantLank?.thang sprite = @surface.lankBoss.lanks[selected.thang.id]
if sprite
thang = sprite.thang
@gameUIState.set('selected', _.extend({}, selected, { sprite, thang }))
else
@gameUIState.set('selected', null)
Backbone.Mediator.publish 'editor:thangs-edited', thangs: @world.thangs Backbone.Mediator.publish 'editor:thangs-edited', thangs: @world.thangs
onTreemaThangSelected: (e, selectedTreemas) => onTreemaThangSelected: (e, selectedTreemas) =>
selectedThangID = _.last(selectedTreemas)?.data.id selectedThangID = _.last(selectedTreemas)?.data.id
if selectedThangID isnt @selectedExtantThang?.id if selectedThangID isnt @gameUIState.get('selected')?.thang.id
@surface.lankBoss.selectThang selectedThangID, null, true @surface.lankBoss.selectThang selectedThangID, null, true
onTreemaThangDoubleClicked: (e, treema) => onTreemaThangDoubleClicked: (e, treema) =>
@ -655,7 +675,8 @@ module.exports = class ThangsTabView extends CocoView
onDuplicateClicked: (e) -> onDuplicateClicked: (e) ->
$('#contextmenu').hide() $('#contextmenu').hide()
@selectAddThangType @selectedExtantThang.spriteName, @selectedExtantThang selected = @gameUIState.get('selected')
@selectAddThangType(selected.thang.spriteName, selected.thang)
onClickRotationButton: (e) -> onClickRotationButton: (e) ->
$('#contextmenu').hide() $('#contextmenu').hide()
@ -667,7 +688,8 @@ module.exports = class ThangsTabView extends CocoView
@hush = true @hush = true
thangData = @getThangByID thang.id thangData = @getThangByID thang.id
thangData = $.extend true, {}, thangData thangData = $.extend true, {}, thangData
unless component = _.find thangData.components, {original: componentOriginal} component = _.find thangData.components, {original: componentOriginal}
unless component
component = original: componentOriginal, config: {}, majorVersion: 0 component = original: componentOriginal, config: {}, majorVersion: 0
thangData.components.push component thangData.components.push component
modificationFunction component modificationFunction component
@ -682,34 +704,39 @@ module.exports = class ThangsTabView extends CocoView
lank.setDebug true lank.setDebug true
rotateSelectedThangTo: (radians) -> rotateSelectedThangTo: (radians) ->
@modifySelectedThangComponentConfig @selectedExtantThang, LevelComponent.PhysicalID, (component) => selectedThang = @gameUIState.get('selected')?.thang
@modifySelectedThangComponentConfig selectedThang, LevelComponent.PhysicalID, (component) =>
component.config.rotation = radians component.config.rotation = radians
@selectedExtantThang.rotation = component.config.rotation selectedThang.rotation = component.config.rotation
rotateSelectedThangBy: (radians) -> rotateSelectedThangBy: (radians) ->
@modifySelectedThangComponentConfig @selectedExtantThang, LevelComponent.PhysicalID, (component) => selectedThang = @gameUIState.get('selected')?.thang
@modifySelectedThangComponentConfig selectedThang, LevelComponent.PhysicalID, (component) =>
component.config.rotation = ((component.config.rotation ? 0) + radians) % (2 * Math.PI) component.config.rotation = ((component.config.rotation ? 0) + radians) % (2 * Math.PI)
@selectedExtantThang.rotation = component.config.rotation selectedThang.rotation = component.config.rotation
moveSelectedThangBy: (xDir, yDir) -> moveSelectedThangBy: (xDir, yDir) ->
@modifySelectedThangComponentConfig @selectedExtantThang, LevelComponent.PhysicalID, (component) => selectedThang = @gameUIState.get('selected')?.thang
@modifySelectedThangComponentConfig selectedThang, LevelComponent.PhysicalID, (component) =>
component.config.pos.x += 0.5 * xDir component.config.pos.x += 0.5 * xDir
component.config.pos.y += 0.5 * yDir component.config.pos.y += 0.5 * yDir
@selectedExtantThang.pos.x = component.config.pos.x selectedThang.pos.x = component.config.pos.x
@selectedExtantThang.pos.y = component.config.pos.y selectedThang.pos.y = component.config.pos.y
resizeSelectedThangBy: (xDir, yDir) -> resizeSelectedThangBy: (xDir, yDir) ->
@modifySelectedThangComponentConfig @selectedExtantThang, LevelComponent.PhysicalID, (component) => selectedThang = @gameUIState.get('selected')?.thang
@modifySelectedThangComponentConfig selectedThang, LevelComponent.PhysicalID, (component) =>
component.config.width = (component.config.width ? 4) + 0.5 * xDir component.config.width = (component.config.width ? 4) + 0.5 * xDir
component.config.height = (component.config.height ? 4) + 0.5 * yDir component.config.height = (component.config.height ? 4) + 0.5 * yDir
@selectedExtantThang.width = component.config.width selectedThang.width = component.config.width
@selectedExtantThang.height = component.config.height selectedThang.height = component.config.height
toggleSelectedThangCollision: -> toggleSelectedThangCollision: ->
@modifySelectedThangComponentConfig @selectedExtantThang, LevelComponent.CollidesID, (component) => selectedThang = @gameUIState.get('selected')?.thang
@modifySelectedThangComponentConfig selectedThang, LevelComponent.CollidesID, (component) =>
component.config ?= {} component.config ?= {}
component.config.collisionCategory = if component.config.collisionCategory is 'none' then 'ground' else 'none' component.config.collisionCategory = if component.config.collisionCategory is 'none' then 'ground' else 'none'
@selectedExtantThang.collisionCategory = component.config.collisionCategory selectedThang.collisionCategory = component.config.collisionCategory
toggleThangsContainer: (e) -> toggleThangsContainer: (e) ->
$('#all-thangs').toggleClass('hide') $('#all-thangs').toggleClass('hide')