mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-18 19:42:36 -05:00
842 lines
33 KiB
CoffeeScript
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
|