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') 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') 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 = $("#{@keyForParent} (#{@countThangs(data)})") # 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($("")) 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($("")) onEnterPressed: -> Backbone.Mediator.publish 'editor:edit-level-thang', thangID: @getData().id