CocoView = require 'views/core/CocoView' template = require 'templates/editor/component/thang-components-edit-view' Level = require 'models/Level' LevelComponent = require 'models/LevelComponent' LevelSystem = require 'models/LevelSystem' ComponentsCollection = require 'collections/ComponentsCollection' ThangComponentConfigView = require './ThangComponentConfigView' AddThangComponentsModal = require './AddThangComponentsModal' nodes = require '../level/treema_nodes' require 'vendor/treema' ThangType = require 'models/ThangType' CocoCollection = require 'collections/CocoCollection' LC = (componentName, config) -> original: LevelComponent[componentName + 'ID'], majorVersion: 0, config: config DEFAULT_COMPONENTS = Unit: [LC('Equips')] Hero: [LC('Equips')] Floor: [ LC('Exists', stateless: true) LC('Physical', width: 20, height: 17, depth: 2, shape: 'sheet', pos: {x: 10, y: 8.5, z: 1}) LC('Land') ] Wall: [ LC('Exists', stateless: true) LC('Physical', width: 4, height: 4, depth: 12, shape: 'box', pos: {x: 2, y: 2, z: 6}) LC('Collides', collisionType: 'static', collisionCategory: 'obstacles', mass: 1000, fixedRotation: true, restitution: 1) ] Doodad: [ LC('Exists', stateless: true) LC('Physical') LC('Collides', collisionType: 'static', fixedRotation: true) ] Misc: [LC('Exists'), LC('Physical')] Mark: [] Item: [LC('Item')] class ItemThangTypeSearchCollection extends CocoCollection url: '/db/thang.type?view=items&project=original,name,version,slug,kind,components' model: ThangType module.exports = class ThangComponentsEditView extends CocoView id: 'thang-components-edit-view' template: template subscriptions: 'editor:thang-type-kind-changed': 'onThangTypeKindChanged' events: 'click #add-components-button': 'onAddComponentsButtonClicked' constructor: (options) -> super options @originalsLoaded = {} @components = options.components or [] @components = $.extend true, [], @components # just to be sure @setThangType options.thangType @lastComponentLength = @components.length @world = options.world @level = options.level @loadComponents(@components) setThangType: (@thangType) -> return unless componentRefs = @thangType?.get('components') @loadComponents(componentRefs) loadComponents: (components) -> for componentRef in components # just to handle if ever somehow the same component is loaded twice, through bad data and alike continue if @originalsLoaded[componentRef.original] @originalsLoaded[componentRef.original] = componentRef.original levelComponent = new LevelComponent(componentRef) url = "/db/level.component/#{componentRef.original}/version/#{componentRef.majorVersion}" levelComponent.setURL(url) resource = @supermodel.loadModel levelComponent, 'component' continue unless resource.isLoading @listenToOnce resource, 'loaded', -> return if @handlingChange @handlingChange = true @onComponentsAdded() @handlingChange = false afterRender: -> super() return unless @supermodel.finished() @buildComponentsTreema() @addThangComponentConfigViews() buildComponentsTreema: -> components = _.zipObject((c.original for c in @components), @components) defaultValue = undefined if thangTypeComponents = @thangType?.get('components', true) defaultValue = _.zipObject((c.original for c in thangTypeComponents), thangTypeComponents) treemaOptions = supermodel: @supermodel schema: { type: 'object' default: defaultValue additionalProperties: Level.schema.properties.thangs.items.properties.components.items }, data: $.extend true, {}, components callbacks: {select: @onSelectComponent, change: @onComponentsTreemaChanged} nodeClasses: 'object': ThangComponentsObjectNode @componentsTreema = @$el.find('#thang-components-column .treema').treema treemaOptions @componentsTreema.build() onComponentsTreemaChanged: => return if @handlingChange @handlingChange = true componentMap = {} for component in @components componentMap[component.original] = component newComponentsList = [] for component in _.values(@componentsTreema.data) newComponentsList.push(componentMap[component.original] or component) @components = newComponentsList # update the components list here @onComponentsChanged() @handlingChange = false onComponentsChanged: => # happens whenever the list of components changed, one way or another # * if the treema gets changed # * if components are added externally, like by a modal # * if a dependency loads and is added to the list if @components.length < @lastComponentLength @onComponentsRemoved() @onComponentsAdded() onComponentsRemoved: -> componentMap = {} for component in @components componentMap[component.original] = component thangComponentMap = {} if thangTypeComponents = @thangType?.get('components') for thangTypeComponent in thangTypeComponents thangComponentMap[thangTypeComponent.original] = thangTypeComponent # Deleting components missing dependencies. while true removedSomething = false for componentRef in _.values(componentMap) componentModel = @supermodel.getModelByOriginalAndMajorVersion( LevelComponent, componentRef.original, componentRef.majorVersion) for dependency in componentModel.get('dependencies') or [] unless (componentMap[dependency.original] or thangComponentMap[dependency.original]) delete componentMap[componentRef.original] component = @supermodel.getModelByOriginal( LevelComponent, componentRef.original) noty { text: "Removed dependent component: #{component.get('name')}" layout: 'topCenter' timeout: 5000 type: 'information' } removedSomething = true break if removedSomething break unless removedSomething @components = _.values(componentMap) # Delete individual component config views that are no longer included. for subview in _.values(@subviews) continue unless subview instanceof ThangComponentConfigView unless (componentMap[subview.component.get('original')] or thangComponentMap[subview.component.get('original')]) @removeSubView(subview) @updateComponentsList() @reportChanges() updateComponentsList: -> # Before I was setting the data to the existing treema but then we had some # nasty sorting/callback bugs. This is less efficient, but it's also less bug prone. @buildComponentsTreema() onComponentsAdded: -> return unless @componentsTreema componentMap = {} for component in @components componentMap[component.original] = component if thangTypeComponents = @thangType?.get('components') for thangTypeComponent in thangTypeComponents componentMap[thangTypeComponent.original] = thangTypeComponent # Go through the map, adding missing dependencies. while true addedSomething = false for componentRef in _.values(componentMap) componentModel = @supermodel.getModelByOriginalAndMajorVersion( LevelComponent, componentRef.original, componentRef.majorVersion) if not componentModel?.loaded @loadComponents([componentRef]) continue for dependency in componentModel?.get('dependencies') or [] if not componentMap[dependency.original] component = @supermodel.getModelByOriginalAndMajorVersion( LevelComponent, dependency.original, dependency.majorVersion) if not component?.loaded @loadComponents([dependency]) # will run onComponentsAdded once more when the model loads else addedSomething = true noty { text: "Added dependency: #{component.get('name')}" layout: 'topCenter' timeout: 5000 type: 'information' } componentMap[dependency.original] = dependency @components.push dependency break unless addedSomething # Sort the component list, reorder the component config views @updateComponentsList() @addThangComponentConfigViews() @checkForMissingSystems() @reportChanges() addThangComponentConfigViews: -> # Detach all component config views temporarily. componentConfigViews = {} for subview in _.values(@subviews) continue unless subview instanceof ThangComponentConfigView componentConfigViews[subview.component.get('original')] = subview subview.$el.detach() # Put back config views into the DOM based on the component list ordering, # adding and registering new ones as needed. configsEl = @$el.find('#thang-component-configs') componentRefs = _.merge {}, @componentsTreema.data if thangTypeComponents = @thangType?.get('components') thangComponentRefs = _.zipObject((c.original for c in thangTypeComponents), thangTypeComponents) for thangTypeComponent in thangTypeComponents if componentRef = componentRefs[thangTypeComponent.original] componentRef.additionalDefaults = thangTypeComponent.config else modifiedRef = _.merge {}, thangTypeComponent modifiedRef.additionalDefaults = modifiedRef.config delete modifiedRef.config componentRefs[thangTypeComponent.original] = modifiedRef for componentRef in _.values(componentRefs) subview = componentConfigViews[componentRef.original] if not subview subview = @makeThangComponentConfigView(componentRef) continue unless subview @registerSubView(subview) subview.setIsDefaultComponent(not @componentsTreema.data[componentRef.original]) configsEl.append(subview.$el) makeThangComponentConfigView: (thangComponent) -> component = @supermodel.getModelByOriginal(LevelComponent, thangComponent.original) return unless component?.loaded config = thangComponent.config ? {} configView = new ThangComponentConfigView({ supermodel: @supermodel level: @level world: @world config: config component: component additionalDefaults: thangComponent.additionalDefaults }) configView.render() @listenTo configView, 'changed', @onConfigChanged configView onConfigChanged: (e) -> foundComponent = false for thangComponent in @components if thangComponent.original is e.component.get('original') thangComponent.config = e.config foundComponent = true break if not foundComponent @components.push({ original: e.component.get('original') majorVersion: e.component.get('version').major config: e.config }) for subview in _.values(@subviews) continue unless subview instanceof ThangComponentConfigView if subview.component.get('original') is e.component.get('original') _.defer -> subview.setIsDefaultComponent(false) break @updateComponentsList() @reportChanges() onSelectComponent: (e, nodes) => @componentsTreema.$el.find('.dependent').removeClass('dependent') @$el.find('.selected-component').removeClass('selected-component') return unless nodes.length is 1 # find dependent components dependents = {} dependents[nodes[0].getData().original] = true componentsToCheck = [nodes[0].getData().original] while componentsToCheck.length componentOriginal = componentsToCheck.pop() for otherComponentRef in @components continue if otherComponentRef.original is componentOriginal continue if dependents[otherComponentRef.original] otherComponent = @supermodel.getModelByOriginal(LevelComponent, otherComponentRef.original) for dependency in otherComponent.get('dependencies', true) if dependents[dependency.original] dependents[otherComponentRef.original] = true componentsToCheck.push otherComponentRef.original # highlight them for child in _.values(@componentsTreema.childrenTreemas) if dependents[child.getData().original] child.$el.addClass('dependent') # scroll to the config for subview in _.values(@subviews) continue unless subview instanceof ThangComponentConfigView if subview.component.get('original') is nodes[0].getData().original subview.$el[0].scrollIntoView() subview.$el.addClass('selected-component') break onChangeExtantComponents: => @buildAddComponentTreema() @reportChanges() checkForMissingSystems: -> return unless @level extantSystems = (@supermodel.getModelByOriginalAndMajorVersion LevelSystem, sn.original, sn.majorVersion).attributes.name.toLowerCase() for idx, sn of @level.get('systems') componentModels = (@supermodel.getModelByOriginal(LevelComponent, c.original) for c in @components) componentSystems = (c.get('system') for c in componentModels when c) for system in componentSystems if system not in extantSystems s = "Component requires system #{system} which is currently not included in this level." noty({ text: s, layout: 'bottomLeft', type: 'warning' }) reportChanges: -> @lastComponentLength = @components.length @trigger 'components-changed', $.extend(true, [], @components) undo: -> @componentsTreema.undo() redo: -> @componentsTreema.redo() onAddComponentsButtonClicked: -> modal = new AddThangComponentsModal({skipOriginals: (c.original for c in @components)}) @openModalView modal @listenToOnce modal, 'hidden', -> componentsToAdd = modal.getSelectedComponents() sparseComponents = ({original: c.get('original'), majorVersion: c.get('version').major} for c in componentsToAdd) @loadComponents(sparseComponents) @components = @components.concat(sparseComponents) @onComponentsChanged() onThangTypeKindChanged: (e) -> return unless defaultComponents = DEFAULT_COMPONENTS[e.kind] for component in defaultComponents when not _.find(@components, original: component.original) @components.push component @onComponentsAdded() destroy: -> @componentsTreema?.destroy() super() class ThangComponentsObjectNode extends TreemaObjectNode addNewChild: -> @addNewChildForKey('') # HACK to get the object adding to act more like adding to an array getChildren: -> children = super(arguments...) children.sort(@sortFunction) sortFunction: (a, b) => a = a.value ? a.defaultData b = b.value ? b.defaultData a = @settings.supermodel.getModelByOriginalAndMajorVersion(LevelComponent, a.original, a.majorVersion) b = @settings.supermodel.getModelByOriginalAndMajorVersion(LevelComponent, b.original, b.majorVersion) return 0 if not (a or b) return 1 if not b return -1 if not a return 1 if a.get('system') > b.get('system') return -1 if a.get('system') < b.get('system') return 1 if a.get('name') > b.get('name') return -1 if a.get('name') < b.get('name') return 0