mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-12 00:31:21 -05:00
4dda1b67dd
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.
806 lines
31 KiB
CoffeeScript
806 lines
31 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)
|
|
@willRepositionCamera =true
|
|
|
|
# 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
|
|
|
|
$(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) ->
|
|
@dragged = false
|
|
@willRepositionCamera = 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) ->
|
|
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
|
|
$('#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) ->
|
|
selected = @gameUIState.get('selected')
|
|
return unless selected and e.thang?.id is selected.thang.id
|
|
@dragged = true
|
|
@willRepositionCamera = false
|
|
{stageX, stageY} = e.originalEvent
|
|
cap = @surface.camera.screenToCanvas x: stageX, y: stageY
|
|
wop = @surface.camera.canvasToWorld cap
|
|
wop.z = selected.thang.depth / 2
|
|
@adjustThangPos selected.sprite, selected.thang, wop
|
|
[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 selected
|
|
@onSpriteContextMenu e
|
|
clearInterval(@movementInterval) if @movementInterval?
|
|
@movementInterval = null
|
|
|
|
return unless selected and e.thang?.id is selected.thang.id
|
|
pos = selected.thang.pos
|
|
|
|
thang = _.find(@level.get('thangs') ? [], {id: selected.thang.id})
|
|
path = "#{@pathForThang(thang)}/components/original=#{LevelComponent.PhysicalID}"
|
|
physical = @thangsTreema.get path
|
|
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
|
|
|
|
onSpriteDoubleClicked: (e) ->
|
|
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) ->
|
|
@dragged = true
|
|
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 selected
|
|
thang = @getThangByID(selected.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, null, null, 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 selected
|
|
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
|
|
|
|
onTreemaThangSelected: (e, selectedTreemas) =>
|
|
selectedThangID = _.last(selectedTreemas)?.data.id
|
|
if selectedThangID isnt @gameUIState.get('selected')?.thang.id
|
|
@surface.lankBoss.selectThang selectedThangID, null, true
|
|
|
|
onTreemaThangDoubleClicked: (e, treema) =>
|
|
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.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-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.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-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 = @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) ->
|
|
selectedThang = @gameUIState.get('selected')?.thang
|
|
@modifySelectedThangComponentConfig selectedThang, LevelComponent.PhysicalID, (component) =>
|
|
component.config.rotation = radians
|
|
selectedThang.rotation = component.config.rotation
|
|
|
|
rotateSelectedThangBy: (radians) ->
|
|
selectedThang = @gameUIState.get('selected')?.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) ->
|
|
selectedThang = @gameUIState.get('selected')?.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) ->
|
|
selectedThang = @gameUIState.get('selected')?.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: ->
|
|
selectedThang = @gameUIState.get('selected')?.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
|