2014-01-03 13:32:13 -05:00
CocoModel = require ' ./CocoModel '
LevelComponent = require ' ./LevelComponent '
LevelSystem = require ' ./LevelSystem '
ThangType = require ' ./ThangType '
module.exports = class Level extends CocoModel
2014-06-30 22:16:26 -04:00
@className: ' Level '
2014-04-22 14:11:08 -04:00
@schema: require ' schemas/models/level '
2014-12-07 17:57:23 -05:00
@levels:
' dungeons-of-kithgard ' : ' 5411cb3769152f1707be029c '
' defense-of-plainswood ' : ' 541b67f71ccc8eaae19f3c62 '
2014-06-30 22:16:26 -04:00
urlRoot: ' /db/level '
2015-02-25 21:41:39 -05:00
editableByArtisans: true
2014-06-30 22:16:26 -04:00
2016-07-13 13:04:43 -04:00
serialize: (options) ->
{ supermodel , session , otherSession , @ headless , @ sessionless , cached = false } = options
2014-10-18 17:51:43 -04:00
o = @ denormalize supermodel , session , otherSession # hot spot to optimize
2014-01-03 13:32:13 -05:00
# Figure out Components
2014-09-03 12:33:21 -04:00
o.levelComponents = if cached then @ getCachedLevelComponents ( supermodel ) else $ . extend true , [ ] , ( lc . attributes for lc in supermodel . getModels LevelComponent )
2014-08-22 17:52:35 -04:00
@ sortThangComponents o . thangs , o . levelComponents , ' Level Thang '
2014-09-03 12:33:21 -04:00
@ fillInDefaultComponentConfiguration o . thangs , o . levelComponents # hot spot to optimize
2014-01-03 13:32:13 -05:00
# Figure out Systems
2014-09-02 15:33:34 -04:00
systemModels = $ . extend true , [ ] , ( ls . attributes for ls in supermodel . getModels LevelSystem )
2014-01-03 13:32:13 -05:00
o.systems = @ sortSystems o . systems , systemModels
@ fillInDefaultSystemConfiguration o . systems
2014-08-11 19:15:46 -04:00
# Figure out ThangTypes' Components
2014-09-03 12:33:21 -04:00
tmap = { }
2014-09-03 15:30:08 -04:00
tmap [ t . thangType ] = true for t in o . thangs ? [ ]
2016-01-12 12:42:34 -05:00
sessionHeroes = [ session ? . get ( ' heroConfig ' ) ? . thangType , otherSession ? . get ( ' heroConfig ' ) ? . thangType ]
2016-01-07 15:26:40 -05:00
o.thangTypes = [ ]
for tt in supermodel . getModels ThangType
if tmap [ tt . get ( ' original ' ) ] or
( tt . get ( ' kind ' ) isnt ' Hero ' and tt . get ( ' kind ' ) ? and tt . get ( ' components ' ) and not tt . notInLevel ) or
2016-07-14 11:58:43 -04:00
( tt . get ( ' kind ' ) is ' Hero ' and ( @ isType ( ' course ' , ' course-ladder ' , ' game-dev ' ) or tt . get ( ' original ' ) in sessionHeroes ) )
2016-01-07 15:26:40 -05:00
o . thangTypes . push ( original: tt . get ( ' original ' ) , name: tt . get ( ' name ' ) , components: $ . extend ( true , [ ] , tt . get ( ' components ' ) ) )
2014-08-22 17:52:35 -04:00
@ sortThangComponents o . thangTypes , o . levelComponents , ' ThangType '
2014-08-11 19:15:46 -04:00
@ fillInDefaultComponentConfiguration o . thangTypes , o . levelComponents
2014-09-03 22:19:04 -04:00
2016-03-03 20:18:17 -05:00
o.picoCTFProblem = @ picoCTFProblem if @ picoCTFProblem
2014-01-03 13:32:13 -05:00
o
2014-09-03 22:19:04 -04:00
2014-09-03 12:33:21 -04:00
cachedLevelComponents: null
2014-09-03 22:19:04 -04:00
2014-09-03 12:33:21 -04:00
getCachedLevelComponents: (supermodel) ->
@ cachedLevelComponents ? = { }
levelComponents = supermodel . getModels LevelComponent
newLevelComponents = [ ]
for levelComponent in levelComponents
if levelComponent . hasLocalChanges ( )
newLevelComponents . push $ . extend ( true , { } , levelComponent . attributes )
continue
@ cachedLevelComponents [ levelComponent . id ] ? = @ cachedLevelComponents [ levelComponent . id ] = $ . extend ( true , { } , levelComponent . attributes )
newLevelComponents . push ( @ cachedLevelComponents [ levelComponent . id ] )
newLevelComponents
2014-09-03 22:19:04 -04:00
2014-10-18 17:51:43 -04:00
denormalize: (supermodel, session, otherSession) ->
2014-08-06 18:18:22 -04:00
o = $ . extend true , { } , @ attributes
2016-07-14 12:38:45 -04:00
if o . thangs and @ isType ( ' hero ' , ' hero-ladder ' , ' hero-coop ' , ' course ' , ' course-ladder ' , ' game-dev ' , ' web-dev ' )
2016-05-31 18:32:32 -04:00
thangTypesWithComponents = ( tt for tt in supermodel . getModels ( ThangType ) when tt . get ( ' components ' ) ? )
thangTypesByOriginal = _ . indexBy thangTypesWithComponents , (tt) -> tt . get ( ' original ' ) # Optimization
2014-08-14 18:09:10 -04:00
for levelThang in o . thangs
2016-05-31 18:32:32 -04:00
@ denormalizeThang ( levelThang , supermodel , session , otherSession , thangTypesByOriginal )
2014-08-06 18:18:22 -04:00
o
2016-05-31 18:32:32 -04:00
denormalizeThang: (levelThang, supermodel, session, otherSession, thangTypesByOriginal) ->
2014-08-07 21:27:47 -04:00
levelThang . components ? = [ ]
2016-07-14 11:58:43 -04:00
isHero = /Hero Placeholder/ . test ( levelThang . id ) and @ isType ( ' hero ' , ' hero-ladder ' , ' hero-coop ' )
2014-10-18 17:51:43 -04:00
if isHero and otherSession
# If it's a hero and there's another session, find the right session for it.
# If there is no other session (playing against default code, or on single player), clone all placeholders.
2014-10-19 20:38:10 -04:00
# TODO: actually look at the teams on these Thangs to determine which session should go with which placeholder.
2014-10-18 17:51:43 -04:00
if levelThang . id is ' Hero Placeholder 1 ' and session . get ( ' team ' ) is ' humans '
session = otherSession
else if levelThang . id is ' Hero Placeholder ' and session . get ( ' team ' ) is ' ogres '
session = otherSession
2014-09-15 16:15:18 -04:00
2014-08-14 19:34:55 -04:00
# Empty out placeholder Components and store their values if we're the hero placeholder.
2014-09-15 16:15:18 -04:00
if isHero
placeholders = { }
placeholdersUsed = { }
2016-05-31 18:32:32 -04:00
placeholderThangType = thangTypesByOriginal [ levelThang . thangType ]
2014-11-18 09:50:13 -05:00
unless placeholderThangType
console . error " Couldn ' t find placeholder ThangType for the hero! "
isHero = false
else
for defaultPlaceholderComponent in placeholderThangType . get ( ' components ' )
placeholders [ defaultPlaceholderComponent . original ] = defaultPlaceholderComponent
for thangComponent in levelThang . components
placeholders [ thangComponent . original ] = thangComponent
levelThang.components = [ ] # We have stored the placeholder values, so we can inherit everything else.
heroThangType = session ? . get ( ' heroConfig ' ) ? . thangType
levelThang.thangType = heroThangType if heroThangType
2014-08-14 19:34:55 -04:00
2016-05-31 18:32:32 -04:00
thangType = thangTypesByOriginal [ levelThang . thangType ]
2014-08-31 18:08:52 -04:00
2014-08-06 18:18:22 -04:00
configs = { }
for thangComponent in levelThang . components
configs [ thangComponent . original ] = thangComponent
2014-08-11 01:09:13 -04:00
2014-09-21 16:26:56 -04:00
for defaultThangComponent in thangType ? . get ( ' components ' ) or [ ]
2014-08-06 18:18:22 -04:00
if levelThangComponent = configs [ defaultThangComponent . original ]
2014-08-14 18:09:10 -04:00
# Take the ThangType default Components and merge level-specific Component config into it
2014-08-06 18:18:22 -04:00
copy = $ . extend true , { } , defaultThangComponent . config
levelThangComponent.config = _ . merge copy , levelThangComponent . config
2014-08-11 01:09:13 -04:00
2014-08-06 18:18:22 -04:00
else
2014-08-14 18:09:10 -04:00
# Just add the Component as is
levelThangComponent = $ . extend true , { } , defaultThangComponent
levelThang . components . push levelThangComponent
2014-09-15 16:15:18 -04:00
if isHero and placeholderComponent = placeholders [ defaultThangComponent . original ]
placeholdersUsed [ placeholderComponent . original ] = true
2014-08-14 18:09:10 -04:00
placeholderConfig = placeholderComponent . config ? { }
2015-12-17 16:54:32 -05:00
levelThangComponent . config ? = { }
config = levelThangComponent . config
2014-08-14 18:09:10 -04:00
if placeholderConfig . pos # Pull in Physical pos x and y
2015-12-17 16:54:32 -05:00
config . pos ? = { }
config.pos.x = placeholderConfig . pos . x
config.pos.y = placeholderConfig . pos . y
config.rotation = placeholderConfig . rotation
2014-08-14 18:09:10 -04:00
else if placeholderConfig . team # Pull in Allied team
2015-12-17 16:54:32 -05:00
config.team = placeholderConfig . team
2014-11-04 19:09:29 -05:00
else if placeholderConfig . significantProperty # For levels where we cheat on what counts as an enemy
2015-12-17 16:54:32 -05:00
config.significantProperty = placeholderConfig . significantProperty
2014-08-14 18:09:10 -04:00
else if placeholderConfig . programmableMethods
# Take the ThangType default Programmable and merge level-specific Component config into it
copy = $ . extend true , { } , placeholderConfig
2015-12-17 16:54:32 -05:00
programmableProperties = config ? . programmableProperties ? [ ]
2015-03-27 01:29:21 -04:00
copy.programmableProperties = _ . union programmableProperties , copy . programmableProperties ? [ ]
2015-12-23 15:08:07 -05:00
levelThangComponent.config = config = _ . merge copy , config
2014-11-06 21:18:57 -05:00
else if placeholderConfig . extraHUDProperties
2015-12-17 16:54:32 -05:00
config.extraHUDProperties = _ . union ( config . extraHUDProperties ? [ ] , placeholderConfig . extraHUDProperties )
2014-12-29 23:06:27 -05:00
else if placeholderConfig . voiceRange # Pull in voiceRange
2015-12-17 16:54:32 -05:00
config.voiceRange = placeholderConfig . voiceRange
config.cooldown = placeholderConfig . cooldown
2014-08-14 18:09:10 -04:00
2014-09-15 16:15:18 -04:00
if isHero
if equips = _ . find levelThang . components , { original: LevelComponent . EquipsID }
inventory = session ? . get ( ' heroConfig ' ) ? . inventory
equips . config ? = { }
equips.config.inventory = $ . extend true , { } , inventory if inventory
for original , placeholderComponent of placeholders when not placeholdersUsed [ original ]
levelThang . components . push placeholderComponent
2014-08-14 18:09:10 -04:00
2016-05-25 18:24:51 -04:00
# Load the user's chosen hero AFTER getting stats from default char
2016-07-14 11:58:43 -04:00
if /Hero Placeholder/ . test ( levelThang . id ) and @ isType ( ' course ' ) and not @ headless and not @ sessionless
2016-06-20 13:47:15 -04:00
heroThangType = me . get ( ' heroConfig ' ) ? . thangType or ThangType . heroes . captain
2016-05-25 18:24:51 -04:00
levelThang.thangType = heroThangType if heroThangType
2014-01-03 13:32:13 -05:00
sortSystems: (levelSystems, systemModels) ->
[ sorted , originalsSeen ] = [ [ ] , { } ]
visit = (system) ->
return if system . original of originalsSeen
systemModel = _ . find systemModels , { original: system . original }
2014-07-13 12:31:31 -04:00
return console . error ' Couldn \' t find model for original ' , system . original , ' from ' , systemModels unless systemModel
2014-01-03 13:32:13 -05:00
for d in systemModel . dependencies or [ ]
system2 = _ . find levelSystems , { original: d . original }
visit system2
2014-06-30 22:16:26 -04:00
#console.log 'sorted systems adding', systemModel.name
2014-09-02 15:33:34 -04:00
sorted . push { model: systemModel , config: $ . extend true , { } , system . config }
2014-01-03 13:32:13 -05:00
originalsSeen [ system . original ] = true
2014-08-25 23:34:46 -04:00
visit system for system in levelSystems ? [ ]
2014-01-03 13:32:13 -05:00
sorted
2014-08-22 17:52:35 -04:00
sortThangComponents: (thangs, levelComponents, parentType) ->
2014-01-03 13:32:13 -05:00
# Here we have to sort the Components by their dependencies.
# It's a bit tricky though, because we don't have either soft dependencies or priority levels.
# Example: Programmable must come last, since it has to override any Component-provided methods that any other Component might have created. Can't enumerate all soft dependencies.
2014-09-15 17:06:26 -04:00
# Example: Plans needs to come after everything except Programmable, since other Components that add plannable methods need to have done so by the time Plans is attached.
2014-01-03 13:32:13 -05:00
# Example: Collides doesn't depend on Allied, but if both exist, Collides must come after Allied. Soft dependency example. Can't just figure out a proper priority to take care of it.
2014-10-18 17:51:43 -04:00
# Example: Moves doesn't depend on Acts, but if both exist, Moves must come after Acts. Another soft dependency example.
2014-01-03 13:32:13 -05:00
# Decision? Just special case the sort logic in here until we have more examples than these two and decide how best to handle most of the cases then, since we don't really know the whole of the problem yet.
# TODO: anything that depends on Programmable will break right now.
2016-05-31 18:32:32 -04:00
originalsToComponents = _ . indexBy levelComponents , ' original ' # Optimization for speed
alliedComponent = _ . find levelComponents , name: ' Allied '
actsComponent = _ . find levelComponents , name: ' Acts '
2014-08-25 23:34:46 -04:00
for thang in thangs ? [ ]
2016-05-31 18:32:32 -04:00
originalsToThangComponents = _ . indexBy thang . components , ' original '
2014-01-03 13:32:13 -05:00
sorted = [ ]
2015-11-02 15:35:28 -05:00
visit = (c, namesToIgnore) ->
2014-01-03 13:32:13 -05:00
return if c in sorted
2016-05-31 18:32:32 -04:00
lc = originalsToComponents [ c . original ]
2014-08-12 15:50:41 -04:00
console . error thang . id or thang . name , ' couldn \' t find lc for ' , c , ' of ' , levelComponents unless lc
2014-08-11 19:15:46 -04:00
return unless lc
2015-11-02 15:35:28 -05:00
return if namesToIgnore and lc . name in namesToIgnore
2014-09-15 17:06:26 -04:00
if lc . name is ' Plans '
# Plans always comes second-to-last, behind Programmable
2015-11-02 15:35:28 -05:00
visit c2 , [ lc . name , ' Programmable ' ] for c2 in thang . components
2014-09-15 17:06:26 -04:00
else if lc . name is ' Programmable '
2014-01-03 13:32:13 -05:00
# Programmable always comes last
2015-11-02 15:35:28 -05:00
visit c2 , [ lc . name ] for c2 in thang . components
2014-01-03 13:32:13 -05:00
else
for d in lc . dependencies or [ ]
2016-05-31 18:32:32 -04:00
c2 = originalsToThangComponents [ d . original ]
2014-08-22 17:52:35 -04:00
unless c2
2016-05-31 18:32:32 -04:00
dependent = originalsToComponents [ d . original ]
2014-08-22 17:52:35 -04:00
dependent = dependent ? . name or d . original
console . error parentType , thang . id or thang . name , ' does not have dependent Component ' , dependent , ' from ' , lc . name
2014-07-13 12:31:31 -04:00
visit c2 if c2
2016-05-31 18:32:32 -04:00
if lc . name is ' Collides ' and alliedComponent
2016-05-31 18:47:31 -04:00
if allied = originalsToThangComponents [ alliedComponent . original ]
visit allied
2016-05-31 18:32:32 -04:00
if lc . name is ' Moves ' and actsComponent
if acts = originalsToThangComponents [ actsComponent . original ]
visit acts
2014-06-30 22:16:26 -04:00
#console.log thang.id, 'sorted comps adding', lc.name
2014-01-03 13:32:13 -05:00
sorted . push c
for comp in thang . components
visit comp
thang.components = sorted
fillInDefaultComponentConfiguration: (thangs, levelComponents) ->
2014-09-16 20:31:00 -04:00
# This is slow, so I inserted some optimizations to speed it up by caching the eventual defaults of commonly-used Components.
@ defaultComponentConfigurations ? = { }
cached = 0
missed = 0
cachedConfigs = 0
2014-08-25 23:34:46 -04:00
for thang in thangs ? [ ]
2014-01-03 13:32:13 -05:00
for component in thang . components or [ ]
2014-09-16 20:31:00 -04:00
isPhysical = component . original is LevelComponent . PhysicalID
if not isPhysical and defaultConfiguration = _ . find @ defaultComponentConfigurations [ component . original ] , ( (d) -> _ . isEqual component , d . originalComponent )
component.config = defaultConfiguration . defaultedConfig
++ cached
continue
2014-01-03 13:32:13 -05:00
continue unless lc = _ . find levelComponents , { original: component . original }
2014-09-16 20:31:00 -04:00
unless isPhysical
originalComponent = $ . extend true , { } , component
2014-01-03 13:32:13 -05:00
component . config ? = { }
2014-09-16 23:19:52 -04:00
TreemaUtils . populateDefaults ( component . config , lc . configSchema ? { } , tv4 )
2014-08-28 20:57:39 -04:00
@lastType = ' component '
@lastOriginal = component . original
2014-09-16 20:31:00 -04:00
unless isPhysical
@ defaultComponentConfigurations [ component . original ] ? = [ ]
@ defaultComponentConfigurations [ component . original ] . push originalComponent: originalComponent , defaultedConfig: component . config
++ cachedConfigs
++ missed
#console.log 'cached', cached, 'missed', missed
2014-01-03 13:32:13 -05:00
fillInDefaultSystemConfiguration: (levelSystems) ->
for system in levelSystems ? [ ]
system . config ? = { }
2014-09-02 15:33:34 -04:00
TreemaUtils . populateDefaults ( system . config , system . model . configSchema , tv4 )
2014-08-28 20:57:39 -04:00
@lastType = ' system '
@lastOriginal = system . model . name
2014-08-29 20:18:03 -04:00
2014-01-03 13:32:13 -05:00
dimensions: ->
width = 0
height = 0
for thang in @ get ( ' thangs ' ) or [ ]
for component in thang . components
c = component . config
continue unless c ?
width = c . width if c . width ? and c . width > width
height = c . height if c . height ? and c . height > height
2014-06-30 22:16:26 -04:00
return { width: width , height: height }
2016-04-07 22:06:57 -04:00
2016-03-30 16:57:19 -04:00
isLadder: ->
return @ get ( ' type ' ) ? . indexOf ( ' ladder ' ) > - 1
2016-04-13 12:54:24 -04:00
2016-07-14 11:58:43 -04:00
isType: (types...) ->
return @ get ( ' type ' , true ) in types
2016-06-26 16:51:14 -04:00
fetchNextForCourse: ({ levelOriginalID, courseInstanceID, courseID, sessionID }, options={}) ->
2016-04-27 18:36:16 -04:00
if courseInstanceID
2016-06-26 16:51:14 -04:00
options.url = " /db/course_instance/ #{ courseInstanceID } /levels/ #{ levelOriginalID } /sessions/ #{ sessionID } /next "
2016-04-27 18:36:16 -04:00
else
options.url = " /db/course/ #{ courseID } /levels/ #{ levelOriginalID } /next "
@ fetch ( options )
2016-05-30 16:51:09 -04:00
getSolutions: ->
return [ ] unless hero = _ . find ( @ get ( " thangs " ) ? [ ] ) , id: ' Hero Placeholder '
return [ ] unless config = _ . find ( hero . components ? [ ] , (x) -> x . config ? . programmableMethods ? . plan ) ? . config
solutions = _ . cloneDeep config . programmableMethods . plan . solutions ? [ ]
for solution in solutions
try
solution.source = _ . template ( solution . source ) ( config ? . programmableMethods ? . plan . context )
catch e
console . error " Problem with template and solution comments for " , @ get ( ' slug ' ) , e
solutions