codecombat/app/views/editor/component/ThangComponentsEditView.coffee

404 lines
15 KiB
CoffeeScript
Raw Normal View History

CocoView = require 'views/kinds/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'
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
2014-08-22 14:40:31 -04:00
@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
2014-08-22 14:40:31 -04:00
# 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: ->
2014-08-22 14:40:31 -04:00
# 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)
2014-08-22 14:40:31 -04:00
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)
2014-08-22 14:40:31 -04:00
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)
2014-08-22 14:40:31 -04:00
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 <strong>#{system}</strong> 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