codecombat/app/views/editor/level/thangs/ThangsTabView.coffee
2016-07-14 09:38:45 -07:00

842 lines
33 KiB
CoffeeScript

CocoView = require 'views/core/CocoView'
AddThangsView = require './AddThangsView'
thangs_template = require 'templates/editor/level/thangs-tab-view'
Level = require 'models/Level'
ThangType = require 'models/ThangType'
LevelComponent = require 'models/LevelComponent'
CocoCollection = require 'collections/CocoCollection'
{isObjectID} = require 'models/CocoModel'
Surface = require 'lib/surface/Surface'
Thang = require 'lib/world/thang'
LevelThangEditView = require './LevelThangEditView'
ComponentsCollection = require 'collections/ComponentsCollection'
require 'vendor/treema'
GameUIState = require 'models/GameUIState'
# Moving the screen while dragging thangs constants
MOVE_MARGIN = 0.15
MOVE_SPEED = 13
# Let us place these on top of other Thangs
overlappableThangTypeNames = ['Torch', 'Chains', 'Bird', 'Cloud 1', 'Cloud 2', 'Cloud 3', 'Waterfall', 'Obstacle', 'Electrowall', 'Spike Walls']
class ThangTypeSearchCollection extends CocoCollection
url: '/db/thang.type?project=original,name,version,slug,kind,components,prerenderedSpriteSheetData'
model: ThangType
module.exports = class ThangsTabView extends CocoView
id: 'thangs-tab-view'
className: 'tab-pane active'
template: thangs_template
subscriptions:
'surface:mouse-moved': 'onSurfaceMouseMoved'
'surface:mouse-over': 'onSurfaceMouseOver'
'surface:mouse-out': 'onSurfaceMouseOut'
'editor:edit-level-thang': 'editThang'
'editor:level-thang-edited': 'onLevelThangEdited'
'editor:level-thang-done-editing': 'onLevelThangDoneEditing'
'editor:view-switched': 'onViewSwitched'
'sprite:dragged': 'onSpriteDragged'
'sprite:mouse-up': 'onSpriteMouseUp'
'sprite:double-clicked': 'onSpriteDoubleClicked'
'surface:stage-mouse-down': 'onStageMouseDown'
'surface:stage-mouse-up': 'onStageMouseUp'
'editor:random-terrain-generated': 'onRandomTerrainGenerated'
events:
'click #extant-thangs-filter button': 'onFilterExtantThangs'
'click #delete': 'onDeleteClicked'
'click #duplicate': 'onDuplicateClicked'
'click #thangs-container-toggle': 'toggleThangsContainer'
'click #thangs-palette-toggle': 'toggleThangsPalette'
# 'click .add-thang-palette-icon': 'toggleThangsPalette'
'click #rotation-menu-item button': 'onClickRotationButton'
shortcuts:
'esc': 'selectAddThang'
'delete, del, backspace': 'deleteSelectedExtantThang'
'ctrl+z, ⌘+z': 'undo'
'ctrl+shift+z, ⌘+shift+z': 'redo'
'alt+c': 'toggleSelectedThangCollision'
'left': -> @moveSelectedThangBy -1, 0
'right': -> @moveSelectedThangBy 1, 0
'up': -> @moveSelectedThangBy 0, 1
'down': -> @moveSelectedThangBy 0, -1
'alt+left': -> @rotateSelectedThangTo Math.PI unless key.shift
'alt+right': -> @rotateSelectedThangTo 0 unless key.shift
'alt+up': -> @rotateSelectedThangTo -Math.PI / 2
'alt+down': -> @rotateSelectedThangTo Math.PI / 2
'alt+shift+left': -> @rotateSelectedThangBy Math.PI / 16
'alt+shift+right': -> @rotateSelectedThangBy -Math.PI / 16
'shift+left': -> @resizeSelectedThangBy -1, 0
'shift+right': -> @resizeSelectedThangBy 1, 0
'shift+up': -> @resizeSelectedThangBy 0, 1
'shift+down': -> @resizeSelectedThangBy 0, -1
constructor: (options) ->
super options
@world = options.world
@gameUIState = new GameUIState()
@listenTo(@gameUIState, 'sprite:mouse-down', @onSpriteMouseDown)
@listenTo(@gameUIState, 'surface:stage-mouse-move', @onStageMouseMove)
@listenTo(@gameUIState, 'change:selected', @onChangeSelected)
# should load depended-on Components, too
@thangTypes = @supermodel.loadCollection(new ThangTypeSearchCollection(), 'thangs').model
# just loading all Components for now: https://github.com/codecombat/codecombat/issues/405
@componentCollection = @supermodel.loadCollection(new ComponentsCollection(), 'components').load()
@level = options.level
@onThangsChanged = _.debounce(@onThangsChanged)
$(document).bind 'contextmenu', @preventDefaultContextMenu
getRenderData: (context={}) ->
context = super(context)
return context unless @supermodel.finished()
for thangType in @thangTypes.models
thangType.notInLevel = true
thangTypes = (thangType.attributes for thangType in @supermodel.getModels(ThangType))
thangTypes = _.uniq thangTypes, false, 'original'
thangTypes = _.reject thangTypes, (tt) -> tt.kind in ['Mark', undefined]
groupMap = {}
for thangType in thangTypes
groupMap[thangType.kind] ?= []
groupMap[thangType.kind].push thangType
groups = []
for groupName in Object.keys(groupMap).sort()
someThangTypes = groupMap[groupName]
someThangTypes = _.sortBy someThangTypes, 'name'
group =
name: groupName
thangs: someThangTypes
groups.push group
context.thangTypes = thangTypes
context.groups = groups
context
undo: (e) ->
if not @editThangView then @thangsTreema.undo() else @editThangView.undo()
redo: (e) ->
if not @editThangView then @thangsTreema.redo() else @editThangView.redo()
afterRender: ->
super()
return unless @supermodel.finished()
$('.tab-content').mousedown @selectAddThang
$('#thangs-list').bind 'mousewheel', @preventBodyScrollingInThangList
@$el.find('#extant-thangs-filter button:first').button('toggle')
$(window).on 'resize', @onWindowResize
@addThangsView = @insertSubView new AddThangsView world: @world
@buildInterface() # refactor to not have this trigger when this view re-renders?
if _.keys(@thangsTreema.data).length
@$el.find('#canvas-overlay').css('display', 'none')
onFilterExtantThangs: (e) ->
@$el.find('#extant-thangs-filter button.active').button('toggle')
button = $(e.target).closest('button')
button.button('toggle')
val = button.val()
@thangsTreema.$el.removeClass(@lastHideClass) if @lastHideClass
@thangsTreema.$el.addClass(@lastHideClass = "hide-except-#{val}") if val
preventBodyScrollingInThangList: (e) ->
@scrollTop += (if e.deltaY < 0 then 1 else -1) * 30
e.preventDefault()
buildInterface: (e) ->
@level = e.level if e
data = $.extend(true, [], @level.attributes.thangs ? [])
thangsObject = @groupThangs(data)
schema = {
type: 'object'
format: 'thangs-folder'
additionalProperties: {
anyOf: [
{
type: 'object'
format: 'thang'
required: ['thangType', 'id']
}
{ $ref: '#' }
]
}
}
treemaOptions =
schema: schema
data: thangsObject
skipValidation: true
supermodel: @supermodel
callbacks:
change: @onThangsChanged
select: @onTreemaThangSelected
dblclick: @onTreemaThangDoubleClicked
readOnly: true
nodeClasses:
thang: ThangNode
'thangs-folder': ThangsFolderNode
world: @world
@thangsTreema = @$el.find('#thangs-treema').treema treemaOptions
@thangsTreema.build()
@thangsTreema.open()
@openSmallerFolders(@thangsTreema)
@onThangsChanged() # Initialize the World with Thangs
@initSurface()
thangsHeaderHeight = $('#thangs-header').height()
oldHeight = $('#thangs-list').height()
$('#thangs-list').height(oldHeight - thangsHeaderHeight)
if data?.length
@$el.find('.generate-terrain-button').hide()
openSmallerFolders: (folderTreema) ->
children = _.values folderTreema.childrenTreemas
for child in children
continue if child.data.thangType
if _.keys(child.data).length < 5
child.open()
@openSmallerFolders(child)
initSurface: ->
webGLCanvas = $('canvas#webgl-surface', @$el)
normalCanvas = $('canvas#normal-surface', @$el)
@surface = new Surface(@world, normalCanvas, webGLCanvas, {
paths: false
coords: true
grid: true
navigateToSelection: false
thangTypes: @supermodel.getModels(ThangType)
showInvisible: true
frameRate: 15
levelType: @level.get 'type', true
@gameUIState
handleEvents: false
})
@surface.playing = false
@surface.setWorld @world
@surface.lankBoss.suppressSelectionSounds = true
@centerCamera()
centerCamera: ->
[width, height] = @world.size()
width = Math.max width, 80
height = Math.max height, 68
{left, top, right, bottom} = @world.getBounds()
center = x: left + width / 2, y: bottom + height / 2
sup = @surface.camera.worldToSurface center
zoom = 0.94 * 92.4 / width # Zoom 1.0 lets us see 92.4 meters.
@surface.camera.zoomTo(sup, zoom, 0)
destroy: ->
@selectAddThangType null
@surface?.destroy()
$(window).off 'resize', @onWindowResize
$(document).unbind 'contextmenu', @preventDefaultContextMenu
@thangsTreema?.destroy()
super()
onViewSwitched: (e) ->
@selectAddThang null, true
@surface?.lankBoss?.selectLank null, null
onStageMouseDown: (e) ->
# initial values for a mouse click lifecycle
@dragged = 0
@willUnselectSprite = false
@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
else if e.onBackground
@gameUIState.set('selected', [])
onStageMouseMove: (e) ->
@dragged += 1
onStageMouseUp: (e) ->
@paintingWalls = false
$('#contextmenu').hide()
onSpriteMouseDown: (e) ->
nativeEvent = e.originalEvent.nativeEvent
# update selection
selected = []
if nativeEvent.metaKey or nativeEvent.ctrlKey
selected = _.clone(@gameUIState.get('selected'))
if e.thang?.isSelectable
alreadySelected = _.find(selected, (s) -> s.thang is e.thang)
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
lastSelected = _.last(selected)
@selectAddThangType lastSelected.thang.spriteName, lastSelected.thang
selected = []
@gameUIState.set('selected', selected)
if _.any(selected)
@gameUIState.set('canDragCamera', false)
onSpriteDragged: (e) ->
selected = @gameUIState.get('selected')
return unless _.any(selected) and @dragged > 10
@willUnselectSprite = false
{stageX, stageY} = e.originalEvent
# move the one under the mouse
lastSelected = _.last(selected)
cap = @surface.camera.screenToCanvas x: stageX, y: stageY
wop = @surface.camera.canvasToWorld cap
wop.z = lastSelected.thang.depth / 2
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]
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
cap.x -= sidebarWidths[0]
@calculateMovement(cap.x / w, cap.y / h, w / h)
onSpriteMouseUp: (e) ->
selected = @gameUIState.get('selected')
if e.originalEvent.nativeEvent.button == 2 and _.any(selected)
@onSpriteContextMenu e
clearInterval(@movementInterval) if @movementInterval?
@movementInterval = null
return unless _.any(selected)
for singleSelected in selected
pos = singleSelected.thang.pos
thang = _.find(@level.get('thangs') ? [], {id: singleSelected.thang.id})
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) ->
return if @dragged > 10
return unless e.thang
@editThang thangID: e.thang.id
onRandomTerrainGenerated: (e) ->
@thangsBatch = []
@hush = true
nonRandomThangs = (thang for thang in @flattenThangs(@thangsTreema.data) when not /Random/.test thang.id)
@thangsTreema.set '', @groupThangs(nonRandomThangs)
listening = {}
for thang in e.thangs
@selectAddThangType thang.id
# kind of a hack to get the walls to show up correctly when they load.
# might also fix other thangs who need to show up looking a certain way based on thang type components
unless @addThangType.isFullyLoaded() or listening[@addThangType.cid]
listening[@addThangType.cid] = true
@listenToOnce @addThangType, 'build-complete', @onThangsChanged
@addThang @addThangType, thang.pos, true
@hush = false
@onThangsChanged()
@selectAddThangType null
onChangeSelected: (gameUIState, selected) ->
previousSprite = gameUIState.previousAttributes()?.selected?.sprite
sprite = selected?.sprite
thang = selected?.thang
previousSprite?.setNameLabel?(null) unless previousSprite is sprite
if thang and not (@addThangLank and @addThangType.get('name') in overlappableThangTypeNames)
# We clicked on a Thang (or its Treema), so select the Thang
@selectAddThang(null, true)
@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
sprite.setNameLabel(sprite.thangType.get('name') + ': ' + thang.id)
sprite.updateLabels()
sprite.updateMarks()
justAdded: -> @lastAddTime and (new Date() - @lastAddTime) < 150
selectAddThang: (e, forceDeselect=false) =>
return if e? and $(e.target).closest('#thang-search').length # Ignore if you're trying to search thangs
return unless (e? and $(e.target).closest('#thangs-tab-view').length) or key.isPressed('esc') or forceDeselect
if e then target = $(e.target) else target = @$el.find('.add-thangs-palette') # pretend to click on background if no event
return true if target.attr('id') is 'webgl-surface'
target = target.closest('.add-thang-palette-icon')
wasSelected = target.hasClass 'selected'
@$el.find('.add-thangs-palette .add-thang-palette-icon.selected').removeClass('selected')
@selectAddThangType(if wasSelected then null else target.attr 'data-thang-type') unless key.alt or key.meta
@addThangLank?.playSound? 'selected'
target.addClass('selected') if @addThangType
moveAddThangSelection: (direction) ->
return unless @addThangType
icons = $('.add-thangs-palette .add-thang-palette-icon')
selectedIcon = icons.filter('.selected')
selectedIndex = icons.index selectedIcon
nextSelectedIndex = (selectedIndex + direction + icons.length) % icons.length
@selectAddThang {target: icons[nextSelectedIndex]}
selectAddThangType: (type, @cloneSourceThang) ->
if _.isString type
type = _.find @supermodel.getModels(ThangType), (m) -> m.get('name') is type
pos = @addThangLank?.thang.pos # Maintain old sprite's pos if we have it
@surface.lankBoss.removeLank @addThangLank if @addThangLank
@addThangType = type
if @addThangType
@surface.lankBoss.reallyStopMoving = true
thang = @createAddThang()
@addThangLank = @surface.lankBoss.addThangToLanks thang, @surface.lankBoss.layerAdapters['Floating']
@addThangLank.notOfThisWorld = true
@addThangLank.sprite.alpha = 0.75
pos ?= x: Math.round(@world.width / 2), y: Math.round(@world.height / 2)
@adjustThangPos @addThangLank, thang, pos
else
@addThangLank = null
@surface?.lankBoss.reallyStopMoving = false
createEssentialComponents: (defaultComponents) ->
physicalConfig = {pos: {x: 10, y: 10, z: 1}}
if physicalOriginal = _.find(defaultComponents ? [], original: LevelComponent.PhysicalID)
physicalConfig.pos.z = physicalOriginal.config?.pos?.z ? 1 # Get the z right
[
{original: LevelComponent.ExistsID, majorVersion: 0, config: {}}
{original: LevelComponent.PhysicalID, majorVersion: 0, config: physicalConfig}
]
createAddThang: ->
allComponents = (lc.attributes for lc in @supermodel.getModels LevelComponent)
rawComponents = @addThangType.get('components') ? []
rawComponents = @createEssentialComponents() unless rawComponents.length
mockThang = {components: rawComponents}
@level.sortThangComponents [mockThang], allComponents
components = []
for raw in mockThang.components
comp = _.find allComponents, {original: raw.original}
continue if comp.name in ['Selectable', 'Attackable'] # Don't draw health bars or intercept clicks
componentClass = @world.loadClassFromCode comp.js, comp.name, 'component'
components.push [componentClass, raw.config]
thang = new Thang @world, @addThangType.get('name'), 'Add Thang Phantom'
thang.addComponents components...
thang
adjustThangPos: (sprite, thang, pos) ->
if key.shift
# Meter resolution when holding shift, not caring about thang size.
pos.x = Math.round pos.x
pos.y = Math.round pos.y
else
snap = sprite?.data?.snap or sprite?.thangType?.get('snap') or x: 0.01, y: 0.01 # Centimeter resolution by default
pos.x = Math.round((pos.x - (thang.width ? 1) / 2) / snap.x) * snap.x + (thang.width ? 1) / 2
pos.y = Math.round((pos.y - (thang.height ? 1) / 2) / snap.y) * snap.y + (thang.height ? 1) / 2
pos.z = thang.depth / 2
thang.pos = pos
thang.stateChanged = true
@surface.lankBoss.update true # Make sure Obstacle layer resets cache
onSurfaceMouseMoved: (e) ->
return unless @addThangLank
wop = @surface.camera.screenToWorld x: e.x, y: e.y
wop.z = 0.5
@adjustThangPos @addThangLank, @addThangLank.thang, wop
if @paintingWalls
unless _.find @surface.lankBoss.lankArray, ((lank) =>
lank.thangType.get('kind') is 'Wall' and
Math.abs(lank.thang.pos.x - @addThangLank.thang.pos.x) < 2 and
Math.abs(lank.thang.pos.y - @addThangLank.thang.pos.y) < 2 and
lank isnt @addThangLank
)
@addThang @addThangType, @addThangLank.thang.pos
@lastAddTime = new Date()
@paintedWalls = true
null
onSurfaceMouseOver: (e) ->
return unless @addThangLank
@addThangLank.sprite.visible = true
onSurfaceMouseOut: (e) ->
return unless @addThangLank
@addThangLank.sprite.visible = false
calculateMovement: (pctX, pctY, widthHeightRatio) ->
MOVE_TOP_MARGIN = 1.0 - MOVE_MARGIN
if MOVE_TOP_MARGIN > pctX > MOVE_MARGIN and MOVE_TOP_MARGIN > pctY > MOVE_MARGIN
clearInterval(@movementInterval) if @movementInterval?
@movementInterval = null
return @moveLatitude = @moveLongitude = @speed = 0
# calculating speed to be 0.0 to 1.0 within the movement buffer on the outer edge
diff = (MOVE_MARGIN * 2) # comments are assuming MOVE_MARGIN is 0.1
@speed = Math.max(Math.abs(pctX-0.5), Math.abs(pctY-0.5)) * 2 # pct is now 0.8 - 1.0
@speed -= 1.0 - diff # 0.0 - 0.2
@speed *= (1.0 / diff) # 0.0 - 1.0
@speed *= MOVE_SPEED
@moveLatitude = pctX * 2 - 1
@moveLongitude = pctY * 2 - 1
@moveLongitude /= widthHeightRatio if widthHeightRatio > 1.0
@moveLatitude *= widthHeightRatio if widthHeightRatio < 1.0
@movementInterval = setInterval(@moveSide, 16) unless @movementInterval?
moveSide: =>
return unless @speed
c = @surface.camera
p = {x: c.target.x + @moveLatitude * @speed / c.zoom, y: c.target.y + @moveLongitude * @speed / c.zoom}
c.zoomTo(p, c.zoom, 0)
deleteSelectedExtantThang: (e) =>
return if $(e.target).hasClass 'treema-node'
selected = @gameUIState.get('selected')
return unless _.any(selected)
for singleSelected in selected
thang = @getThangByID(singleSelected.thang.id)
@thangsTreema.delete(@pathForThang(thang))
@deleteEmptyTreema(thang)
Thang.resetThangIDs() # TODO: find some way to do this when we delete from treema, too
deleteEmptyTreema: (thang)->
thangType = @supermodel.getModelByOriginal ThangType, thang.thangType
children = @thangsTreema.childrenTreemas
thangKind = children[thangType.get('kind', true)].data
thangName = thangKind[thangType.get('name', true)]
if Object.keys(thangName).length == 0
folderPath = [thangType.get('kind', true), thangType.get('name', true)].join('/')
@thangsTreema.delete(folderPath)
if Object.keys(thangKind).length == 0
folderPath = [thangType.get('kind', true)].join('/')
@thangsTreema.delete(folderPath)
groupThangs: (thangs) ->
# array of thangs -> foldered thangs
grouped = {}
for thang, index in thangs
path = @folderForThang(thang)
obj = grouped
for key in path
obj[key] ?= {}
obj = obj[key]
obj[thang.id] = thang
thang.index = index
grouped
folderForThang: (thang) ->
thangType = @supermodel.getModelByOriginal ThangType, thang.thangType
console.error 'uhh, we had kind', thangType.get('kind', true), 'for', thangType unless thangType.get('kind', true)
[thangType.get('kind', true), thangType.get('name', true)]
pathForThang: (thang) ->
folder = @folderForThang(thang)
folder.push thang.id
folder.join('/')
flattenThangs: (thangs) ->
# foldered thangs -> array of thangs
flattened = []
for key, value of thangs
if value.id? and value.thangType
flattened.push value
else
flattened = flattened.concat @flattenThangs(value)
flattened
populateFoldersForThang: (thang) ->
thangFolder = @folderForThang(thang)
prefix = ''
for segment in thangFolder
if prefix then prefix += '/'
prefix += segment
if not @thangsTreema.get(prefix) then @thangsTreema.set(prefix, {})
onThangsChanged: (skipSerialization) =>
return if @hush
# keep the thangs in the same order as before, roughly
thangs = @flattenThangs(@thangsTreema.data)
thangs = $.extend true, [], thangs
thangs = _.sortBy thangs, 'index'
delete thang.index for thang in thangs
@level.set 'thangs', thangs
return if @editThangView
return if skipSerialization
serializedLevel = @level.serialize {@supermodel, session: null, otherSession: null, headless: false, sessionless: true, cached: true}
try
@world.loadFromLevel serializedLevel, false
catch error
console.error 'Catastrophic error loading the level:', error
thang.isSelectable = not thang.isLand for thang in @world.thangs # let us select walls and such
@surface?.setWorld @world
@surface?.lankBoss.cachedObstacles = false
@selectAddThangType @addThangType, @cloneSourceThang if @addThangType # make another addThang sprite, since the World just refreshed
# update selection, since the thangs have been remade
selected = @gameUIState.get('selected')
if _.any(selected)
for singleSelected in selected
sprite = @surface.lankBoss.lanks[singleSelected.thang.id]
if sprite
sprite.updateMarks()
singleSelected.sprite = sprite
singleSelected.thang = sprite.thang
Backbone.Mediator.publish 'editor:thangs-edited', thangs: @world.thangs
onTreemaThangSelected: (e, selectedTreemas) =>
selectedThangTreemas = _.filter(selectedTreemas, (t) -> t instanceof ThangNode)
thangIDs = (node.data.id for node in selectedThangTreemas)
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) =>
nativeEvent = e.originalEvent.nativeEvent
return if nativeEvent and (nativeEvent.ctrlKey or nativeEvent.metaKey)
id = treema?.data?.id
@editThang thangID: id if id
getThangByID: (id) -> _.find(@level.get('thangs') ? [], {id: id})
addThang: (thangType, pos, batchInsert=false) ->
@$el.find('.generate-terrain-button').hide()
if batchInsert
if thangType.get('name') is 'Hero Placeholder'
thangID = 'Hero Placeholder'
return if not @level.isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev', 'web-dev') or @getThangByID(thangID)
else
thangID = "Random #{thangType.get('name')} #{@thangsBatch.length}"
else
thangID = Thang.nextID(thangType.get('name'), @world) until thangID and not @getThangByID(thangID)
if @cloneSourceThang
components = _.cloneDeep @getThangByID(@cloneSourceThang.id).components
else if @level.isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev', 'web-dev')
components = [] # Load them all from default ThangType Components
else
components = _.cloneDeep thangType.get('components') ? []
components = @createEssentialComponents(thangType.get('components')) unless components.length
physical = _.find components, (c) -> c.config?.pos?
physical.config.pos = x: pos.x, y: pos.y, z: physical.config.pos.z if physical
thang = thangType: thangType.get('original'), id: thangID, components: components
if batchInsert
@thangsBatch.push thang
@populateFoldersForThang(thang)
@thangsTreema.set(@pathForThang(thang), thang)
editThang: (e) ->
if e.target # click event
thangData = $(e.target).data 'thang-data'
else # Mediator event
thangData = @getThangByID(e.thangID)
return unless thangData
@editThangView = new LevelThangEditView thangData: thangData, level: @level, world: @world, supermodel: @supermodel, oldPath: @pathForThang(thangData) # supermodel needed for checkForMissingSystems
@insertSubView @editThangView
@$el.find('>').hide()
@editThangView.$el.show()
Backbone.Mediator.publish 'editor:view-switched', {}
onLevelThangDoneEditing: (e) ->
@removeSubView @editThangView
@editThangView = null
@updateEditedThang e.thangData, e.oldPath
@$el.find('>').show()
onLevelThangEdited: (e) ->
@updateEditedThang e.thangData, e.oldPath
updateEditedThang: (newThang, oldPath) ->
@hush = true
@thangsTreema.delete oldPath
@populateFoldersForThang(newThang)
@thangsTreema.set(@pathForThang(newThang), newThang)
@hush = false
@onThangsChanged()
preventDefaultContextMenu: (e) ->
return unless $(e.target).closest('#canvas-wrapper').length
e.preventDefault()
onSpriteContextMenu: (e) ->
{clientX, clientY} = e.originalEvent.nativeEvent
if @addThangType
$('#duplicate a').html $.i18n.t 'editor.stop_duplicate'
else
$('#duplicate a').html $.i18n.t 'editor.duplicate'
$('#contextmenu').css { position: 'fixed', left: clientX, top: clientY }
$('#contextmenu').show()
#- Context menu callbacks
onDeleteClicked: (e) ->
$('#contextmenu').hide()
@deleteSelectedExtantThang e
onDuplicateClicked: (e) ->
$('#contextmenu').hide()
selected = _.last(@gameUIState.get('selected'))
@selectAddThangType(selected.thang.spriteName, selected.thang)
onClickRotationButton: (e) ->
$('#contextmenu').hide()
rotation = parseFloat($(e.target).closest('button').data('rotation'))
@rotateSelectedThangTo rotation * Math.PI
modifySelectedThangComponentConfig: (thang, componentOriginal, modificationFunction) ->
return unless thang
@hush = true
thangData = @getThangByID thang.id
thangData = $.extend true, {}, thangData
component = _.find thangData.components, {original: componentOriginal}
unless component
component = original: componentOriginal, config: {}, majorVersion: 0
thangData.components.push component
modificationFunction component
@thangsTreema.set @pathForThang(thangData), thangData
@hush = false
@onThangsChanged true
thang.stateChanged = true
lank = @surface.lankBoss.lanks[thang.id]
lank.update true
lank.marks.debug?.destroy()
delete lank.marks.debug
lank.setDebug true
rotateSelectedThangTo: (radians) ->
for singleSelected in @gameUIState.get('selected')
selectedThang = singleSelected.thang
@modifySelectedThangComponentConfig selectedThang, LevelComponent.PhysicalID, (component) =>
component.config.rotation = radians
selectedThang.rotation = component.config.rotation
rotateSelectedThangBy: (radians) ->
for singleSelected in @gameUIState.get('selected')
selectedThang = singleSelected.thang
@modifySelectedThangComponentConfig selectedThang, LevelComponent.PhysicalID, (component) =>
component.config.rotation = ((component.config.rotation ? 0) + radians) % (2 * Math.PI)
selectedThang.rotation = component.config.rotation
moveSelectedThangBy: (xDir, yDir) ->
for singleSelected in @gameUIState.get('selected')
selectedThang = singleSelected.thang
@modifySelectedThangComponentConfig selectedThang, LevelComponent.PhysicalID, (component) =>
component.config.pos.x += 0.5 * xDir
component.config.pos.y += 0.5 * yDir
selectedThang.pos.x = component.config.pos.x
selectedThang.pos.y = component.config.pos.y
resizeSelectedThangBy: (xDir, yDir) ->
for singleSelected in @gameUIState.get('selected')
selectedThang = singleSelected.thang
@modifySelectedThangComponentConfig selectedThang, LevelComponent.PhysicalID, (component) =>
component.config.width = (component.config.width ? 4) + 0.5 * xDir
component.config.height = (component.config.height ? 4) + 0.5 * yDir
selectedThang.width = component.config.width
selectedThang.height = component.config.height
toggleSelectedThangCollision: ->
for singleSelected in @gameUIState.get('selected')
selectedThang = singleSelected.thang
@modifySelectedThangComponentConfig selectedThang, LevelComponent.CollidesID, (component) =>
component.config ?= {}
component.config.collisionCategory = if component.config.collisionCategory is 'none' then 'ground' else 'none'
selectedThang.collisionCategory = component.config.collisionCategory
toggleThangsContainer: (e) ->
$('#all-thangs').toggleClass('hide')
toggleThangsPalette: (e) ->
$('#add-thangs-view').toggleClass('hide')
class ThangsFolderNode extends TreemaNode.nodeMap.object
valueClass: 'treema-thangs-folder'
nodeDescription: 'Thang'
@nameToThangTypeMap: null
getTrackedActionDescription: (trackedAction) ->
trackedActionDescription = super(trackedAction)
if trackedActionDescription is 'Edit ' + @nodeDescription
path = trackedAction.path.split '/'
if path[path.length-1] is 'pos'
trackedActionDescription = 'Move Thang'
trackedActionDescription
buildValueForDisplay: (valEl, data) ->
el = $("<span><strong>#{@keyForParent}</strong> <span class='text-muted'>(#{@countThangs(data)})</span></span>")
# Kind of like having the portraits on the individual thang rows, rather than the parent folder row
# but keeping this logic here in case we want to have it the other way.
# if thangType = @nameToThangType(@keyForParent)
# el.prepend($("<img class='img-circle' src='#{thangType.getPortraitURL()}' />"))
valEl.append(el)
countThangs: (data) ->
return 0 if data.thangType and data.id?
num = 0
for key, value of data
if value.thangType and value.id?
num += 1
else
num += @countThangs(value)
num
nameToThangType: (name) ->
if not ThangsFolderNode.nameToThangTypeMap
thangTypes = @settings.supermodel.getModels(ThangType)
map = {}
map[thangType.get('name')] = thangType for thangType in thangTypes
ThangsFolderNode.nameToThangTypeMap = map
ThangsFolderNode.nameToThangTypeMap[name]
class ThangNode extends TreemaObjectNode
valueClass: 'treema-thang'
collection: false
@thangNameMap: {}
@thangKindMap: {}
buildValueForDisplay: (valEl, data) ->
pos = _.find(data.components, (c) -> c.config?.pos?)?.config.pos # TODO: hack
s = data.id
if pos
s += " (#{Math.round(pos.x)}, #{Math.round(pos.y)})"
else
s += ' (non-physical)'
@buildValueForDisplaySimply valEl, s
thangType = @settings.supermodel.getModelByOriginal(ThangType, data.thangType)
if thangType
valEl.prepend($("<img class='img-circle' src='#{thangType.getPortraitURL()}' />"))
onEnterPressed: ->
Backbone.Mediator.publish 'editor:edit-level-thang', thangID: @getData().id