Implement multi-select, remove click-to-navigate from level editor

This commit is contained in:
Scott Erickson 2016-06-24 11:07:38 -07:00
parent 4dda1b67dd
commit fe1598cab2
6 changed files with 158 additions and 100 deletions

View file

@ -649,6 +649,10 @@ module.exports = Lank = class Lank extends CocoClass
@marks[name] ?= new Mark name: name, lank: @, camera: @options.camera, layer: layer ? @options.groundLayer, thangType: thangType @marks[name] ?= new Mark name: name, lank: @, camera: @options.camera, layer: layer ? @options.groundLayer, thangType: thangType
@marks[name] @marks[name]
removeMark: (name) ->
@marks[name].destroy()
delete @marks[name]
notifySpeechUpdated: (e) -> notifySpeechUpdated: (e) ->
e = _.clone(e) e = _.clone(e)
e.sprite = @ e.sprite = @

View file

@ -282,11 +282,21 @@ module.exports = class LankBoss extends CocoClass
@selectLank e if e.onBackground @selectLank e if e.onBackground
onChangeSelected: (gameUIState, selected) -> onChangeSelected: (gameUIState, selected) ->
if selected oldLanks = (s.sprite for s in gameUIState.previousAttributes().selected or [])
@selectThang(selected.thang.id, selected.spellName) newLanks = (s.sprite for s in selected or [])
else addedLanks = _.difference(newLanks, oldLanks)
@selectedLank?.selected = false removedLanks = _.difference(oldLanks, newLanks)
@selectedLank = null
for lank in addedLanks
layer = if lank.sprite.parent isnt @layerAdapters.Default.container then @layerAdapters.Default else @layerAdapters.Ground
mark = new Mark name: 'selection', camera: @camera, layer: layer, thangType: 'selection'
mark.toggle true
mark.setLank(lank)
mark.update()
lank.marks.selection = mark # TODO: Figure out how to non-hackily assign lank this mark
for lank in removedLanks
lank.removeMark?('selection')
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]

View file

@ -2,4 +2,28 @@ CocoModel = require './CocoModel'
module.exports = class GameUIState extends CocoModel module.exports = class GameUIState extends CocoModel
@className: 'GameUIState' @className: 'GameUIState'
@schema: {
type: 'object'
properties: {
canDragCamera: {
type: 'boolean'
description: 'Serves as a lock to enable or disable camera movement.'
}
selected: {
# TODO: Turn this into a collection which can be listened to? With Thang models.
type: 'object'
description: 'Array of selected thangs'
properties: {
sprite: { description: 'Lank instance' }
thang: { description: 'Thang object generated by the world' }
}
}
}
}
defaults: -> {
selected: []
canDragCamera: true
}

View file

@ -81,13 +81,13 @@ module.exports = class ThangsTabView extends CocoView
@listenTo(@gameUIState, 'sprite:mouse-down', @onSpriteMouseDown) @listenTo(@gameUIState, 'sprite:mouse-down', @onSpriteMouseDown)
@listenTo(@gameUIState, 'surface:stage-mouse-move', @onStageMouseMove) @listenTo(@gameUIState, 'surface:stage-mouse-move', @onStageMouseMove)
@listenTo(@gameUIState, 'change:selected', @onChangeSelected) @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
# just loading all Components for now: https://github.com/codecombat/codecombat/issues/405 # just loading all Components for now: https://github.com/codecombat/codecombat/issues/405
@componentCollection = @supermodel.loadCollection(new ComponentsCollection(), 'components').load() @componentCollection = @supermodel.loadCollection(new ComponentsCollection(), 'components').load()
@level = options.level @level = options.level
@onThangsChanged = _.debounce(@onThangsChanged)
$(document).bind 'contextmenu', @preventDefaultContextMenu $(document).bind 'contextmenu', @preventDefaultContextMenu
@ -247,8 +247,9 @@ module.exports = class ThangsTabView extends CocoView
@surface?.lankBoss?.selectLank null, null @surface?.lankBoss?.selectLank null, null
onStageMouseDown: (e) -> onStageMouseDown: (e) ->
@dragged = false # initial values for a mouse click lifecycle
@willRepositionCamera = true @dragged = 0
@willUnselectSprite = false
@gameUIState.set('canDragCamera', true) @gameUIState.set('canDragCamera', true)
if @addThangLank?.thangType.get('kind') is 'Wall' if @addThangLank?.thangType.get('kind') is 'Wall'
@ -258,48 +259,66 @@ module.exports = class ThangsTabView extends CocoView
else if @addThangLank else if @addThangLank
# We clicked on the background when we had an add Thang selected, so add it # We clicked on the background when we had an add Thang selected, so add it
@addThang @addThangType, @addThangLank.thang.pos @addThang @addThangType, @addThangLank.thang.pos
@willRepositionCamera = false
else if e.onBackground else if e.onBackground
@gameUIState.set('selected', null) @gameUIState.set('selected', [])
onStageMouseMove: (e) -> onStageMouseMove: (e) ->
@willRepositionCamera = false @dragged += 1
onStageMouseUp: (e) -> onStageMouseUp: (e) ->
if @willRepositionCamera
worldPos = @surface.camera.screenToWorld {x: e.originalEvent.rawX, y: e.originalEvent.rawY}
@surface.camera.zoomTo(@surface.camera.worldToSurface(worldPos), @surface.camera.zoom, 1000)
@paintingWalls = false @paintingWalls = false
$('#contextmenu').hide() $('#contextmenu').hide()
onSpriteMouseDown: (e) -> onSpriteMouseDown: (e) ->
nativeEvent = e.originalEvent.nativeEvent
# update selection # update selection
# TODO: Handle key.shift, lankBoss.dragged property selected = []
selected = null if nativeEvent.metaKey or nativeEvent.ctrlKey
selected = _.clone(@gameUIState.get('selected'))
if e.thang?.isSelectable if e.thang?.isSelectable
selected = { thang: e.thang, sprite: e.sprite, spellName: e.spellName } alreadySelected = _.find(selected, (s) -> s.thang is e.thang)
if selected and (key.alt or key.meta) if alreadySelected
# move to end (make it the last selected) and maybe unselect it
@willUnselectSprite = true
selected = _.without(selected, alreadySelected)
selected.push({ thang: e.thang, sprite: e.sprite, spellName: e.spellName })
if _.any(selected) and key.alt
# Clone selected thang instead of selecting it # Clone selected thang instead of selecting it
@willRepositionCamera = false lastSelected = _.last(selected)
@selectAddThangType selected.thang.spriteName, selected.thang @selectAddThangType lastSelected.thang.spriteName, lastSelected.thang
selected = null selected = []
@gameUIState.set('selected', selected) @gameUIState.set('selected', selected)
if selected if _.any(selected)
@willRepositionCamera = false
@gameUIState.set('canDragCamera', false) @gameUIState.set('canDragCamera', false)
onSpriteDragged: (e) -> onSpriteDragged: (e) ->
selected = @gameUIState.get('selected') selected = @gameUIState.get('selected')
return unless selected and e.thang?.id is selected.thang.id return unless _.any(selected) and @dragged > 10
@dragged = true @willUnselectSprite = false
@willRepositionCamera = false
{stageX, stageY} = e.originalEvent {stageX, stageY} = e.originalEvent
# move the one under the mouse
lastSelected = _.last(selected)
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 = selected.thang.depth / 2 wop.z = lastSelected.thang.depth / 2
@adjustThangPos selected.sprite, selected.thang, wop posBefore = _.clone(lastSelected.thang.pos)
@adjustThangPos lastSelected.sprite, lastSelected.thang, wop
posAfter = lastSelected.thang.pos
# move any others selected, proportionally to how the 'main' sprite moved
xDiff = posAfter.x - posBefore.x
yDiff = posAfter.y - posBefore.y
if xDiff or yDiff
for singleSelected in selected.slice(0, selected.length - 1)
newPos = {
x: singleSelected.thang.pos.x + xDiff
y: singleSelected.thang.pos.y + yDiff
}
@adjustThangPos singleSelected.sprite, singleSelected.thang, newPos
# move the camera if we're on the edge of the screen
[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
@ -308,21 +327,28 @@ module.exports = class ThangsTabView extends CocoView
onSpriteMouseUp: (e) -> onSpriteMouseUp: (e) ->
selected = @gameUIState.get('selected') selected = @gameUIState.get('selected')
if e.originalEvent.nativeEvent.button == 2 and selected if e.originalEvent.nativeEvent.button == 2 and _.any(selected)
@onSpriteContextMenu e @onSpriteContextMenu e
clearInterval(@movementInterval) if @movementInterval? clearInterval(@movementInterval) if @movementInterval?
@movementInterval = null @movementInterval = null
return unless selected and e.thang?.id is selected.thang.id return unless _.any(selected)
pos = selected.thang.pos
thang = _.find(@level.get('thangs') ? [], {id: selected.thang.id}) for singleSelected in selected
path = "#{@pathForThang(thang)}/components/original=#{LevelComponent.PhysicalID}" pos = singleSelected.thang.pos
physical = @thangsTreema.get path
return if not physical or (physical.config.pos.x is pos.x and physical.config.pos.y is pos.y) thang = _.find(@level.get('thangs') ? [], {id: singleSelected.thang.id})
@thangsTreema.set path + '/config/pos', x: pos.x, y: pos.y, z: pos.z path = "#{@pathForThang(thang)}/components/original=#{LevelComponent.PhysicalID}"
physical = @thangsTreema.get path
continue 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
if @willUnselectSprite
clickedSprite = _.find(selected, {sprite: e.sprite})
@gameUIState.set('selected', _.without(selected, clickedSprite))
onSpriteDoubleClicked: (e) -> onSpriteDoubleClicked: (e) ->
return if @dragged > 10
return unless e.thang return unless e.thang
@editThang thangID: e.thang.id @editThang thangID: e.thang.id
@ -443,7 +469,6 @@ 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
@ -497,11 +522,13 @@ module.exports = class ThangsTabView extends CocoView
deleteSelectedExtantThang: (e) => deleteSelectedExtantThang: (e) =>
return if $(e.target).hasClass 'treema-node' return if $(e.target).hasClass 'treema-node'
selected = @gameUIState.get('selected') selected = @gameUIState.get('selected')
return unless selected return unless _.any(selected)
thang = @getThangByID(selected.thang.id)
@thangsTreema.delete(@pathForThang(thang)) for singleSelected in selected
@deleteEmptyTreema(thang) thang = @getThangByID(singleSelected.thang.id)
Thang.resetThangIDs() # TODO: find some way to do this when we delete from treema, too @thangsTreema.delete(@pathForThang(thang))
@deleteEmptyTreema(thang)
Thang.resetThangIDs() # TODO: find some way to do this when we delete from treema, too
deleteEmptyTreema: (thang)-> deleteEmptyTreema: (thang)->
thangType = @supermodel.getModelByOriginal ThangType, thang.thangType thangType = @supermodel.getModelByOriginal ThangType, thang.thangType
@ -580,21 +607,25 @@ module.exports = class ThangsTabView extends CocoView
# update selection, since the thangs have been remade # update selection, since the thangs have been remade
selected = @gameUIState.get('selected') selected = @gameUIState.get('selected')
if selected if _.any(selected)
sprite = @surface.lankBoss.lanks[selected.thang.id] for singleSelected in selected
if sprite sprite = @surface.lankBoss.lanks[singleSelected.thang.id]
thang = sprite.thang if sprite
@gameUIState.set('selected', _.extend({}, selected, { sprite, thang })) sprite.updateMarks()
else singleSelected.sprite = sprite
@gameUIState.set('selected', null) singleSelected.thang = sprite.thang
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 selectedThangTreemas = _.filter(selectedTreemas, (t) -> t instanceof ThangNode)
if selectedThangID isnt @gameUIState.get('selected')?.thang.id thangIDs = (node.data.id for node in selectedThangTreemas)
@surface.lankBoss.selectThang selectedThangID, null, true lanks = (@surface.lankBoss.lanks[thangID] for thangID in thangIDs when thangID)
selected = ({ thang: lank.thang, sprite: lank } for lank in lanks when lank)
@gameUIState.set('selected', selected)
onTreemaThangDoubleClicked: (e, treema) => onTreemaThangDoubleClicked: (e, treema) =>
nativeEvent = e.originalEvent.nativeEvent
return if nativeEvent and (nativeEvent.ctrlKey or nativeEvent.metaKey)
id = treema?.data?.id id = treema?.data?.id
@editThang thangID: id if id @editThang thangID: id if id
@ -675,7 +706,7 @@ module.exports = class ThangsTabView extends CocoView
onDuplicateClicked: (e) -> onDuplicateClicked: (e) ->
$('#contextmenu').hide() $('#contextmenu').hide()
selected = @gameUIState.get('selected') selected = _.last(@gameUIState.get('selected'))
@selectAddThangType(selected.thang.spriteName, selected.thang) @selectAddThangType(selected.thang.spriteName, selected.thang)
onClickRotationButton: (e) -> onClickRotationButton: (e) ->
@ -704,39 +735,44 @@ module.exports = class ThangsTabView extends CocoView
lank.setDebug true lank.setDebug true
rotateSelectedThangTo: (radians) -> rotateSelectedThangTo: (radians) ->
selectedThang = @gameUIState.get('selected')?.thang for singleSelected in @gameUIState.get('selected')
@modifySelectedThangComponentConfig selectedThang, LevelComponent.PhysicalID, (component) => selectedThang = singleSelected.thang
component.config.rotation = radians @modifySelectedThangComponentConfig selectedThang, LevelComponent.PhysicalID, (component) =>
selectedThang.rotation = component.config.rotation component.config.rotation = radians
selectedThang.rotation = component.config.rotation
rotateSelectedThangBy: (radians) -> rotateSelectedThangBy: (radians) ->
selectedThang = @gameUIState.get('selected')?.thang for singleSelected in @gameUIState.get('selected')
@modifySelectedThangComponentConfig selectedThang, LevelComponent.PhysicalID, (component) => selectedThang = singleSelected.thang
component.config.rotation = ((component.config.rotation ? 0) + radians) % (2 * Math.PI) @modifySelectedThangComponentConfig selectedThang, LevelComponent.PhysicalID, (component) =>
selectedThang.rotation = component.config.rotation component.config.rotation = ((component.config.rotation ? 0) + radians) % (2 * Math.PI)
selectedThang.rotation = component.config.rotation
moveSelectedThangBy: (xDir, yDir) -> moveSelectedThangBy: (xDir, yDir) ->
selectedThang = @gameUIState.get('selected')?.thang for singleSelected in @gameUIState.get('selected')
@modifySelectedThangComponentConfig selectedThang, LevelComponent.PhysicalID, (component) => selectedThang = singleSelected.thang
component.config.pos.x += 0.5 * xDir @modifySelectedThangComponentConfig selectedThang, LevelComponent.PhysicalID, (component) =>
component.config.pos.y += 0.5 * yDir component.config.pos.x += 0.5 * xDir
selectedThang.pos.x = component.config.pos.x component.config.pos.y += 0.5 * yDir
selectedThang.pos.y = component.config.pos.y selectedThang.pos.x = component.config.pos.x
selectedThang.pos.y = component.config.pos.y
resizeSelectedThangBy: (xDir, yDir) -> resizeSelectedThangBy: (xDir, yDir) ->
selectedThang = @gameUIState.get('selected')?.thang for singleSelected in @gameUIState.get('selected')
@modifySelectedThangComponentConfig selectedThang, LevelComponent.PhysicalID, (component) => selectedThang = singleSelected.thang
component.config.width = (component.config.width ? 4) + 0.5 * xDir @modifySelectedThangComponentConfig selectedThang, LevelComponent.PhysicalID, (component) =>
component.config.height = (component.config.height ? 4) + 0.5 * yDir component.config.width = (component.config.width ? 4) + 0.5 * xDir
selectedThang.width = component.config.width component.config.height = (component.config.height ? 4) + 0.5 * yDir
selectedThang.height = component.config.height selectedThang.width = component.config.width
selectedThang.height = component.config.height
toggleSelectedThangCollision: -> toggleSelectedThangCollision: ->
selectedThang = @gameUIState.get('selected')?.thang for singleSelected in @gameUIState.get('selected')
@modifySelectedThangComponentConfig selectedThang, LevelComponent.CollidesID, (component) => selectedThang = singleSelected.thang
component.config ?= {} @modifySelectedThangComponentConfig selectedThang, LevelComponent.CollidesID, (component) =>
component.config.collisionCategory = if component.config.collisionCategory is 'none' then 'ground' else 'none' component.config ?= {}
selectedThang.collisionCategory = component.config.collisionCategory component.config.collisionCategory = if component.config.collisionCategory is 'none' then 'ground' else 'none'
selectedThang.collisionCategory = component.config.collisionCategory
toggleThangsContainer: (e) -> toggleThangsContainer: (e) ->
$('#all-thangs').toggleClass('hide') $('#all-thangs').toggleClass('hide')

View file

@ -96,24 +96,6 @@ describe 'Camera (Surface point of view)', ->
checkConversionsFromWorldPos wop, cam checkConversionsFromWorldPos wop, cam
checkCameraPos cam, wop checkCameraPos cam, wop
it 'works at 90 degrees', ->
cam = new Camera {attr: (attr) -> 100}, Math.PI / 2
expect(cam.x2y).toBeCloseTo 1
expect(cam.x2z).toBeGreaterThan 9001
expect(cam.z2y).toBeCloseTo 0
it 'works at 0 degrees', ->
cam = new Camera {attr: (attr) -> 100}, 0
expect(cam.x2y).toBeGreaterThan 9001
expect(cam.x2z).toBeCloseTo 1
expect(cam.z2y).toBeGreaterThan 9001
it 'works at 45 degrees', ->
cam = new Camera {attr: (attr) -> 100}, Math.PI / 4
expect(cam.x2y).toBeCloseTo Math.sqrt(2)
expect(cam.x2z).toBeCloseTo Math.sqrt(2)
expect(cam.z2y).toBeCloseTo 1
it 'works at default angle of asin(0.75) ~= 48.9 degrees', -> it 'works at default angle of asin(0.75) ~= 48.9 degrees', ->
cam = new Camera {attr: (attr) -> 100}, null cam = new Camera {attr: (attr) -> 100}, null
angle = Math.asin(3 / 4) angle = Math.asin(3 / 4)

View file

@ -2,6 +2,7 @@ LankBoss = require 'lib/surface/LankBoss'
Camera = require 'lib/surface/Camera' Camera = require 'lib/surface/Camera'
World = require 'lib/world/world' World = require 'lib/world/world'
ThangType = require 'models/ThangType' ThangType = require 'models/ThangType'
GameUIState = require 'models/GameUIState'
treeData = require 'test/app/fixtures/tree1.thang.type' treeData = require 'test/app/fixtures/tree1.thang.type'
munchkinData = require 'test/app/fixtures/ogre-munchkin-m.thang.type' munchkinData = require 'test/app/fixtures/ogre-munchkin-m.thang.type'
@ -53,6 +54,7 @@ describe 'LankBoss', ->
surfaceTextLayer: new createjs.Container() surfaceTextLayer: new createjs.Container()
world: world world: world
thangTypes: thangTypes thangTypes: thangTypes
gameUIState: new GameUIState()
} }
window.lankBoss = lankBoss = new LankBoss(options) window.lankBoss = lankBoss = new LankBoss(options)