codecombat/app/lib/LevelLoader.coffee

295 lines
11 KiB
CoffeeScript
Raw Normal View History

2014-01-03 13:32:13 -05:00
Level = require 'models/Level'
LevelComponent = require 'models/LevelComponent'
LevelSystem = require 'models/LevelSystem'
Article = require 'models/Article'
2014-01-03 13:32:13 -05:00
LevelSession = require 'models/LevelSession'
ThangType = require 'models/ThangType'
ThangNamesCollection = require 'collections/ThangNamesCollection'
CocoClass = require 'lib/CocoClass'
AudioPlayer = require 'lib/AudioPlayer'
2014-01-03 13:32:13 -05:00
app = require 'application'
World = require 'lib/world/world'
2014-01-03 13:32:13 -05:00
# This is an initial stab at unifying loading and setup into a single place which can
# monitor everything and keep a LoadingScreen visible overall progress.
#
# Would also like to incorporate into here:
# * World Building
# * Sprite map generation
# * Connecting to Firebase
module.exports = class LevelLoader extends CocoClass
2014-02-15 18:44:45 -05:00
constructor: (options) ->
@t0 = new Date().getTime()
2014-01-03 13:32:13 -05:00
super()
2014-02-15 18:44:45 -05:00
@supermodel = options.supermodel
@supermodel.setMaxProgress 0.2
2014-02-15 18:44:45 -05:00
@levelID = options.levelID
@sessionID = options.sessionID
@opponentSessionID = options.opponentSessionID
@team = options.team
@headless = options.headless
2014-03-12 20:51:09 -04:00
@spectateMode = options.spectateMode ? false
@editorMode = options.editorMode # TODO: remove when the surface can load ThangTypes itself
2014-02-15 18:44:45 -05:00
2014-01-03 13:32:13 -05:00
@loadSession()
@loadLevel()
2014-01-03 13:32:13 -05:00
@loadAudio()
@playJingle()
if @supermodel.finished()
@onSupermodelLoaded()
else
@listenToOnce @supermodel, 'loaded-all', @onSupermodelLoaded
2014-01-03 13:32:13 -05:00
playJingle: ->
2014-02-15 18:44:45 -05:00
return if @headless
# Apparently the jingle, when it tries to play immediately during all this loading, you can't hear it.
# Add the timeout to fix this weird behavior.
f = ->
jingles = ["ident_1", "ident_2"]
AudioPlayer.playInterfaceSound jingles[Math.floor Math.random() * jingles.length]
setTimeout f, 500
2014-01-03 13:32:13 -05:00
# Session Loading
2014-01-03 13:32:13 -05:00
loadSession: ->
2014-03-18 14:52:23 -04:00
return if @headless
if @sessionID
url = "/db/level_session/#{@sessionID}"
else
url = "/db/level/#{@levelID}/session"
url += "?team=#{@team}" if @team
2014-02-15 18:44:45 -05:00
session = new LevelSession().setURL url
@sessionResource = @supermodel.loadModel(session, 'level_session', {cache:false})
@session = @sessionResource.model
@session.once 'sync', -> @url = -> '/db/level.session/' + @id
2014-02-15 18:44:45 -05:00
if @opponentSessionID
opponentSession = new LevelSession().setURL "/db/level_session/#{@opponentSessionID}"
@opponentSessionResource = @supermodel.loadModel(opponentSession, 'opponent_session')
@opponentSession = @opponentSessionResource.model
2014-01-03 13:32:13 -05:00
# Supermodel (Level) Loading
loadLevel: ->
2014-01-03 13:32:13 -05:00
@level = @supermodel.getModel(Level, @levelID) or new Level _id: @levelID
if @level.loaded
@populateLevel()
else
@level = @supermodel.loadModel(@level, 'level').model
@listenToOnce @level, 'sync', @onLevelLoaded
2014-04-29 19:43:46 -04:00
onLevelLoaded: ->
@populateLevel()
2014-04-29 19:43:46 -04:00
populateLevel: ->
thangIDs = []
componentVersions = []
systemVersions = []
articleVersions = []
2014-04-29 19:43:46 -04:00
for thang in @level.get('thangs') or []
thangIDs.push thang.thangType
for comp in thang.components or []
componentVersions.push _.pick(comp, ['original', 'majorVersion'])
2014-04-29 19:43:46 -04:00
for system in @level.get('systems') or []
systemVersions.push _.pick(system, ['original', 'majorVersion'])
if indieSprites = system?.config?.indieSprites
for indieSprite in indieSprites
thangIDs.push indieSprite.thangType
2014-04-29 19:43:46 -04:00
unless @headless
for article in @level.get('documentation')?.generalArticles or []
articleVersions.push _.pick(article, ['original', 'majorVersion'])
objUniq = (array) -> _.uniq array, false, (arg) -> JSON.stringify(arg)
worldNecessities = []
@thangIDs = _.uniq thangIDs
@thangNames = new ThangNamesCollection(@thangIDs)
worldNecessities.push @supermodel.loadCollection(@thangNames, 'thang_names')
worldNecessities.push @sessionResource if @sessionResource?.isLoading
worldNecessities.push @opponentSessionResource if @opponentSessionResource?.isLoading
for obj in objUniq componentVersions
url = "/db/level.component/#{obj.original}/version/#{obj.majorVersion}"
worldNecessities.push @maybeLoadURL(url, LevelComponent, 'component')
for obj in objUniq systemVersions
url = "/db/level.system/#{obj.original}/version/#{obj.majorVersion}"
worldNecessities.push @maybeLoadURL(url, LevelSystem, 'system')
for obj in objUniq articleVersions
url = "/db/article/#{obj.original}/version/#{obj.majorVersion}"
@maybeLoadURL url, Article, 'article'
if obj = @level.get 'nextLevel'
url = "/db/level/#{obj.original}/version/#{obj.majorVersion}"
@maybeLoadURL url, Level, 'level'
2014-05-01 18:44:50 -04:00
unless @headless and not @editorMode
2014-04-29 19:43:46 -04:00
wizard = ThangType.loadUniversalWizard()
@supermodel.loadModel wizard, 'thang'
jqxhrs = (resource.jqxhr for resource in worldNecessities when resource?.jqxhr)
$.when(jqxhrs...).done(@onWorldNecessitiesLoaded)
onWorldNecessitiesLoaded: =>
@initWorld()
@supermodel.clearMaxProgress()
@trigger 'world-necessities-loaded'
return if @headless and not @editorMode
thangsToLoad = _.uniq( (t.spriteName for t in @world.thangs when t.exists) )
nameModelTuples = ([thangType.get('name'), thangType] for thangType in @thangNames.models)
nameModelMap = _.zipObject nameModelTuples
@spriteSheetsToBuild = []
for thangTypeName in thangsToLoad
thangType = nameModelMap[thangTypeName]
continue if thangType.isFullyLoaded()
thangType.fetch()
thangType = @supermodel.loadModel(thangType, 'thang').model
res = @supermodel.addSomethingResource "sprite_sheet", 5
res.thangType = thangType
res.markLoading()
@spriteSheetsToBuild.push res
@buildLoopInterval = setInterval @buildLoop, 5
maybeLoadURL: (url, Model, resourceName) ->
return if @supermodel.getModel(url)
model = new Model().setURL url
@supermodel.loadModel(model, resourceName)
2014-04-29 19:43:46 -04:00
onSupermodelLoaded: ->
console.log 'SuperModel for Level loaded in', new Date().getTime() - @t0, 'ms'
2014-01-03 13:32:13 -05:00
@loadLevelSounds()
@denormalizeSession()
2014-03-18 14:52:23 -04:00
app.tracker.updatePlayState(@level, @session) unless @headless
buildLoop: =>
2014-05-20 13:46:52 -04:00
someLeft = false
for spriteSheetResource, i in @spriteSheetsToBuild
2014-05-20 13:46:52 -04:00
continue if spriteSheetResource.spriteSheetKeys
someLeft = true
thangType = spriteSheetResource.thangType
if thangType.loaded and not thangType.loading
keys = @buildSpriteSheetsForThangType spriteSheetResource.thangType
if keys and keys.length
@listenTo spriteSheetResource.thangType, 'build-complete', @onBuildComplete
spriteSheetResource.spriteSheetKeys = keys
else
spriteSheetResource.markLoaded()
2014-05-20 13:46:52 -04:00
clearInterval @buildLoopInterval unless someLeft
onBuildComplete: (e) ->
resource = null
for resource in @spriteSheetsToBuild
break if e.thangType is resource.thangType
resource.spriteSheetKeys = (k for k in resource.spriteSheetKeys when k isnt e.key)
resource.markLoaded() if resource.spriteSheetKeys.length is 0
2014-01-03 13:32:13 -05:00
denormalizeSession: ->
2014-03-18 14:52:23 -04:00
return if @headless or @sessionDenormalized or @spectateMode
2014-01-03 13:32:13 -05:00
patch =
'levelName': @level.get('name')
'levelID': @level.get('slug') or @level.id
if me.id is @session.get 'creator'
patch.creatorName = me.get('name')
for key, value of patch
if @session.get(key) is value
delete patch[key]
unless _.isEmpty patch
@session.set key, value for key, value of patch
tempSession = new LevelSession _id: @session.id
tempSession.save(patch, {patch: true})
2014-01-03 13:32:13 -05:00
@sessionDenormalized = true
# Building sprite sheets
buildSpriteSheetsForThangType: (thangType) ->
return if @headless
# TODO: Finish making sure the supermodel loads the raster image before triggering load complete, and that the cocosprite has access to the asset.
# if f = thangType.get('raster')
# queue = new createjs.LoadQueue()
# queue.loadFile('/file/'+f)
@grabThangTypeTeams() unless @thangTypeTeams
2014-05-20 13:46:52 -04:00
keys = []
for team in @thangTypeTeams[thangType.get('original')] ? [null]
2014-05-20 13:46:52 -04:00
spriteOptions = {resolutionFactor: SPRITE_RESOLUTION_FACTOR, async: true}
if thangType.get('kind') is 'Floor'
spriteOptions.resolutionFactor = 2
if team and color = @teamConfigs[team]?.color
spriteOptions.colorConfig = team: color
2014-05-20 13:46:52 -04:00
key = @buildSpriteSheet thangType, spriteOptions
if _.isString(key) then keys.push key
keys
grabThangTypeTeams: ->
@grabTeamConfigs()
@thangTypeTeams = {}
for thang in @level.get('thangs')
for component in thang.components
if team = component.config?.team
@thangTypeTeams[thang.thangType] ?= []
@thangTypeTeams[thang.thangType].push team unless team in @thangTypeTeams[thang.thangType]
break
@thangTypeTeams
grabTeamConfigs: ->
for system in @level.get('systems')
if @teamConfigs = system.config?.teamConfigs
break
unless @teamConfigs
# Hack: pulled from Alliance System code. TODO: put in just one place.
@teamConfigs = {"humans":{"superteam":"humans","color":{"hue":0,"saturation":0.75,"lightness":0.5},"playable":true},"ogres":{"superteam":"ogres","color":{"hue":0.66,"saturation":0.75,"lightness":0.5},"playable":false},"neutral":{"superteam":"neutral","color":{"hue":0.33,"saturation":0.75,"lightness":0.5}}}
@teamConfigs
buildSpriteSheet: (thangType, options) ->
if thangType.get('name') is 'Wizard'
options.colorConfig = me.get('wizard')?.colorConfig or {}
thangType.buildSpriteSheet options
# World init
initWorld: ->
return if @initialized
@initialized = true
@world = new World()
@world.levelSessionIDs = if @opponentSessionID then [@sessionID, @opponentSessionID] else [@sessionID]
serializedLevel = @level.serialize(@supermodel)
@world.loadFromLevel serializedLevel, false
console.log "World has been initialized from level loader."
2014-01-03 13:32:13 -05:00
# Initial Sound Loading
loadAudio: ->
2014-02-15 18:44:45 -05:00
return if @headless
2014-01-03 13:32:13 -05:00
AudioPlayer.preloadInterfaceSounds ["victory"]
2014-01-03 13:32:13 -05:00
loadLevelSounds: ->
2014-02-15 18:44:45 -05:00
return if @headless
2014-01-03 13:32:13 -05:00
scripts = @level.get 'scripts'
return unless scripts
for script in scripts when script.noteChain
for noteGroup in script.noteChain when noteGroup.sprites
for sprite in noteGroup.sprites when sprite.say?.sound
AudioPlayer.preloadSoundReference(sprite.say.sound)
thangTypes = @supermodel.getModels(ThangType)
for thangType in thangTypes
for trigger, sounds of thangType.get('soundTriggers') or {} when trigger isnt 'say'
AudioPlayer.preloadSoundReference sound for sound in sounds
2014-01-03 13:32:13 -05:00
# everything else sound wise is loaded as needed as worlds are generated
2014-04-29 19:43:46 -04:00
progress: -> @supermodel.progress
destroy: ->
clearInterval @buildLoopInterval if @buildLoopInterval
super()