Refactored SuperModel, CocoModel and LevelLoader.

Removed the dynamic population of the Level, instead putting straightforward logic into LevelLoader.
Simplified SuperModel.
This commit is contained in:
Scott Erickson 2014-04-25 14:30:06 -07:00
parent c754f7b943
commit 278d6752c3
13 changed files with 170 additions and 291 deletions

View file

@ -1,8 +1,12 @@
Level = require 'models/Level'
CocoClass = require 'lib/CocoClass'
AudioPlayer = require 'lib/AudioPlayer'
LevelComponent = require 'models/LevelComponent'
LevelSystem = require 'models/LevelSystem'
Article = require 'models/Article'
LevelSession = require 'models/LevelSession'
ThangType = require 'models/ThangType'
CocoClass = require 'lib/CocoClass'
AudioPlayer = require 'lib/AudioPlayer'
app = require 'application'
World = require 'lib/world/world'
@ -16,9 +20,6 @@ World = require 'lib/world/world'
module.exports = class LevelLoader extends CocoClass
spriteSheetsBuilt: 0
spriteSheetsToBuild: 0
constructor: (options) ->
super()
@supermodel = options.supermodel
@ -30,10 +31,10 @@ module.exports = class LevelLoader extends CocoClass
@spectateMode = options.spectateMode ? false
@loadSession()
@loadLevelModels()
@loadLevel()
@loadAudio()
@playJingle()
_.defer @update # Lets everything else resolve first
@listenToOnce @supermodel, 'loaded-all', @onSupermodelLoaded
playJingle: ->
return if @headless
@ -56,72 +57,84 @@ module.exports = class LevelLoader extends CocoClass
@session = new LevelSession()
@session.url = -> url
# Unless you specify cache:false, sometimes the browser will use a cached session
# and players will 'lose' code
sessionRes = @supermodel.addModelResource(@session, 'level_session', {cache:false})
@listenToOnce(sessionRes, 'resource:loaded', @onSessionLoaded)
sessionRes.load()
@supermodel.addModelResource(@session, 'level_session', {cache:false}).load()
@session.once 'sync', -> @url = -> '/db/level.session/' + @id
if @opponentSessionID
@opponentSession = new LevelSession()
@opponentSession.url = "/db/level_session/#{@opponentSessionID}"
opponentSessionRes = @supermodel.addModelResource(@opponentSession, 'opponent_session')
@listenToOnce(opponentSessionRes, 'resource:loaded', @onSessionLoaded)
opponentSessionRes.load()
sessionsLoaded: ->
return true if @headless
@session.loaded and ((not @opponentSession) or @opponentSession.loaded)
onSessionLoaded: ->
return if @destroyed
# 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()
@supermodel.addModelResource(@opponentSession, 'opponent_session').load()
# Supermodel (Level) Loading
loadLevelModels: ->
@listenTo(@supermodel, 'superModel:updateProgress', @onSupermodelLoadedOne) # Some models are not added via addModelResource()
@listenToOnce(@supermodel, 'error', @onSupermodelError)
loadLevel: ->
@level = @supermodel.getModel(Level, @levelID) or new Level _id: @levelID
levelID = @levelID
headless = @headless
if @level.loaded
@populateLevel()
else
@supermodel.addModelResource(@level, 'level').load()
@listenToOnce @level, 'sync', @onLevelLoaded
onLevelLoaded: ->
@populateLevel()
populateLevel: ->
thangIDs = []
componentVersions = []
systemVersions = []
articleVersions = []
for thang in @level.get('thangs') or []
thangIDs.push thang.thangType
for comp in thang.components or []
componentVersions.push _.pick(comp, ['original', 'majorVersion'])
for system in @level.get('systems') or []
systemVersions.push _.pick(system, ['original', 'majorVersion'])
if indieSprites = system?.config?.indieSprites
for indieSprite in indieSprites
console.log 'do not forget', indieSprite
thangIDs.push indieSprite.thangType
for article in @level.get('articles')?.generalArticles or []
articleVersions.push _.pick(article, ['original', 'majorVersion'])
@supermodel.shouldPopulate = (model) ->
# 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']
return model.constructor.className isnt "Level" or levelID in handles
objUniq = (array) -> _.uniq array, false, (arg) -> JSON.stringify(arg)
for thangID in _.uniq thangIDs
url = "/db/thang.type/#{thangID}/version"
res = @maybeLoadURL url, ThangType, 'thang'
@listenToOnce res.model, 'sync', @buildSpriteSheetsForThangType if res
for obj in objUniq componentVersions
url = "/db/level.component/#{obj.original}/version/#{obj.majorVersion}"
@maybeLoadURL url, LevelComponent, 'component'
for obj in objUniq systemVersions
url = "/db/level.system/#{obj.original}/version/#{obj.majorVersion}"
@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'
@supermodel.shouldLoadProjection = (model) ->
return true if headless and model.constructor.className is 'ThangType'
false
wizard = ThangType.loadUniversalWizard()
@supermodel.registerModel wizard
if wizard.loading
window.res = @supermodel.addModelResource(wizard, 'thang').load()
console.log window.res.loading, window.res.loaded
@supermodel.populateModel(@level, 'level')
onSupermodelError: ->
onSupermodelLoadedOne: (e) ->
@buildSpriteSheetsForThangType e.model if not @headless and e.model instanceof ThangType
@update() unless @destroyed
# Things to do when either the Session or Supermodel load
update: =>
return if @destroyed
@notifyProgress()
return if @updateCompleted
return unless @supermodel?.finished() and @sessionsLoaded()
@denormalizeSession()
maybeLoadURL: (url, Model, resourceName) ->
return if @supermodel.getModel(url)
model = new Model()
model.url = url
@supermodel.addModelResource(model, resourceName).load()
onSupermodelLoaded: ->
@loadLevelSounds()
@denormalizeSession()
app.tracker.updatePlayState(@level, @session) unless @headless
@updateCompleted = true
@initWorld()
denormalizeSession: ->
return if @headless or @sessionDenormalized or @spectateMode
@ -141,6 +154,16 @@ module.exports = class LevelLoader extends CocoClass
# Building sprite sheets
buildSpriteSheetsForThangType: (thangType) ->
@grabThangTypeTeams() unless @thangTypeTeams
for team in @thangTypeTeams[thangType.get('original')] ? [null]
spriteOptions = {resolutionFactor: 4, async: false}
if thangType.get('kind') is 'Floor'
spriteOptions.resolutionFactor = 2
if team and color = @teamConfigs[team]?.color
spriteOptions.colorConfig = team: color
@buildSpriteSheet thangType, spriteOptions
grabThangTypeTeams: ->
@grabTeamConfigs()
@thangTypeTeams = {}
@ -161,41 +184,20 @@ module.exports = class LevelLoader extends CocoClass
@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: false}
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
@spriteSheetsToBuild += 1
onBuildComplete = =>
return if @destroyed
@spriteSheetsBuilt += 1
@notifyProgress()
if options.async
thangType.once 'build-complete', onBuildComplete
else
onBuildComplete()
thangType.buildSpriteSheet options
# World init
initWorld: ->
console.debug('gintau', 'init-world')
return if @initialized
@initialized = true
@world = new World @level.get('name')
serializedLevel = @level.serialize(@supermodel)
@world.loadFromLevel serializedLevel, false
console.log 'loaded from level?', @world
# Initial Sound Loading
@ -220,30 +222,4 @@ module.exports = class LevelLoader extends CocoClass
# everything else sound wise is loaded as needed as worlds are generated
allDone: ->
@supermodel.finished() and @sessionsLoaded() and @spriteSheetsBuilt is @spriteSheetsToBuild
progress: ->
return 0 unless @level.loaded
supermodelProgress = @supermodel.getProgress()
overallProgress = supermodelProgress * 0.7
overallProgress += 0.1 if @sessionsLoaded()
spriteMapProgress = 1
unless @headless
spriteMapProgress = @spriteSheetsBuilt / @spriteSheetsToBuild if @spriteSheetsToBuild
spriteMapProgress *= 0.2
overallProgress += spriteMapProgress
return overallProgress
notifyProgress: ->
progress = @progress()
Backbone.Mediator.publish 'level-loader:progress-changed', progress: progress
@initWorld() if @allDone()
@trigger 'progress'
# console.debug 'gintau', 'notify-notifyProgress', progress
if progress is 1
# console.debug 'gintau', 'notify-loaded-all'
@trigger 'loaded-all'
progress: -> @supermodel.progress

View file

@ -2,58 +2,14 @@ module.exports = class SuperModel extends Backbone.Model
constructor: ->
@num = 0
@denom = 0
@showing = false
@progress = 0
@resources = {}
@rid = 0
@models = {}
@collections = {}
@schemas = {}
# setInterval(@checkModelStatus, 5000)
# For debugging
checkModelStatus: =>
for key, res of @resources
continue if res.isLoaded
console.debug 'resource ' + res.name + ' is still loading'
populateModel: (model, resName) ->
@mustPopulate = model
model.saveBackups = @shouldSaveBackups(model)
@addModel(model)
@modelLoaded(model) if model.loaded
resName = model.url unless resName
modelRes = @addModelResource(model, model.url)
schema = model.schema()
@schemas[schema.urlRoot] = schema
modelRes.load()
return modelRes
# replace or overwrite
shouldLoadReference: (model) -> true
shouldLoadProjection: (model) -> false
shouldPopulate: (url) -> true
shouldSaveBackups: (model) -> false
modelErrored: (model) ->
@trigger 'error'
@removeEventsFromModel(model)
modelLoaded: (model) ->
@trigger 'loaded-one', model: model
@removeEventsFromModel(model)
removeEventsFromModel: (model) ->
# "Request" resource may have no off()
# "Something" resource may have no model.
model?.off? 'sync', @modelLoaded, @
model?.off? 'error', @modelErrored, @
# Caching logic
getModel: (ModelClass_or_url, id) ->
return @getModelByURL(ModelClass_or_url) if _.isString(ModelClass_or_url)
@ -74,7 +30,7 @@ module.exports = class SuperModel extends Backbone.Model
return (m for key, m of @models when m.constructor.className is ModelClass.className) if ModelClass
return _.values @models
addModel: (model) ->
registerModel: (model) ->
url = model.url
@models[url] = model
@ -96,15 +52,17 @@ module.exports = class SuperModel extends Backbone.Model
if cachedModel
collection.models[i] = cachedModel
else
@addModel(model)
@registerModel(model)
collection
# New, loading tracking stuff
finished: ->
return @progress is 1.0 or Object.keys(@resources).length is 0
addModelResource: (modelOrCollection, name, fetchOptions, value=1) ->
@checkName(name)
@addModel(modelOrCollection)
@registerModel(modelOrCollection)
res = new ModelResource(modelOrCollection, name, fetchOptions, value)
@storeResource(res, value)
return res
@ -129,88 +87,58 @@ module.exports = class SuperModel extends Backbone.Model
@rid++
resource.rid = @rid
@resources[@rid] = resource
@listenToOnce(resource, 'resource:loaded', @onResourceLoaded)
@listenTo(resource, 'resource:failed', @onResourceFailed)
@listenToOnce(resource, 'loaded', @onResourceLoaded)
@listenTo(resource, 'failed', @onResourceFailed)
@denom += value
loadResources: ->
for rid, res of @resources
res.load()
onResourceLoaded: (r) ->
@modelLoaded(r.model)
# Check if the model has references
if r.constructor.name is 'ModelResource'
model = r.model
@addModelRefencesToLoad(model)
@updateProgress(r)
else
@updateProgress(r)
@num += r.value
_.defer @updateProgress
onResourceFailed: (source) ->
@trigger('resource:failed', source)
@modelErrored(source.resource.model)
@trigger('failed', source)
addModelRefencesToLoad: (model) ->
schema = model.schema?()
return unless schema
refs = model.getReferencedModels(model.attributes, schema.attributes, '/', @shouldLoadProjection)
refs = [] unless @mustPopulate is model or @shouldPopulate(model)
for ref, i in refs when @shouldLoadReference ref
ref.saveBackups = @shouldSaveBackups(ref)
refURL = ref.url()
continue if @models[refURL]
@models[refURL] = ref
res = @addModelResource(ref, refURL)
res.load()
updateProgress: (r) =>
@num += r.value
updateProgress: =>
# Because this is _.defer'd, this might end up getting called after
# a bunch of things load all at once.
# So make sure we only emit events if @progress has changed.
return if @progress is @num / @denom
@progress = @num / @denom
# console.debug 'gintau', 'supermodel-updateProgress', @progress, @num, @denom
@trigger('superModel:updateProgress', @progress)
@trigger('update-progress', @progress)
@trigger('loaded-all') if @finished()
getResource: (rid)->
return @resources[rid]
getProgress: -> return @progress
class Resource extends Backbone.Model
constructor: (name, value=1) ->
@name = name
@value = value
@dependencies = []
@rid = -1 # Used for checking state and reloading
@isLoading = false
@isLoaded = false
@model = null
@loadDeferred = null
addDependency: (depRes) ->
return if depRes.isLoaded
@dependencies.push(depRes)
@jqxhr = null
markLoaded: ->
# console.debug 'gintau', 'markLoaded', @
@trigger('resource:loaded', @) if not @isLoaded
return if @isLoaded
@trigger('loaded', @)
@isLoaded = true
@isLoading = false
markFailed: (error) ->
@trigger('resource:failed', {resource: @, error: error}) if not @isLoaded
markFailed: ->
return if @isLoaded
@trigger('failed', {resource: @})
@isLoaded = @isLoading = false
markLoading: ->
@isLoaded = false
@isLoading = false
@isLoading = true
load: -> @
load: ->
isReadyForLoad: -> return not (@isloaded and @isLoading)
getModel: -> @model
class ModelResource extends Resource
constructor: (modelOrCollection, name, fetchOptions, value)->
@ -219,78 +147,29 @@ class ModelResource extends Resource
@fetchOptions = fetchOptions
load: ->
return @loadDeferred.promise() if @isLoading or @isLoaded
@isLoading = true
@loadDeferred = $.Deferred()
@markLoading()
@fetchModel()
return @loadDeferred.promise()
@
fetchModel: ->
@model.fetch(@fetchOptions)
@listenToOnce(@model, 'sync', ->
@markLoaded()
@loadDeferred.resolve(@)
)
@jqxhr = @model.fetch(@fetchOptions) unless @model.loading
@listenToOnce @model, 'sync', -> @markLoaded()
@listenToOnce @model, 'error', -> @markFailed()
@listenToOnce(@model, 'error', ->
@markFailed('Failed to load resource.')
@loadDeferred.reject(@)
)
class RequestResource extends Resource
constructor: (name, jqxhrOptions, value) ->
super(name, value)
@model = $.ajax(jqxhrOptions)
@jqxhrOptions = jqxhrOptions
@loadDeferred = @model
load: ->
return @loadDeferred.promise() if @isLoading or @isLoaded
@markLoading()
@jqxhr = $.ajax(jqxhrOptions)
@jqxhr.done @markLoaded()
@jqxhr.fail @markFailed()
@
@isLoading = true
$.when.apply($, @loadDependencies())
.then(@onLoadDependenciesSuccess, @onLoadDependenciesFailed)
.always(()=> @isLoading = false)
return @loadDeferred.promise()
loadDependencies: ->
promises = []
for dep in @dependencies
continue if not dep.isReadyForLoad()
promises.push(dep.load())
return promises
onLoadDependenciesSuccess: =>
@model = $.ajax(@jqxhrOptions)
@model.done(
=> @markLoaded()
).fail(
(jqXHR, textStatus, errorThrown) =>
@markFailed(errorThrown)
)
onLoadDependenciesFailed: =>
@markFailed('Failed to load dependencies.')
class SomethingResource extends Resource
constructor: (name, value) ->
super(name, value)
@loadDeferred = $.Deferred()
load: ->
return @loadDeferred.promise()
markLoaded: ->
@loadDeferred.resolve()
super()
markFailed: (error) ->
@loadDeferred.reject()
super(error)

View file

@ -332,7 +332,7 @@ class LatestVersionReferenceNode extends TreemaNode
if @instance and not m
m = @instance
m.url = -> urlGoingFor
@settings.supermodel.addModel(m)
@settings.supermodel.registerModel(m)
return 'Unknown' unless m
return m.get('name')

View file

@ -20,7 +20,7 @@ module.exports = class ThangComponentEditView extends CocoView
@componentCollection = @supermodel.getCollection new ComponentsCollection()
@componentCollectionRes = @supermodel.addModelResource(@componentCollection, 'component_collection')
@listenToOnce(@componentCollectionRes, 'resource:loaded', @onComponentsSync)
@listenToOnce(@componentCollectionRes, 'loaded', @onComponentsSync)
@componentCollectionRes.load()
onloaded: -> @render()

View file

@ -33,6 +33,6 @@ module.exports = class LevelComponentNewView extends View
console.log "Got errors:", JSON.parse(res.responseText)
forms.applyErrorsToForm(@$el, JSON.parse(res.responseText))
res.success =>
@supermodel.addModel component
@supermodel.registerModel component
Backbone.Mediator.publish 'edit-level-component', original: component.get('_id'), majorVersion: 0
@hide()

View file

@ -63,7 +63,7 @@ module.exports = class EditorLevelView extends View
)
@levelRes = @supermodel.addModelResource(@level, 'level')
@listenToOnce(@levelRes, 'resource:loaded', ->
@listenToOnce(@levelRes, 'loaded', ->
@world = new World @level.name
@worldRes.markLoaded()
)

View file

@ -32,6 +32,6 @@ module.exports = class LevelSystemNewView extends View
console.log "Got errors:", JSON.parse(res.responseText)
forms.applyErrorsToForm(@$el, JSON.parse(res.responseText))
res.success =>
@supermodel.addModel system
@supermodel.registerModel system
Backbone.Mediator.publish 'edit-level-system', original: system.get('_id'), majorVersion: 0
@hide()

View file

@ -37,13 +37,13 @@ module.exports = class SystemsTabView extends View
do (url) -> ls.url = -> url
continue if @supermodel.getModelByURL ls.url
@listenToOnce(lsRes, 'resource:loaded', @onSystemLoaded)
@listenToOnce(lsRes, 'loaded', @onSystemLoaded)
++@toLoad
@onDefaultSystemsLoaded() unless @toLoad
onSystemLoaded: (lsRes) ->
ls = lsRes.model
@supermodel.addModel(ls)
@supermodel.registerModel(ls)
--@toLoad
@onDefaultSystemsLoaded() unless @toLoad

View file

@ -63,7 +63,7 @@ module.exports = class ThangsTabView extends View
@thangTypes = @supermodel.getCollection new ThangTypeSearchCollection() # should load depended-on Components, too
@thangTypesRes = @supermodel.addModelResource(@thangTypes, 'thang_type_search_collection')
@listenToOnce(@thangTypesRes, 'resource:loaded', @onThangTypesLoaded)
@listenToOnce(@thangTypesRes, 'loaded', @onThangTypesLoaded)
@thangTypesRes.load()
$(document).bind 'contextmenu', @preventDefaultContextMenu
@ -72,7 +72,7 @@ module.exports = class ThangsTabView extends View
@componentCollection = @supermodel.getCollection new ComponentsCollection()
@componentCollectionRes = @supermodel.addModelResource(@componentCollection, 'components_collection')
@listenToOnce(@componentCollectionRes, 'resource:loaded', @onComponentsLoaded)
@listenToOnce(@componentCollectionRes, 'loaded', @onComponentsLoaded)
@componentCollectionRes.load()
onThangTypesLoaded: ->

View file

@ -22,7 +22,7 @@ module.exports = class PatchesView extends CocoView
@patches = new PatchesCollection([], {}, @model, @status)
@patchesRes = @supermodel.addModelResource(@patches, 'patches')
@patchesRes.load()
@listenTo(@patchesRes, 'resource:loaded', @load)
@listenTo(@patchesRes, 'loaded', @load)
load: ->
return unless @patchesRes.loaded

View file

@ -44,8 +44,8 @@ module.exports = class CocoView extends Backbone.View
# Backbone.Mediator handles subscription setup/teardown automatically
@listenToOnce(@supermodel, 'loaded-all', @onLoaded)
@listenTo(@supermodel, 'superModel:updateProgress', @updateProgress)
@listenTo(@supermodel, 'resource:failed', @onResourceLoadFailed)
@listenTo(@supermodel, 'update-progress', @updateProgress)
@listenTo(@supermodel, 'failed', @onResourceLoadFailed)
super options

View file

@ -97,6 +97,7 @@ module.exports = class PlayLevelView extends View
application.tracker?.trackEvent 'Started Level Load', level: @levelID, label: @levelID
onLevelLoadError: (e) ->
# TODO NOW: remove this in favor of the supermodel handling it
application.router.navigate "/play?not_found=#{@levelID}", {trigger: true}
setLevel: (@level, @supermodel) ->
@ -110,8 +111,7 @@ module.exports = class PlayLevelView extends View
load: ->
@loadStartTime = new Date()
@levelLoader = new LevelLoader supermodel: @supermodel, levelID: @levelID, sessionID: @sessionID, opponentSessionID: @getQueryVariable('opponent'), team: @getQueryVariable("team")
@listenToOnce(@levelLoader, 'loaded-all', @onLevelLoaderLoaded)
@listenTo(@levelLoader, 'progress', @onLevelLoaderProgressChanged)
# @listenTo(@levelLoader, 'progress', @onLevelLoaderProgressChanged) # TODO NOW: transfer to supermodel system
@god = new God()
getRenderData: ->
@ -123,7 +123,6 @@ module.exports = class PlayLevelView extends View
c.explainHourOfCode = elapsed < 86400 * 1000
c
onLoaded: ->
afterRender: ->
super()
window.onPlayLevelViewLoaded? @ # still a hack
@ -151,7 +150,10 @@ module.exports = class PlayLevelView extends View
Backbone.Mediator.subscribeOnce 'modal-closed', @onLevelLoaderLoaded, @
return true
onLevelLoaderLoaded: ->
onLoaded: ->
_.defer => @onLevelLoaded()
onLevelLoaded: ->
console.debug 'level_view', 'onLevelLoaderLoaded', @levelLoader.progress()
return unless @levelLoader.progress() is 1 # double check, since closing the guide may trigger this early
@ -378,6 +380,7 @@ module.exports = class PlayLevelView extends View
.css('top', target_top - 50)
.css('left', target_left - 50)
setTimeout(()=>
return if @destroyed
@animatePointer()
clearInterval(@pointerInterval)
@pointerInterval = setInterval(@animatePointer, 1200)

View file

@ -0,0 +1,21 @@
CocoView = require 'views/kinds/CocoView'
template = require 'templates/supermodel.demo'
User = require 'models/User'
module.exports = class UnnamedView extends CocoView
id: "supermodel-demo-view"
template: template
constructor: (options) ->
super(options)
@load()
load: ->
@supermodel.addModelResource(new User(me.id))
# getRenderData: ->
# c = super()
# c
# destroy: ->
# super()