From cc025942f81b81e4221f84b6fa4f93e90436e8f9 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Thu, 7 Aug 2014 18:27:47 -0700 Subject: [PATCH] Extended the LevelLoader to load thang types and components dynamically for hero levels. --- app/assets/javascripts/mock-ajax.js | 26 +++ app/lib/LevelLoader.coffee | 63 ++++++- app/models/Level.coffee | 1 + app/models/LevelComponent.coffee | 2 + app/models/SuperModel.coffee | 1 + app/schemas/models/level.coffee | 3 +- app/schemas/models/level_session.coffee | 4 + server/commons/Handler.coffee | 4 +- ...er.spec.coffee => GoalManager.spec.coffee} | 0 test/app/lib/LevelLoader.spec.coffee | 171 ++++++++++++++++++ test/app/require.spec.coffee | 2 +- test/app/utils.coffee | 2 + 12 files changed, 272 insertions(+), 7 deletions(-) rename test/app/lib/{goal_manager.spec.coffee => GoalManager.spec.coffee} (100%) create mode 100644 test/app/lib/LevelLoader.spec.coffee create mode 100644 test/app/utils.coffee diff --git a/app/assets/javascripts/mock-ajax.js b/app/assets/javascripts/mock-ajax.js index 966033263..6743da0c8 100644 --- a/app/assets/javascripts/mock-ajax.js +++ b/app/assets/javascripts/mock-ajax.js @@ -260,6 +260,32 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. return matching_requests; }; + + this.sendResponses = function(responseMap) { + var urls = Object.keys(responseMap); + for(var i in urls) { + var url = urls[i]; + var responseBody = responseMap[url]; + var responded = false; + + var requests = jasmine.Ajax.requests.all(); + for(var j in requests) { + var request = requests[j]; + if(request.url.startsWith(url)) { + request.response({status: 200, responseText: JSON.stringify(responseBody)}); + responded = true; + break; + } + } + if(!responded) { + var allRequests = jasmine.Ajax.requests.all(); + urls = []; + for(var k in allRequests) urls.push(allRequests[k].url); + console.error('could not find response for', url, 'in', urls, allRequests); + continue; + } + } + } } function RequestStub(url, stubData) { diff --git a/app/lib/LevelLoader.coffee b/app/lib/LevelLoader.coffee index 12f0158cc..6b7382120 100644 --- a/app/lib/LevelLoader.coffee +++ b/app/lib/LevelLoader.coffee @@ -33,6 +33,8 @@ module.exports = class LevelLoader extends CocoClass @headless = options.headless @spectateMode = options.spectateMode ? false + @worldNecessities = [] + @listenTo @supermodel, 'resource-loaded', @onWorldNecessityLoaded @loadSession() @loadLevel() @loadAudio() @@ -64,12 +66,23 @@ module.exports = class LevelLoader extends CocoClass session = new LevelSession().setURL url @sessionResource = @supermodel.loadModel(session, 'level_session', {cache: false}) @session = @sessionResource.model - @session.once 'sync', -> @url = -> '/db/level.session/' + @id + @listenToOnce @session, 'sync', @onSessionLoaded if @opponentSessionID opponentSession = new LevelSession().setURL "/db/level_session/#{@opponentSessionID}" @opponentSessionResource = @supermodel.loadModel(opponentSession, 'opponent_session') @opponentSession = @opponentSessionResource.model + @listenToOnce @opponentSession, 'sync', @onSessionLoaded + + onSessionLoaded: (session) -> + session.url = -> '/db/level.session/' + @id + if heroConfig = session.get('heroConfig') + url = "/db/thang.type/#{heroConfig.thangType}/version?project=name,components" + @worldNecessities.push @maybeLoadURL(url, ThangType, 'thang') + + for itemThangType in _.values(heroConfig.inventory) + url = "/db/thang.type/#{itemThangType}/version?project=name,components" + @worldNecessities.push @maybeLoadURL(url, ThangType, 'thang') # Supermodel (Level) Loading @@ -92,6 +105,7 @@ module.exports = class LevelLoader extends CocoClass for thang in @level.get('thangs') or [] thangIDs.push thang.thangType + @loadItemThangsEquippedByLevelThang(thang) for comp in thang.components or [] componentVersions.push _.pick(comp, ['original', 'majorVersion']) @@ -112,6 +126,7 @@ module.exports = class LevelLoader extends CocoClass @thangIDs = _.uniq thangIDs @thangNames = new ThangNamesCollection(@thangIDs) worldNecessities.push @supermodel.loadCollection(@thangNames, 'thang_names') + @listenToOnce @thangNames, 'sync', @onThangNamesLoaded worldNecessities.push @sessionResource if @sessionResource?.isLoading worldNecessities.push @opponentSessionResource if @opponentSessionResource?.isLoading @@ -132,8 +147,50 @@ module.exports = class LevelLoader extends CocoClass wizard = ThangType.loadUniversalWizard() @supermodel.loadModel wizard, 'thang' - jqxhrs = (resource.jqxhr for resource in worldNecessities when resource?.jqxhr) - $.when(jqxhrs...).done(@onWorldNecessitiesLoaded) + @worldNecessities = @worldNecessities.concat worldNecessities + + loadItemThangsEquippedByLevelThang: (levelThang) -> + return unless levelThang.components + for component in levelThang.components + if component.original is LevelComponent.EquipsID and inventory = component.config?.inventory + for itemThangType in _.values(inventory) + url = "/db/thang.type/#{itemThangType}/version?project=name,components" + @worldNecessities.push @maybeLoadURL(url, ThangType, 'thang') + + onThangNamesLoaded: (thangNames) -> + if @level.get('type') is 'hero' + for thangType in thangNames.models + @loadDefaultComponentsForThangType(thangType) + @loadEquippedItemsInheritedFromThangType(thangType) + + loadDefaultComponentsForThangType: (thangType) -> + return unless components = thangType.get('components') + for component in components + url = "/db/level.component/#{component.original}/version/#{component.majorVersion}" + @worldNecessities.push @maybeLoadURL(url, LevelComponent, 'component') + + loadEquippedItemsInheritedFromThangType: (thangType) -> + for levelThang in @level.get('thangs') or [] + if levelThang.thangType is thangType.get('original') + levelThang = $.extend true, {}, levelThang + @level.denormalizeThang(levelThang, @supermodel) + equipsComponent = _.find levelThang.components, {original: LevelComponent.EquipsID} + inventory = equipsComponent.config?.inventory + continue unless inventory + for itemThangType in _.values inventory + url = "/db/thang.type/#{itemThangType}/version?project=name,components" + @worldNecessities.push @maybeLoadURL(url, ThangType, 'thang') + + onWorldNecessityLoaded: (resource) -> + index = @worldNecessities.indexOf(resource) + if @level.get('type') is 'hero' and resource.name is 'thang' + @loadDefaultComponentsForThangType(resource.model) + @loadEquippedItemsInheritedFromThangType(resource.model) + + return unless index >= 0 + @worldNecessities.splice(index, 1) + @worldNecessities = (r for r in @worldNecessities when r?) + @onWorldNecessitiesLoaded() if @worldNecessities.length is 0 onWorldNecessitiesLoaded: => @initWorld() diff --git a/app/models/Level.coffee b/app/models/Level.coffee index cf7cfd52e..d458031ce 100644 --- a/app/models/Level.coffee +++ b/app/models/Level.coffee @@ -32,6 +32,7 @@ module.exports = class Level extends CocoModel o denormalizeThang: (levelThang, supermodel) -> + levelThang.components ?= [] thangType = supermodel.getModelByOriginal(ThangType, levelThang.thangType) configs = {} for thangComponent in levelThang.components diff --git a/app/models/LevelComponent.coffee b/app/models/LevelComponent.coffee index 0e261482d..4ab97c9c5 100644 --- a/app/models/LevelComponent.coffee +++ b/app/models/LevelComponent.coffee @@ -3,6 +3,8 @@ CocoModel = require './CocoModel' module.exports = class LevelComponent extends CocoModel @className: 'LevelComponent' @schema: require 'schemas/models/level_component' + + @EquipsID: '53e217d253457600003e3ebb' urlRoot: '/db/level.component' set: (key, val, options) -> diff --git a/app/models/SuperModel.coffee b/app/models/SuperModel.coffee index e67b65436..24f9b3e04 100644 --- a/app/models/SuperModel.coffee +++ b/app/models/SuperModel.coffee @@ -170,6 +170,7 @@ module.exports = class SuperModel extends Backbone.Model @num += r.value _.defer @updateProgress r.clean() + @trigger 'resource-loaded', r onResourceFailed: (r) -> return unless @resources[r.rid] diff --git a/app/schemas/models/level.coffee b/app/schemas/models/level.coffee index 262c02d18..c3c9c59b2 100644 --- a/app/schemas/models/level.coffee +++ b/app/schemas/models/level.coffee @@ -174,7 +174,6 @@ LevelThangSchema = c.object { components: [] }, id: thang # TODO: figure out if we can make this unique and how to set dynamic defaults - # TODO: split thangType into 'original' and 'majorVersion' like the rest for consistency thangType: c.objectId(links: [{rel: 'db', href: '/db/thang.type/{($)}/version'}], title: 'Thang Type', description: 'A reference to the original Thang template being configured.', format: 'thang-type') components: c.array {title: 'Components', description: 'Thangs are configured by changing the Components attached to them.', uniqueItems: true, format: 'thang-components-array'}, ThangComponentSchema # TODO: uniqueness should be based on 'original', not whole thing @@ -239,7 +238,7 @@ _.extend LevelSchema.properties, icon: {type: 'string', format: 'image-file', title: 'Icon'} banner: {type: 'string', format: 'image-file', title: 'Banner'} goals: c.array {title: 'Goals', description: 'An array of goals which are visible to the player and can trigger scripts.'}, GoalSchema - type: c.shortString(title: 'Type', description: 'What kind of level this is.', 'enum': ['campaign', 'ladder', 'ladder-tutorial']) + type: c.shortString(title: 'Type', description: 'What kind of level this is.', 'enum': ['campaign', 'ladder', 'ladder-tutorial', 'hero']) showsGuide: c.shortString(title: 'Shows Guide', description: 'If the guide is shown at the beginning of the level.', 'enum': ['first-time', 'always']) c.extendBasicProperties LevelSchema, 'level' diff --git a/app/schemas/models/level_session.coffee b/app/schemas/models/level_session.coffee index 92ff0f5ba..284d98fc0 100644 --- a/app/schemas/models/level_session.coffee +++ b/app/schemas/models/level_session.coffee @@ -55,6 +55,10 @@ _.extend LevelSessionSchema.properties, screenshot: type: 'string' + + heroConfig: c.object {}, + inventory: c.object() + thangType: c.objectId() state: c.object {}, complete: diff --git a/server/commons/Handler.coffee b/server/commons/Handler.coffee index 55b45884f..311666c19 100644 --- a/server/commons/Handler.coffee +++ b/server/commons/Handler.coffee @@ -166,7 +166,9 @@ module.exports = class Handler ids = ids.split(',') if _.isString ids ids = _.uniq ids - project = {name:1, original:1, kind:1} + # HACK: levels loading thang types need the components returned as well + # Need a way to specify a projection for a query. + project = {name:1, original:1, kind:1, components: 1} sort = {'version.major':-1, 'version.minor':-1} makeFunc = (id) => diff --git a/test/app/lib/goal_manager.spec.coffee b/test/app/lib/GoalManager.spec.coffee similarity index 100% rename from test/app/lib/goal_manager.spec.coffee rename to test/app/lib/GoalManager.spec.coffee diff --git a/test/app/lib/LevelLoader.spec.coffee b/test/app/lib/LevelLoader.spec.coffee new file mode 100644 index 000000000..c8d83338a --- /dev/null +++ b/test/app/lib/LevelLoader.spec.coffee @@ -0,0 +1,171 @@ +Level = require 'models/Level' +LevelSession = require 'models/LevelSession' +SuperModel = require 'models/SuperModel' +LevelComponent = require 'models/LevelComponent' +LevelLoader = require 'lib/LevelLoader' + +# LEVELS + +levelWithOgreWithMace = { + type: 'hero' + thangs: [{ + thangType: 'ogre' + components: [{ + original: LevelComponent.EquipsID + majorVersion: 0 + config: { inventory: { 'left-hand': 'mace' } } + }] + }] +} + +levelWithShaman = { + type: 'hero' + thangs: [{ + thangType: 'shaman' + }] +} + +levelWithShamanWithSuperWand = { + type: 'hero' + thangs: [{ + thangType: 'shaman' + components: [{ + original: LevelComponent.EquipsID + majorVersion: 0 + config: { inventory: { 'left-hand': 'super-wand' } } + }] + }] +} + +# SESSIONS + +sessionWithTharinWithHelmet = { heroConfig: { thangType: 'tharin', inventory: { 'head': 'helmet' }}} + +# THANG TYPES + +thangTypeOgreWithPhysicalComponent = { + name: 'Ogre' + original: 'ogre' + components: [{ + original: 'physical' + majorVersion: 0 + }] +} + +thangTypeShamanWithWandEquipped = { + name: 'Shaman' + original: 'shaman' + components: [{ + original: LevelComponent.EquipsID + majorVersion: 0 + config: { inventory: { 'left-hand': 'wand' }} + }] +} + +thangTypeTharinWithHealsComponent = { + name: 'Tharin' + original: 'tharin' + components: [{ + original: 'heals' + majorVersion: 0 + }] +} + +thangTypeWand = { + name: 'Wand' + original: 'wand' + components: [{ + original: 'poisons' + majorVersion: 0 + }] +} + + + +describe 'LevelLoader', -> + it 'loads hero and item thang types from heroConfig in the LevelSession', -> + new LevelLoader({supermodel:new SuperModel(), sessionID: 'id', levelID: 'id'}) + + responses = { + '/db/level_session/id': sessionWithTharinWithHelmet + } + + jasmine.Ajax.requests.sendResponses(responses) + requests = jasmine.Ajax.requests.all() + urls = (r.url for r in requests) + expect('/db/thang.type/helmet/version?project=name,components' in urls).toBeTruthy() + expect('/db/thang.type/tharin/version?project=name,components' in urls).toBeTruthy() + + it 'loads components for the hero in the heroConfig in the LevelSession', -> + new LevelLoader({supermodel:new SuperModel(), sessionID: 'id', levelID: 'id'}) + + responses = { + '/db/level_session/id': sessionWithTharinWithHelmet + '/db/thang.type/tharin/version?project=name,components': thangTypeTharinWithHealsComponent + } + + jasmine.Ajax.requests.sendResponses(responses) + requests = jasmine.Ajax.requests.all() + urls = (r.url for r in requests) + expect('/db/level.component/heals/version/0' in urls).toBeTruthy() + + it 'loads thangs for items that the level thangs have in their Equips component configs', -> + new LevelLoader({supermodel:supermodel = new SuperModel(), sessionID: 'id', levelID: 'id'}) + + responses = { + '/db/level/id': levelWithOgreWithMace + } + + jasmine.Ajax.requests.sendResponses(responses) + requests = jasmine.Ajax.requests.all() + urls = (r.url for r in requests) + expect('/db/thang.type/mace/version?project=name,components' in urls).toBeTruthy() + + it 'loads components which are inherited by level thangs from thang type default components', -> + new LevelLoader({supermodel:new SuperModel(), sessionID: 'id', levelID: 'id'}) + + responses = + '/db/level/id': levelWithOgreWithMace + '/db/thang.type/names': [thangTypeOgreWithPhysicalComponent] + + jasmine.Ajax.requests.sendResponses(responses) + requests = jasmine.Ajax.requests.all() + urls = (r.url for r in requests) + expect('/db/level.component/physical/version/0' in urls).toBeTruthy() + + it 'loads item thang types which are inherited by level thangs from thang type default equips component configs', -> + new LevelLoader({supermodel:new SuperModel(), sessionID: 'id', levelID: 'id'}) + + responses = + '/db/level/id': levelWithShaman + '/db/thang.type/names': [thangTypeShamanWithWandEquipped] + + jasmine.Ajax.requests.sendResponses(responses) + requests = jasmine.Ajax.requests.all() + urls = (r.url for r in requests) + expect('/db/thang.type/wand/version?project=name,components' in urls).toBeTruthy() + + it 'loads components for item thang types which are inherited by level thangs from thang type default equips component configs', -> + new LevelLoader({supermodel:new SuperModel(), sessionID: 'id', levelID: 'id'}) + + responses = + '/db/level/id': levelWithShaman + '/db/thang.type/names': [thangTypeShamanWithWandEquipped] + '/db/thang.type/wand/version?project=name,components': thangTypeWand + + jasmine.Ajax.requests.sendResponses(responses) + requests = jasmine.Ajax.requests.all() + urls = (r.url for r in requests) + expect('/db/level.component/poisons/version/0' in urls).toBeTruthy() + + it 'does not load item thang types from thang type equips component configs which are overriden by level thang equips component configs', -> + new LevelLoader({supermodel:new SuperModel(), sessionID: 'id', levelID: 'id'}) + + responses = + '/db/level/id': levelWithShamanWithSuperWand + '/db/thang.type/names': [thangTypeShamanWithWandEquipped] + + jasmine.Ajax.requests.sendResponses(responses) + requests = jasmine.Ajax.requests.all() + urls = (r.url for r in requests) + expect('/db/thang.type/wand/version?project=name,components' in urls).toBeFalsy() diff --git a/test/app/require.spec.coffee b/test/app/require.spec.coffee index 33ebcf846..7af4f6b9e 100644 --- a/test/app/require.spec.coffee +++ b/test/app/require.spec.coffee @@ -1,6 +1,6 @@ describe 'require', -> it 'has no modules that error when you import them', -> - modules = window.require.list() + modules = window.require.list(); for module in modules try require(module) diff --git a/test/app/utils.coffee b/test/app/utils.coffee new file mode 100644 index 000000000..c8d2cebc0 --- /dev/null +++ b/test/app/utils.coffee @@ -0,0 +1,2 @@ +module.exports.sendTestResponses = (responseMap) -> + \ No newline at end of file