codecombat/app/lib/LevelLoader.coffee

244 lines
8.2 KiB
CoffeeScript
Raw Normal View History

2014-01-03 13:32:13 -05:00
Level = require 'models/Level'
CocoClass = require 'lib/CocoClass'
AudioPlayer = require 'lib/AudioPlayer'
LevelSession = require 'models/LevelSession'
ThangType = require 'models/ThangType'
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-01-03 13:32:13 -05:00
spriteSheetsBuilt: 0
spriteSheetsToBuild: 0
2014-02-15 18:44:45 -05:00
constructor: (options) ->
2014-01-03 13:32:13 -05:00
super()
2014-02-15 18:44:45 -05:00
@supermodel = options.supermodel
@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
2014-02-15 18:44:45 -05:00
2014-01-03 13:32:13 -05:00
@loadSession()
@loadLevelModels()
@loadAudio()
@playJingle()
2014-02-12 15:41:41 -05:00
_.defer @update # Lets everything else resolve first
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
2014-01-03 13:32:13 -05:00
@session = new LevelSession()
@session.url = -> url
# Unless you specify cache:false, sometimes the browser will use a cached session
# and players will 'lose' code
@session.fetch({cache:false})
2014-02-11 18:47:59 -05:00
@session.once 'sync', @onSessionLoaded, @
2014-02-15 18:44:45 -05:00
if @opponentSessionID
@opponentSession = new LevelSession()
@opponentSession.url = "/db/level_session/#{@opponentSessionID}"
@opponentSession.fetch()
@opponentSession.once 'sync', @onSessionLoaded, @
2014-02-15 18:44:45 -05:00
sessionsLoaded: ->
2014-03-18 14:52:23 -04:00
return true if @headless
@session.loaded and ((not @opponentSession) or @opponentSession.loaded)
2014-02-11 18:47:59 -05:00
onSessionLoaded: ->
return if @destroyed
2014-01-03 13:32:13 -05:00
# TODO: maybe have all non versioned models do this? Or make it work to PUT/PATCH to relative urls
if @session.loaded
@session.url = -> '/db/level.session/' + @id
@update() if @sessionsLoaded()
2014-01-03 13:32:13 -05:00
# Supermodel (Level) Loading
loadLevelModels: ->
2014-02-11 18:47:59 -05:00
@supermodel.on 'loaded-one', @onSupermodelLoadedOne, @
@supermodel.once 'error', @onSupermodelError, @
2014-01-03 13:32:13 -05:00
@level = @supermodel.getModel(Level, @levelID) or new Level _id: @levelID
2014-02-11 18:38:36 -05:00
levelID = @levelID
2014-02-15 18:44:45 -05:00
headless = @headless
2014-02-11 18:38:36 -05:00
@supermodel.shouldPopulate = (model) ->
2014-01-03 13:32:13 -05:00
# if left unchecked, the supermodel would load this level
# and every level next on the chain. This limits the population
handles = [model.id, model.get 'slug']
2014-02-11 18:38:36 -05:00
return model.constructor.className isnt "Level" or levelID in handles
@supermodel.shouldLoadProjection = (model) ->
return true if headless and model.constructor.className is 'ThangType'
false
2014-02-15 18:44:45 -05:00
2014-01-03 13:32:13 -05:00
@supermodel.populateModel @level
2014-02-11 18:47:59 -05:00
onSupermodelError: ->
2014-02-11 18:47:59 -05:00
onSupermodelLoadedOne: (e) ->
@buildSpriteSheetsForThangType e.model if not @headless and e.model instanceof ThangType
@update()
2014-01-03 13:32:13 -05:00
# Things to do when either the Session or Supermodel load
2014-01-03 13:32:13 -05:00
2014-02-12 15:41:41 -05:00
update: =>
return if @destroyed
2014-01-03 13:32:13 -05:00
@notifyProgress()
return if @updateCompleted
2014-02-17 11:15:53 -05:00
return unless @supermodel?.finished() and @sessionsLoaded()
2014-01-03 13:32:13 -05:00
@denormalizeSession()
@loadLevelSounds()
2014-03-18 14:52:23 -04:00
app.tracker.updatePlayState(@level, @session) unless @headless
2014-01-03 13:32:13 -05:00
@updateCompleted = true
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
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
buildSpriteSheetsForThangType: (thangType) ->
@grabThangTypeTeams() unless @thangTypeTeams
for team in @thangTypeTeams[thangType.get('original')] ? [null]
spriteOptions = {resolutionFactor: 4, async: true}
if thangType.get('kind') is 'Floor'
spriteOptions.resolutionFactor = 2
if team and color = @teamConfigs[team]?.color
spriteOptions.colorConfig = team: color
@buildSpriteSheet thangType, spriteOptions
buildSpriteSheet: (thangType, options) ->
if thangType.get('name') is 'Wizard'
options.colorConfig = me.get('wizard')?.colorConfig or {}
building = thangType.buildSpriteSheet options
return unless building
#console.log 'Building:', thangType.get('name'), options
t0 = new Date()
@spriteSheetsToBuild += 1
2014-02-12 15:41:41 -05:00
thangType.once 'build-complete', =>
return if @destroyed
@spriteSheetsBuilt += 1
@notifyProgress()
console.log "Built", thangType.get('name'), 'after', ((new Date()) - t0), 'ms'
# World init
initWorld: ->
return if @initialized
@initialized = true
@world = new World @level.get('name')
serializedLevel = @level.serialize(@supermodel)
@world.loadFromLevel serializedLevel, false
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-01-03 13:32:13 -05:00
allDone: ->
@supermodel.finished() and @sessionsLoaded() and @spriteSheetsBuilt is @spriteSheetsToBuild
2014-01-03 13:32:13 -05:00
progress: ->
return 0 unless @level.loaded
overallProgress = 0
supermodelProgress = @supermodel.progress()
overallProgress += supermodelProgress * 0.7
overallProgress += 0.1 if @sessionsLoaded()
2014-02-15 18:44:45 -05:00
if @headless
spriteMapProgress = 0.2
else
spriteMapProgress = if supermodelProgress is 1 then 0.2 else 0
spriteMapProgress *= @spriteSheetsBuilt / @spriteSheetsToBuild if @spriteSheetsToBuild
2014-01-03 13:32:13 -05:00
overallProgress += spriteMapProgress
return overallProgress
notifyProgress: ->
Backbone.Mediator.publish 'level-loader:progress-changed', progress: @progress()
@initWorld() if @allDone()
@trigger 'progress'
@trigger 'loaded-all' if @progress() is 1
2014-01-03 13:32:13 -05:00
destroy: ->
@supermodel.off 'loaded-one', @onSupermodelLoadedOne
@world = null # don't hold onto garbage
2014-02-15 18:44:45 -05:00
@update = null
super()