From 0b81796333102c551c634f5571ab1c5e0f05df41 Mon Sep 17 00:00:00 2001 From: Scott Erickson <sderickson@gmail.com> Date: Thu, 11 Dec 2014 11:26:28 -0800 Subject: [PATCH 01/15] Created the Campaign foundation: schema, model, handler. --- app/models/Campaign.coffee | 7 ++ app/schemas/models/campaign.schema.coffee | 105 ++++++++++++++++++++++ server/campaign/Campaign.coffee | 9 ++ server/campaign/campaign_handler.coffee | 22 +++++ 4 files changed, 143 insertions(+) create mode 100644 app/models/Campaign.coffee create mode 100644 app/schemas/models/campaign.schema.coffee create mode 100644 server/campaign/Campaign.coffee create mode 100644 server/campaign/campaign_handler.coffee diff --git a/app/models/Campaign.coffee b/app/models/Campaign.coffee new file mode 100644 index 000000000..f74158c79 --- /dev/null +++ b/app/models/Campaign.coffee @@ -0,0 +1,7 @@ +CocoModel = require './CocoModel' + +module.exports = class Campaign extends CocoModel + @className: 'Campaign' + @schema: require 'schemas/models/campaign.schema' + urlRoot: '/db/campaign' + saveBackups: true diff --git a/app/schemas/models/campaign.schema.coffee b/app/schemas/models/campaign.schema.coffee new file mode 100644 index 000000000..dfa3cfe83 --- /dev/null +++ b/app/schemas/models/campaign.schema.coffee @@ -0,0 +1,105 @@ + +c = require './../schemas' + +CampaignSchema = c.object() +c.extendNamedProperties CampaignSchema # name first + +_.extend CampaignSchema.properties, + i18n: {type: 'object', title: 'i18n', format: 'i18n', props: ['name', 'body']} + + ambientSound: c.object {}, + mp3: { type: 'string', format: 'sound-file' } + ogg: { type: 'string', format: 'sound-file' } + + backgroundImage: c.array {}, { + image: { type: 'string', format: 'image-file' } + width: { type: 'number' } + } + backgroundColor: { type: 'string' } + backgroundColorTransparent: { type: 'string' } + + adjacentCampaigns: { type: 'object', additionalItems: { + title: 'Campaign' + type: 'object' + properties: { + #- denormalized from other Campaigns, either updated automatically or fetched dynamically + name: { type: 'string' } + i18n: { type: 'object' } + + #- normal properties + position: c.point2d() + rotation: { type: 'number', format: 'degrees' } + color: { type: 'string' } + } + }} + + levels: { type: 'object', additionalItems: { + title: 'Level' + type: 'object' + properties: { + #- denormalized from Level + # TODO: take these properties from the Level schema and put them into schema references, use them here + name: { readOnly: true } + description: { readOnly: true } + requiresSubscription: { type: 'boolean' } + type: {'enum': ['campaign', 'ladder', 'ladder-tutorial', 'hero', 'hero-ladder', 'hero-coop']} + slug: { readOnly: true } + original: { readOnly: true } + adventurer: { type: 'boolean' } + practice: { type: 'boolean' } + + # TODO: add these to the level, as well as the 'campaign' property + disableSpaces: { type: 'boolean' } + hidesSubmitUntilRun: { type: 'boolean' } + hidesPlayButton: { type: 'boolean' } + hidesRunShortcut: { type: 'boolean' } + hidesHUD: { type: 'boolean' } + hidesSay: { type: 'boolean' } + hidesCodeToolbar: { type: 'boolean' } + hidesRealTimePlayback: { type: 'boolean' } + backspaceThrottle: { type: 'boolean' } + lockDefaultCode: { type: 'boolean' } + moveRightLoopSnippet: { type: 'boolean' } + + realTimeSpeedFactor: { type: 'number' } + autocompleteFontSizePx: { type: 'number' } + + requiredCode: c.array {}, { + type: 'string' + } + suspectCode: c.array {}, { + type: 'object' + properties: { + name: { type: 'string' } + pattern: { type: 'string' } + } + } + + requiredGear: { type: 'object', additionalProperties: { + type: 'string' # should be an originalID, denormalized on the editor side + }} + restrictedGear: { type: 'object', additionalProperties: { + type: 'string' # should be an originalID, denormalized on the editor side + }} + allowedHeroes: { type: 'array', items: { + type: 'string' # should be an originalID, denormalized on the editor side + }} + + #- denormalized/restructured from Achievements + nextLevels: c.array {} + unlocksHero: c.object { readOnly: true }, { + image: { type: 'string', format: 'image-file' } + original: { type: 'string' } + } + + #- normal properties + position: c.point2d() + + } + } +} + +c.extendBasicProperties CampaignSchema, 'campaign' +c.extendTranslationCoverageProperties CampaignSchema + +module.exports = CampaignSchema diff --git a/server/campaign/Campaign.coffee b/server/campaign/Campaign.coffee new file mode 100644 index 000000000..b0fad1371 --- /dev/null +++ b/server/campaign/Campaign.coffee @@ -0,0 +1,9 @@ +mongoose = require 'mongoose' +plugins = require '../plugins/plugins' + +CampaignSchema = new mongoose.Schema(body: String, {strict: false}) + +CampaignSchema.plugin(plugins.NamedPlugin) +CampaignSchema.plugin(plugins.TranslationCoveragePlugin) + +module.exports = mongoose.model('campaign', CampaignSchema) diff --git a/server/campaign/campaign_handler.coffee b/server/campaign/campaign_handler.coffee new file mode 100644 index 000000000..60540a3a1 --- /dev/null +++ b/server/campaign/campaign_handler.coffee @@ -0,0 +1,22 @@ +Campaign = require './Campaign' +Handler = require '../commons/Handler' + +CampaignHandler = class CampaignHandler extends Handler + modelClass: Campaign + editableProperties: [ + 'name' + 'i18n' + 'i18nCoverage' + 'ambientSound' + 'backgroundImage' + 'backgroundColor' + 'backgroundColorTransparent' + 'adjacentCampaigns' + 'levels' + ] + jsonSchema: require '../../app/schemas/models/campaign.schema' + + hasAccess: (req) -> + req.method is 'GET' or req.user?.isAdmin() + +module.exports = new CampaignHandler() From 1cc6a97e436ee2e4eba06898c0119ffe3f7fb599 Mon Sep 17 00:00:00 2001 From: Scott Erickson <sderickson@gmail.com> Date: Tue, 16 Dec 2014 17:46:24 -0800 Subject: [PATCH 02/15] Added basic campaign view, working on campaign handlers. --- app/core/Router.coffee | 1 + app/models/Campaign.coffee | 4 +- app/schemas/models/campaign.schema.coffee | 44 +- app/schemas/models/level.coffee | 34 + .../editor/campaign/campaign-editor-view.sass | 15 + .../editor/campaign/campaign-editor-view.jade | 44 ++ app/templates/play/world-map-view.jade | 2 +- .../editor/campaign/CampaignEditorView.coffee | 691 ++++++++++++++++++ app/views/play/WorldMapView.coffee | 16 +- server/campaign/campaign_handler.coffee | 22 - .../{campaign => campaigns}/Campaign.coffee | 0 server/campaigns/campaign_handler.coffee | 70 ++ server/commons/mapping.coffee | 1 + server/levels/level_handler.coffee | 22 + test/server/common.coffee | 1 + .../functional/campaign_handler.spec.coffee | 77 ++ 16 files changed, 999 insertions(+), 45 deletions(-) create mode 100644 app/styles/editor/campaign/campaign-editor-view.sass create mode 100644 app/templates/editor/campaign/campaign-editor-view.jade create mode 100644 app/views/editor/campaign/CampaignEditorView.coffee delete mode 100644 server/campaign/campaign_handler.coffee rename server/{campaign => campaigns}/Campaign.coffee (100%) create mode 100644 server/campaigns/campaign_handler.coffee create mode 100644 test/server/functional/campaign_handler.spec.coffee diff --git a/app/core/Router.coffee b/app/core/Router.coffee index 13caacf6c..daad3d754 100644 --- a/app/core/Router.coffee +++ b/app/core/Router.coffee @@ -66,6 +66,7 @@ module.exports = class CocoRouter extends Backbone.Router 'editor/level/:levelID': go('editor/level/LevelEditView') 'editor/thang': go('editor/thang/ThangTypeSearchView') 'editor/thang/:thangID': go('editor/thang/ThangTypeEditView') + 'editor/campaign/:campaignID': go('editor/campaign/CampaignEditorView') 'employers': go('EmployersView') diff --git a/app/models/Campaign.coffee b/app/models/Campaign.coffee index f74158c79..7473d2eca 100644 --- a/app/models/Campaign.coffee +++ b/app/models/Campaign.coffee @@ -1,7 +1,9 @@ CocoModel = require './CocoModel' +schema = require 'schemas/models/campaign.schema' module.exports = class Campaign extends CocoModel @className: 'Campaign' - @schema: require 'schemas/models/campaign.schema' + @schema: schema urlRoot: '/db/campaign' saveBackups: true + @denormalizedLevelProperties: _.keys(_.omit(schema.properties.levels.additionalProperties.properties, ['unlocks', 'position'])) diff --git a/app/schemas/models/campaign.schema.coffee b/app/schemas/models/campaign.schema.coffee index dfa3cfe83..606267155 100644 --- a/app/schemas/models/campaign.schema.coffee +++ b/app/schemas/models/campaign.schema.coffee @@ -1,10 +1,9 @@ - c = require './../schemas' CampaignSchema = c.object() c.extendNamedProperties CampaignSchema # name first -_.extend CampaignSchema.properties, +_.extend CampaignSchema.properties, { i18n: {type: 'object', title: 'i18n', format: 'i18n', props: ['name', 'body']} ambientSound: c.object {}, @@ -12,8 +11,12 @@ _.extend CampaignSchema.properties, ogg: { type: 'string', format: 'sound-file' } backgroundImage: c.array {}, { - image: { type: 'string', format: 'image-file' } - width: { type: 'number' } + type: 'object' + additionalProperties: false + properties: { + image: { type: 'string', format: 'image-file' } + width: { type: 'number' } + } } backgroundColor: { type: 'string' } backgroundColorTransparent: { type: 'string' } @@ -33,18 +36,22 @@ _.extend CampaignSchema.properties, } }} - levels: { type: 'object', additionalItems: { + levels: { type: 'object', format: 'levels', additionalProperties: { title: 'Level' type: 'object' + format: 'level' + additionalProperties: false + + # key is the original property properties: { #- denormalized from Level # TODO: take these properties from the Level schema and put them into schema references, use them here - name: { readOnly: true } - description: { readOnly: true } + name: { type: 'string', format: 'hidden' } + description: { type: 'string', format: 'hidden' } requiresSubscription: { type: 'boolean' } type: {'enum': ['campaign', 'ladder', 'ladder-tutorial', 'hero', 'hero-ladder', 'hero-coop']} - slug: { readOnly: true } - original: { readOnly: true } + slug: { type: 'string', format: 'hidden' } + original: { type: 'string', format: 'hidden' } adventurer: { type: 'boolean' } practice: { type: 'boolean' } @@ -85,20 +92,23 @@ _.extend CampaignSchema.properties, type: 'string' # should be an originalID, denormalized on the editor side }} - #- denormalized/restructured from Achievements - nextLevels: c.array {} - unlocksHero: c.object { readOnly: true }, { - image: { type: 'string', format: 'image-file' } - original: { type: 'string' } - } + #- denormalized from Achievements + unlocks: { type: 'array', items: { + type: 'object' + properties: + original: { type: 'string' } + type: { enum: ['hero', 'item', 'level'] } + achievement: { type: 'string' } + }} #- normal properties position: c.point2d() - } - } + + }} } + c.extendBasicProperties CampaignSchema, 'campaign' c.extendTranslationCoverageProperties CampaignSchema diff --git a/app/schemas/models/level.coffee b/app/schemas/models/level.coffee index 94613e82c..40cb06c52 100644 --- a/app/schemas/models/level.coffee +++ b/app/schemas/models/level.coffee @@ -253,6 +253,40 @@ _.extend LevelSchema.properties, showsGuide: c.shortString(title: 'Shows Guide', description: 'If the guide is shown at the beginning of the level.', 'enum': ['first-time', 'always']) requiresSubscription: {title: 'Requires Subscription', description: 'Whether this level is available to subscribers only.', type: 'boolean'} + # Admin flags + disableSpaces: { type: 'boolean' } + hidesSubmitUntilRun: { type: 'boolean' } + hidesPlayButton: { type: 'boolean' } + hidesRunShortcut: { type: 'boolean' } + hidesHUD: { type: 'boolean' } + hidesSay: { type: 'boolean' } + hidesCodeToolbar: { type: 'boolean' } + hidesRealTimePlayback: { type: 'boolean' } + backspaceThrottle: { type: 'boolean' } + lockDefaultCode: { type: 'boolean' } + moveRightLoopSnippet: { type: 'boolean' } + realTimeSpeedFactor: { type: 'number' } + autocompleteFontSizePx: { type: 'number' } + requiredCode: c.array {}, { + type: 'string' + } + suspectCode: c.array {}, { + type: 'object' + properties: { + name: { type: 'string' } + pattern: { type: 'string' } + } + } + requiredGear: { type: 'object', additionalProperties: { + type: 'string' + }} + restrictedGear: { type: 'object', additionalProperties: { + type: 'string' + }} + allowedHeroes: { type: 'array', items: { + type: 'string' + }} + c.extendBasicProperties LevelSchema, 'level' c.extendSearchableProperties LevelSchema c.extendVersionedProperties LevelSchema, 'level' diff --git a/app/styles/editor/campaign/campaign-editor-view.sass b/app/styles/editor/campaign/campaign-editor-view.sass new file mode 100644 index 000000000..60e5a1a54 --- /dev/null +++ b/app/styles/editor/campaign/campaign-editor-view.sass @@ -0,0 +1,15 @@ +#campaign-editor-view + #left-column + position: absolute + top: 0 + bottom: 0 + left: 0 + right: 0 + margin-right: 1200px + + #right-column + position: absolute + top: 0 + bottom: 0 + right: 0 + width: 1200px diff --git a/app/templates/editor/campaign/campaign-editor-view.jade b/app/templates/editor/campaign/campaign-editor-view.jade new file mode 100644 index 000000000..eeab8c239 --- /dev/null +++ b/app/templates/editor/campaign/campaign-editor-view.jade @@ -0,0 +1,44 @@ +extends /templates/base + +block header + if campaign.loading + nav.navbar.navbar-default(role='navigation')#campaign-editor-top-nav + .container-fluid + ul.nav.navbar-nav + li + a(href="/") + span.glyphicon-home.glyphicon + else + nav.navbar.navbar-default(role='navigation')#campaign-editor-top-nav + ul.nav.navbar-nav + li + a(href="/") + span.glyphicon-home.glyphicon + + ul.nav.navbar-nav.navbar-right + if authorized + li#save-button + a + span.glyphicon-floppy-disk.glyphicon + li.dropdown + a(data-toggle='dropdown') + span.glyphicon-chevron-down.glyphicon + ul.dropdown-menu + li.dropdown-header Actions + li(class=anonymous ? "disabled": "") + a(data-toggle="coco-modal", data-target="modal/RevertModal", data-i18n="editor.revert")#revert-button Revert + li(class=anonymous ? "disabled": "") + a(data-i18n="editor.pop_i18n")#pop-level-i18n-button Populate i18n + li.divider + li.dropdown-header Info + +block outer_content + .outer-content + #left-column + #campaign-treema + + #right-column + #world-map-view + #campaign-level-view + +block footer diff --git a/app/templates/play/world-map-view.jade b/app/templates/play/world-map-view.jade index 57599a8af..68c601450 100644 --- a/app/templates/play/world-map-view.jade +++ b/app/templates/play/world-map-view.jade @@ -8,7 +8,7 @@ - var seenNext = nextLevel; each level in campaign.levels if !level.hidden - - var next = level.id == nextLevel || (!seenNext && levelStatusMap[level.id] != "complete" && !level.locked && !level.disabled); + - var next = level.id == nextLevel || (!seenNext && levelStatusMap[level.id] != "complete" && !level.locked && !level.disabled && !editorMode); - seenNext = seenNext || next; div(style="left: #{level.x}%; bottom: #{level.y}%; background-color: #{level.color}", class="level" + (next ? " next" : "") + (level.disabled ? " disabled" : "") + (level.locked ? " locked" : "") + " " + levelStatusMap[level.id] || "", data-level-id=level.id, title=level.name + (level.disabled ? ' (Coming Soon to Adventurers)' : '')) if level.unlocksHero && !level.unlockedHero diff --git a/app/views/editor/campaign/CampaignEditorView.coffee b/app/views/editor/campaign/CampaignEditorView.coffee new file mode 100644 index 000000000..94d5d7bef --- /dev/null +++ b/app/views/editor/campaign/CampaignEditorView.coffee @@ -0,0 +1,691 @@ +RootView = require 'views/core/RootView' +Campaign = require 'models/Campaign' +Level = require 'models/Level' +WorldMapView = require 'views/play/WorldMapView' +CocoCollection = require 'collections/CocoCollection' + +module.exports = class CampaignEditorView extends RootView + id: "campaign-editor-view" + template: require 'templates/editor/campaign/campaign-editor-view' + className: 'editor' + + constructor: -> + super(arguments...) + + # TODO: move the outputted data to the db, and load the Campaign objects instead + for level in levels + _.extend level, options[level.id] + level.slug = level.id + delete level.id + delete level.nextLevels + level.position = { x: level.x, y: level.y } + delete level.x + delete level.y + if level.unlocksHero + level.unlocks = [{ + original: level.unlocksHero.originalID + type: 'hero' + }] + delete level.unlocksHero + campaign.levels[level.original] = level + @campaign = new Campaign(campaign) + #------------------------------------------------ + + collection = new CocoCollection({model: Level, url}) + collection.ur + + getRenderData: -> + c = super() + c.campaign = @campaign + c + + afterRender: -> + super() + treemaOptions = + schema: Campaign.schema + data: $.extend({}, @campaign.attributes) + callbacks: + change: @onTreemaChanged + select: @onTreemaSelectionChanged + dblclick: @onTreemaDoubleClicked + nodeClasses: + levels: LevelsNode + level: LevelNode + + + @treema = @$el.find('#campaign-treema').treema treemaOptions + @treema.build() + @treema.open() + @treema.childrenTreemas.levels.open() + + worldMapView = new WorldMapView({supermodel: @supermodel, editorMode: true}, 'dungeon') + worldMapView.highlightElement = _.noop # make it stop + @insertSubView worldMapView + + +class LevelsNode extends TreemaObjectNode + valueClass: 'treema-levels' + @levels: {} + + buildValueForDisplay: (valEl, data) -> + @buildValueForDisplaySimply valEl, ''+_.size(data) + + childPropertiesAvailable: -> @childSource + + childSource: (req, res) => + console.log 'calling child source!', req + s = new Backbone.Collection([], {model:Level}) + s.url = '/db/level' + s.fetch({data: {term:req.term, project: Campaign.denormalizedLevelProperties.join(',')}}) + s.once 'sync', (collection) -> + LevelsNode.levels[level.get('original')] = level for level in collection.models + console.log 'results!', collection.models + mapped = ({label: r.get('name'), value: r.get('original')} for r in collection.models) + console.log 'mapped', mapped + res(mapped) + + +class LevelNode extends TreemaObjectNode + valueClass: 'treema-level' + buildValueForDisplay: (valEl, data) -> + @buildValueForDisplaySimply valEl, data.name + + populateData: -> + return if @data.name? + console.log 'how do I do this?', @data, @keyForParent, LevelsNode.levels + data = _.pick LevelsNode.levels[@keyForParent].attributes, Campaign.denormalizedLevelProperties + _.extend @data, data + console.log 'extended to data', data + console.log 'now data is', @data + +campaign = { + name: 'Dungeon' + levels: {} +} + + +levels = [ + { + name: 'Dungeons of Kithgard' + type: 'hero' + id: 'dungeons-of-kithgard' + original: '5411cb3769152f1707be029c' + description: 'Grab the gem, but touch nothing else. Start here.' + x: 14 + y: 15.5 + nextLevels: + continue: 'gems-in-the-deep' + } + { + name: 'Gems in the Deep' + type: 'hero' + id: 'gems-in-the-deep' + original: '54173c90844506ae0195a0b4' + description: 'Quickly collect the gems; you will need them.' + x: 29 + y: 12 + nextLevels: + continue: 'shadow-guard' + } + { + name: 'Shadow Guard' + type: 'hero' + id: 'shadow-guard' + original: '54174347844506ae0195a0b8' + description: 'Evade the Kithgard minion.' + x: 40.54 + y: 11.03 + nextLevels: + continue: 'forgetful-gemsmith' + } + { + name: 'Kounter Kithwise' + type: 'hero' + id: 'kounter-kithwise' + original: '54527a6257e83800009730c7' + description: 'Practice your evasion skills with more guards.' + x: 35.37 + y: 20.61 + nextLevels: + continue: 'crawlways-of-kithgard' + practice: true + requiresSubscription: true + } + { + name: 'Crawlways of Kithgard' + type: 'hero' + id: 'crawlways-of-kithgard' + original: '545287ef57e83800009730d5' + description: 'Dart in and grab the gem–at the right moment.' + x: 36.48 + y: 29.03 + nextLevels: + continue: 'forgetful-gemsmith' + practice: true + requiresSubscription: true + } + { + name: 'Forgetful Gemsmith' + type: 'hero' + id: 'forgetful-gemsmith' + original: '544a98f62d002f0000fe331a' + description: 'Grab even more gems as you practice moving.' + x: 54.98 + y: 10.53 + nextLevels: + continue: 'true-names' + } + { + name: 'True Names' + type: 'hero' + id: 'true-names' + original: '541875da4c16460000ab990f' + description: 'Learn an enemy\'s true name to defeat it.' + x: 68.44 + y: 10.70 + nextLevels: + continue: 'the-raised-sword' + unlocksHero: { + img: '/file/db/thang.type/53e12be0d042f23505c3023b/portrait.png' + originalID: '53e12be0d042f23505c3023b' + } + } + { + name: 'Favorable Odds' + type: 'hero' + id: 'favorable-odds' + original: '5452972f57e83800009730de' + description: 'Test out your battle skills by defeating more munchkins.' + x: 88.25 + y: 14.92 + nextLevels: + continue: 'the-raised-sword' + practice: true + requiresSubscription: true + } + { + name: 'The Raised Sword' + type: 'hero' + id: 'the-raised-sword' + original: '5418aec24c16460000ab9aa6' + description: 'Learn to equip yourself for combat.' + x: 81.51 + y: 17.92 + nextLevels: + continue: 'haunted-kithmaze' + } + { + name: 'Haunted Kithmaze' + type: 'hero' + id: 'haunted-kithmaze' + original: '545a5914d820eb0000f6dc0a' + description: 'The builders of Kithgard constructed many mazes to confuse travelers.' + x: 78 + y: 29 + nextLevels: + continue: 'the-second-kithmaze' + } + { + name: 'Riddling Kithmaze' + type: 'hero' + id: 'riddling-kithmaze' + original: '5418b9d64c16460000ab9ab4' + description: 'If at first you go astray, change your loop to find the way.' + x: 69.97 + y: 28.03 + nextLevels: + continue: 'descending-further' + practice: true + requiresSubscription: true + } + { + name: 'Descending Further' + type: 'hero' + id: 'descending-further' + original: '5452a84d57e83800009730e4' + description: 'Another day, another maze.' + x: 61.68 + y: 22.80 + nextLevels: + continue: 'the-second-kithmaze' + practice: true + requiresSubscription: true + } + { + name: 'The Second Kithmaze' + type: 'hero' + id: 'the-second-kithmaze' + original: '5418cf256bae62f707c7e1c3' + description: 'Many have tried, few have found their way through this maze.' + x: 54.49 + y: 26.49 + nextLevels: + continue: 'dread-door' + } + { + name: 'Dread Door' + type: 'hero' + id: 'dread-door' + original: '5418d40f4c16460000ab9ac2' + description: 'Behind a dread door lies a chest full of riches.' + x: 60.52 + y: 33.70 + nextLevels: + continue: 'known-enemy' + } + { + name: 'Known Enemy' + type: 'hero' + id: 'known-enemy' + original: '5452adea57e83800009730ee' + description: 'Begin to use variables in your battles.' + x: 67 + y: 39 + nextLevels: + continue: 'master-of-names' + } + { + name: 'Master of Names' + type: 'hero' + id: 'master-of-names' + original: '5452c3ce57e83800009730f7' + description: 'Use your glasses to defend yourself from the Kithmen.' + x: 75 + y: 46 + nextLevels: + continue: 'lowly-kithmen' + } + { + name: 'Lowly Kithmen' + type: 'hero' + id: 'lowly-kithmen' + original: '541b24511ccc8eaae19f3c1f' + description: 'Now that you can see them, they\'re everywhere!' + x: 85 + y: 40 + nextLevels: + continue: 'closing-the-distance' + } + { + name: 'Closing the Distance' + type: 'hero' + id: 'closing-the-distance' + original: '541b288e1ccc8eaae19f3c25' + description: 'Kithmen are not the only ones to stand in your way.' + x: 93 + y: 47 + nextLevels: + continue: 'the-final-kithmaze' + } + { + name: 'Tactical Strike' + type: 'hero' + id: 'tactical-strike' + original: '5452cfa706a59e000067e4f5' + description: 'They\'re, uh, coming right for us! Sneak up behind them.' + x: 83.23 + y: 52.73 + nextLevels: + continue: 'the-final-kithmaze' + practice: true + requiresSubscription: true + } + { + name: 'The Final Kithmaze' + type: 'hero' + id: 'the-final-kithmaze' + original: '541b434e1ccc8eaae19f3c33' + description: 'To escape you must find your way through an Elder Kithman\'s maze.' + x: 86.95 + y: 64.70 + nextLevels: + continue: 'kithgard-gates' + } + { + name: 'The Gauntlet' + type: 'hero' + id: 'the-gauntlet' + original: '5452d8b906a59e000067e4fa' + description: 'Rush for the stairs, battling foes at every turn.' + x: 76.50 + y: 72.69 + nextLevels: + continue: 'kithgard-gates' + practice: true + requiresSubscription: true + } + { + name: 'Kithgard Gates' + type: 'hero' + id: 'kithgard-gates' + original: '541c9a30c6362edfb0f34479' + description: 'Escape the Kithgard dungeons and don\'t let the guardians get you.' + x: 89 + y: 82 + nextLevels: + continue: 'defense-of-plainswood' + } + { + name: 'Cavern Survival' + type: 'hero-ladder' + id: 'cavern-survival' + original: '544437e0645c0c0000c3291d' + description: 'Stay alive longer than your opponent amidst hordes of ogres!' + x: 17.54 + y: 78.39 + } +] + +options = + 'dungeons-of-kithgard': + disableSpaces: true + hidesSubmitUntilRun: true + hidesPlayButton: true + hidesRunShortcut: true + hidesHUD: true + hidesSay: true + hidesCodeToolbar: true + hidesRealTimePlayback: true + requiredGear: {feet: 'simple-boots'} + restrictedGear: {feet: 'leather-boots'} + requiredCode: ['moveRight'] + 'gems-in-the-deep': + disableSpaces: true + hidesSubmitUntilRun: true + hidesPlayButton: true + hidesRunShortcut: true + hidesHUD: true + hidesSay: true + hidesCodeToolbar: true + hidesRealTimePlayback: true + requiredGear: {feet: 'simple-boots'} + restrictedGear: {feet: 'leather-boots'} + 'shadow-guard': + disableSpaces: true + hidesSubmitUntilRun: true + hidesPlayButton: true + hidesRunShortcut: true + hidesHUD: true + hidesSay: true + hidesCodeToolbar: true + hidesRealTimePlayback: true + requiredGear: {feet: 'simple-boots'} + restrictedGear: {feet: 'leather-boots', 'right-hand': 'simple-sword'} + 'kounter-kithwise': + disableSpaces: true + hidesPlayButton: true + hidesRunShortcut: true + hidesHUD: true + hidesSay: true + hidesCodeToolbar: true + hidesRealTimePlayback: true + requiredGear: {feet: 'simple-boots'} + restrictedGear: {feet: 'leather-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i'} + 'crawlways-of-kithgard': + hidesPlayButton: true + hidesRunShortcut: true + hidesHUD: true + hidesSay: true + hidesCodeToolbar: true + hidesRealTimePlayback: true + requiredGear: {feet: 'simple-boots'} + restrictedGear: {feet: 'leather-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i'} + 'forgetful-gemsmith': + disableSpaces: true + hidesPlayButton: true + hidesRunShortcut: true + hidesHUD: true + hidesSay: true + hidesCodeToolbar: true + hidesRealTimePlayback: true + requiredGear: {feet: 'simple-boots'} + restrictedGear: {feet: 'leather-boots', 'programming-book': 'programmaticon-i'} + 'true-names': + disableSpaces: true + hidesPlayButton: true + hidesRunShortcut: true + hidesHUD: true + hidesSay: true + hidesCodeToolbar: true + hidesRealTimePlayback: true + requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', waist: 'leather-belt'} + restrictedGear: {feet: 'leather-boots', 'programming-book': 'programmaticon-i'} + requiredCode: ['Brak'] + 'favorable-odds': + disableSpaces: true + hidesPlayButton: true + hidesRunShortcut: true + hidesHUD: true + hidesSay: true + hidesCodeToolbar: true + hidesRealTimePlayback: true + requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword'} + restrictedGear: {feet: 'leather-boots', 'programming-book': 'programmaticon-i'} + 'the-raised-sword': + disableSpaces: true + hidesPlayButton: true + hidesRunShortcut: true + hidesHUD: true + hidesSay: true + hidesCodeToolbar: true + hidesRealTimePlayback: true + requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'tarnished-bronze-breastplate'} + restrictedGear: {feet: 'leather-boots', 'programming-book': 'programmaticon-i'} + 'the-first-kithmaze': + hidesRunShortcut: true + hidesHUD: true + hidesSay: true + hidesCodeToolbar: true + hidesRealTimePlayback: true + requiredGear: {feet: 'simple-boots', 'programming-book': 'programmaticon-i'} + restrictedGear: {feet: 'leather-boots'} + requiredCode: ['loop'] + 'haunted-kithmaze': + hidesRunShortcut: true + hidesHUD: true + hidesSay: true + hidesCodeToolbar: true + hidesRealTimePlayback: true + moveRightLoopSnippet: true + requiredGear: {feet: 'simple-boots', 'programming-book': 'programmaticon-i'} + restrictedGear: {feet: 'leather-boots'} + requiredCode: ['loop'] + 'descending-further': + hidesHUD: true + hidesSay: true + hidesCodeToolbar: true + hidesRealTimePlayback: true + requiredGear: {feet: 'simple-boots', 'programming-book': 'programmaticon-i'} + restrictedGear: {feet: 'leather-boots'} + 'the-second-kithmaze': + hidesHUD: true + hidesSay: true + hidesCodeToolbar: true + hidesRealTimePlayback: true + moveRightLoopSnippet: true + requiredGear: {feet: 'simple-boots', 'programming-book': 'programmaticon-i'} + restrictedGear: {feet: 'leather-boots'} + 'dread-door': + hidesHUD: true + hidesSay: true + hidesCodeToolbar: true + hidesRealTimePlayback: true + requiredGear: {'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i'} + restrictedGear: {feet: 'leather-boots'} + 'known-enemy': + hidesHUD: true + hidesSay: true + hidesCodeToolbar: true + hidesRealTimePlayback: true + requiredGear: {'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i', torso: 'tarnished-bronze-breastplate'} + restrictedGear: {feet: 'leather-boots'} + suspectCode: [{name: 'enemy-in-quotes', pattern: '[\'"]enemy'}] # ' + 'master-of-names': + hidesHUD: true + hidesSay: true + hidesCodeToolbar: true + hidesRealTimePlayback: true + requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses', torso: 'tarnished-bronze-breastplate'} + restrictedGear: {feet: 'leather-boots'} + requiredCode: ['findNearestEnemy'] + suspectCode: [{name: 'lone-find-nearest-enemy', pattern: '^[ ]*(self|this|@)?[:.]?findNearestEnemy()'}] + 'lowly-kithmen': + hidesHUD: true + hidesSay: true + hidesCodeToolbar: true + hidesRealTimePlayback: true + requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses', torso: 'tarnished-bronze-breastplate'} + restrictedGear: {feet: 'leather-boots'} + requiredCode: ['findNearestEnemy'] + suspectCode: [{name: 'lone-find-nearest-enemy', pattern: '^[ ]*(self|this|@)?[:.]?findNearestEnemy()'}] + 'closing-the-distance': + hidesHUD: true + hidesSay: true + hidesCodeToolbar: true + hidesRealTimePlayback: true + requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'tarnished-bronze-breastplate', eyes: 'crude-glasses'} + restrictedGear: {feet: 'leather-boots'} + suspectCode: [{name: 'lone-find-nearest-enemy', pattern: '^[ ]*(self|this|@)?[:.]?findNearestEnemy()'}] + 'tactical-strike': + hidesHUD: true + hidesSay: true + hidesCodeToolbar: true + hidesRealTimePlayback: true + requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'tarnished-bronze-breastplate', eyes: 'crude-glasses'} + restrictedGear: {feet: 'leather-boots'} + suspectCode: [{name: 'lone-find-nearest-enemy', pattern: '^[ ]*(self|this|@)?[:.]?findNearestEnemy()'}] + 'the-final-kithmaze': + hidesHUD: true + hidesSay: true + hidesCodeToolbar: true + hidesRealTimePlayback: true + requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'tarnished-bronze-breastplate', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'} + suspectCode: [{name: 'lone-find-nearest-enemy', pattern: '^[ ]*(self|this|@)?[:.]?findNearestEnemy()'}] + 'the-gauntlet': + hidesHUD: true + hidesSay: true + hidesCodeToolbar: true + hidesRealTimePlayback: true + requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'tarnished-bronze-breastplate', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'} + restrictedGear: {feet: 'leather-boots', 'right-hand': 'crude-builders-hammer'} + suspectCode: [{name: 'lone-find-nearest-enemy', pattern: '^[ ]*(self|this|@)?[:.]?findNearestEnemy()'}] + 'kithgard-gates': + hidesSay: true + hidesCodeToolbar: true + hidesRealTimePlayback: true + requiredGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', torso: 'tarnished-bronze-breastplate'} + restrictedGear: {'right-hand': 'simple-sword'} + 'defense-of-plainswood': + hidesRealTimePlayback: true + hidesCodeToolbar: true + requiredGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer'} + restrictedGear: {'right-hand': 'simple-sword'} + 'winding-trail': + hidesRealTimePlayback: true + hidesCodeToolbar: true + requiredGear: {feet: 'leather-boots', 'right-hand': 'crude-builders-hammer'} + restrictedGear: {feet: 'simple-boots', 'right-hand': 'simple-sword'} + 'patrol-buster': + hidesRealTimePlayback: true + hidesCodeToolbar: true + requiredGear: {feet: 'leather-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses'} + restrictedGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-i'} + 'endangered-burl': + hidesRealTimePlayback: true + hidesCodeToolbar: true + requiredGear: {feet: 'leather-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses'} + restrictedGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-i'} + 'village-guard': + hidesCodeToolbar: true + lockDefaultCode: true + requiredGear: {feet: 'leather-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses'} + restrictedGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-i'} + 'thornbush-farm': + hidesCodeToolbar: true + lockDefaultCode: true + requiredGear: {feet: 'leather-boots', 'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses'} + restrictedGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i'} + requiredCode: ['topEnemy'] + 'back-to-back': + hidesCodeToolbar: true + requiredGear: {feet: 'leather-boots', torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses', 'right-hand': 'simple-sword', 'left-hand': 'wooden-shield'} + restrictedGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-i'} + 'ogre-encampment': + requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses', 'right-hand': 'simple-sword', 'left-hand': 'wooden-shield'} + restrictedGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-i'} + 'woodland-cleaver': + requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses', 'right-hand': 'long-sword', 'left-hand': 'wooden-shield', wrists: 'sundial-wristwatch', feet: 'leather-boots'} + restrictedGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i'} + 'shield-rush': + requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'} + restrictedGear: {'left-hand': 'wooden-shield', 'programming-book': 'programmaticon-i'} + +# Warrior branch + 'peasant-protection': + requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'} + restrictedGear: {eyes: 'crude-glasses', 'programming-book': 'programmaticon-i'} + 'munchkin-swarm': + requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'} + restrictedGear: {'programming-book': 'programmaticon-i', eyes: 'crude-glasses'} + +# Ranger branch + 'munchkin-harvest': + requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'} + restrictedGear: {'programming-book': 'programmaticon-i'} + allowedHeroes: ['captain', 'knight', 'samurai'] + 'swift-dagger': + requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'crude-crossbow', 'left-hand': 'crude-dagger', wrists: 'sundial-wristwatch'} + restrictedGear: {eyes: 'crude-glasses', 'programming-book': 'programmaticon-i'} + allowedHeroes: ['ninja', 'trapper', 'forest-archer'] + 'shrapnel': + requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'crude-crossbow', 'left-hand': 'weak-charge', wrists: 'sundial-wristwatch'} + restrictedGear: {eyes: 'crude-glasses', 'left-hand': 'crude-dagger', 'programming-book': 'programmaticon-i'} + allowedHeroes: ['ninja', 'trapper', 'forest-archer'] + +# Wizard branch + 'arcane-ally': + requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'} + restrictedGear: {eyes: 'crude-glasses', 'programming-book': 'programmaticon-i'} + allowedHeroes: ['captain', 'knight', 'samurai'] + 'touch-of-death': + requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'enchanted-stick', 'left-hand': 'unholy-tome-i', wrists: 'sundial-wristwatch'} + restrictedGear: {'programming-book': 'programmaticon-i', eyes: 'crude-glasses'} + allowedHeroes: ['librarian', 'potion-master', 'sorcerer'] + 'bonemender': + requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'enchanted-stick', 'left-hand': 'book-of-life-i', wrists: 'sundial-wristwatch'} + restrictedGear: {'left-hand': 'unholy-tome-i', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'} + requiredCode: ['canCast'] + allowedHeroes: ['librarian', 'potion-master', 'sorcerer'] + + 'coinucopia': + requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags'} + restrictedGear: {'programming-book': 'programmaticon-i', eyes: 'crude-glasses'} + 'copper-meadows': + requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags', eyes: 'wooden-glasses'} + restrictedGear: {'programming-book': 'programmaticon-i', eyes: 'crude-glasses'} + 'drop-the-flag': + requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags', eyes: 'wooden-glasses', 'right-hand': 'crude-builders-hammer'} + restrictedGear: {'right-hand': 'long-sword', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'} + 'deadly-pursuit': + requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags', eyes: 'wooden-glasses', 'right-hand': 'crude-builders-hammer'} + restrictedGear: {'right-hand': 'long-sword', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'} + 'rich-forager': + requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags', eyes: 'wooden-glasses', torso: 'tarnished-bronze-breastplate', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield'} + restrictedGear: {'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'} + 'multiplayer-treasure-grove': + requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags', eyes: 'wooden-glasses', torso: 'tarnished-bronze-breastplate'} + restrictedGear: {'programming-book': 'programmaticon-i', eyes: 'crude-glasses'} + 'siege-of-stonehold': + requiredGear: {} + restrictedGear: {} + +# Desert + 'the-dunes': + requiredGear: {} + restrictedGear: {} + 'the-mighty-sand-yak': + requiredGear: {} + restrictedGear: {} + 'oasis': + requiredGear: {} + restrictedGear: {} diff --git a/app/views/play/WorldMapView.coffee b/app/views/play/WorldMapView.coffee index f474a249b..f68ea0a36 100644 --- a/app/views/play/WorldMapView.coffee +++ b/app/views/play/WorldMapView.coffee @@ -43,6 +43,8 @@ module.exports = class WorldMapView extends RootView options.supermodel = null @terrain ?= 'dungeon' # or 'forest', 'desert' super options + options ?= {} + @editorMode = options.editorMode @nextLevel = @getQueryVariable 'next' @levelStatusMap = {} @levelPlayCountMap = {} @@ -137,12 +139,12 @@ module.exports = class WorldMapView extends RootView level.locked = false if window.levelUnlocksNotWorking # Temporary; also possible in HeroVictoryModal level.locked = false if @levelStatusMap[level.id] in ['started', 'complete'] level.locked = false if me.get('slug') is 'nick' + level.locked = false if @editorMode level.disabled = false if @levelStatusMap[level.id] in ['started', 'complete'] level.color = 'rgb(255, 80, 60)' if level.requiresSubscription level.color = 'rgb(80, 130, 200)' if level.unlocksHero - level.color = 'rgb(0,0,0)' level.unlockedHero = level.unlocksHero.originalID in (me.get('earned')?.heroes or []) level.hidden = level.locked or level.disabled @@ -159,6 +161,7 @@ module.exports = class WorldMapView extends RootView context.forestIsAvailable = @startedForestLevel or (Level.levels['defense-of-plainswood'] in (me.get('earned')?.levels or [])) context.desertIsAvailable = @startedDesertLevel or (Level.levels['the-mighty-sand-yak'] in (me.get('earned')?.levels or [])) context.requiresSubscription = @requiresSubscription + context.editorMode = @editorMode context afterRender: -> @@ -179,7 +182,7 @@ module.exports = class WorldMapView extends RootView unless window.currentModal or not @fullyRendered @highlightElement '.level.next', delay: 500, duration: 60000, rotation: 0, sides: ['top'] if levelID = @$el.find('.level.next').data('level-id') - @$levelInfo = @$el.find(".level-info-container[data-level-id=#{levelID}]").show() + @$levelInfo = @$el.find(".level-info-container[data-level-id=#{levelID}]").show() unless @editorMode pos = @$el.find('.level.next').offset() @adjustLevelInfoPosition pageX: pos.left, pageY: pos.top @manuallyPositionedLevelInfoID = levelID @@ -194,6 +197,10 @@ module.exports = class WorldMapView extends RootView @openModalView authModal onSessionsLoaded: (e) -> + if @editorMode + @startedForestLevel = true + @startedDesertLevel = true + return forestLevels = (f.id for f in forest) desertLevels = (f.id for f in desert) for session in @sessions.models @@ -252,6 +259,7 @@ module.exports = class WorldMapView extends RootView onMouseEnterLevel: (e) -> return if application.isIPadApp + return if @editorMode levelID = $(e.target).parents('.level').data('level-id') return if @manuallyPositionedLevelInfoID and levelID isnt @manuallyPositionedLevelInfoID @$levelInfo = @$el.find(".level-info-container[data-level-id=#{levelID}]").show() @@ -290,8 +298,8 @@ module.exports = class WorldMapView extends RootView mapHeight = iPadHeight = 1536 mapWidth = {dungeon: 2350, forest: 2500, desert: 2350}[@terrain] or 2350 aspectRatio = mapWidth / mapHeight - pageWidth = $(window).width() - pageHeight = $(window).height() + pageWidth = @$el.width() + pageHeight = @$el.height() widthRatio = pageWidth / mapWidth heightRatio = pageHeight / mapHeight # Make sure we can see the whole map, fading to background in one dimension. diff --git a/server/campaign/campaign_handler.coffee b/server/campaign/campaign_handler.coffee deleted file mode 100644 index 60540a3a1..000000000 --- a/server/campaign/campaign_handler.coffee +++ /dev/null @@ -1,22 +0,0 @@ -Campaign = require './Campaign' -Handler = require '../commons/Handler' - -CampaignHandler = class CampaignHandler extends Handler - modelClass: Campaign - editableProperties: [ - 'name' - 'i18n' - 'i18nCoverage' - 'ambientSound' - 'backgroundImage' - 'backgroundColor' - 'backgroundColorTransparent' - 'adjacentCampaigns' - 'levels' - ] - jsonSchema: require '../../app/schemas/models/campaign.schema' - - hasAccess: (req) -> - req.method is 'GET' or req.user?.isAdmin() - -module.exports = new CampaignHandler() diff --git a/server/campaign/Campaign.coffee b/server/campaigns/Campaign.coffee similarity index 100% rename from server/campaign/Campaign.coffee rename to server/campaigns/Campaign.coffee diff --git a/server/campaigns/campaign_handler.coffee b/server/campaigns/campaign_handler.coffee new file mode 100644 index 000000000..f252e734e --- /dev/null +++ b/server/campaigns/campaign_handler.coffee @@ -0,0 +1,70 @@ +Campaign = require './Campaign' +Level = require '../levels/Level' +Achievement = require '../achievements/Achievement' +Handler = require '../commons/Handler' +async = require 'async' +mongoose = require 'mongoose' + +CampaignHandler = class CampaignHandler extends Handler + modelClass: Campaign + editableProperties: [ + 'name' + 'i18n' + 'i18nCoverage' + 'ambientSound' + 'backgroundImage' + 'backgroundColor' + 'backgroundColorTransparent' + 'adjacentCampaigns' + 'levels' + ] + jsonSchema: require '../../app/schemas/models/campaign.schema' + + hasAccess: (req) -> + req.method is 'GET' or req.user?.isAdmin() + + getByRelationship: (req, res, args...) -> + relationship = args[1] + if relationship in ['levels', 'achievements'] + projection = {} + if req.query.project + projection[field] = 1 for field in req.query.project.split(',') + @getDocumentForIdOrSlug args[0], (err, campaign) => + return @sendDatabaseError(res, err) if err + return @sendNotFoundError(res) unless campaign? + return @getRelatedLevels(req, res, campaign, projection) if relationship is 'levels' + return @getRelatedAchievements(req, res, campaign, projection) if relationship is 'achievements' + else + super(arguments...) + + + getRelatedLevels: (req, res, campaign, projection) -> + levels = campaign.get('levels') or [] + + f = (levelOriginal) -> + (callback) -> + query = { original: mongoose.Types.ObjectId(levelOriginal) } + sort = { 'version.major': -1, 'version.minor': -1 } + Level.findOne(query, projection).sort(sort).exec callback + + fetches = (f(level.original) for level in _.values(levels)) + async.parallel fetches, (err, levels) => + return @sendDatabaseError(res, err) if err + return @sendSuccess(res, (level.toObject() for level in levels)) + + + getRelatedAchievements: (req, res, campaign, projection) -> + levels = campaign.get('levels') or [] + + f = (levelOriginal) -> + (callback) -> + query = { related: levelOriginal } + Achievement.find(query, projection).exec callback + + fetches = (f(level.original) for level in _.values(levels)) + async.parallel fetches, (err, achievementses) => + achievements = _.flatten(achievementses) + return @sendDatabaseError(res, err) if err + return @sendSuccess(res, (achievement.toObject() for achievement in achievements)) + +module.exports = new CampaignHandler() diff --git a/server/commons/mapping.coffee b/server/commons/mapping.coffee index dc3b728f8..2d9724b00 100644 --- a/server/commons/mapping.coffee +++ b/server/commons/mapping.coffee @@ -1,6 +1,7 @@ module.exports.handlers = 'analytics_users_active': 'analytics/analytics_users_active_handler' 'article': 'articles/article_handler' + 'campaign': 'campaigns/campaign_handler' 'level': 'levels/level_handler' 'level_component': 'levels/components/level_component_handler' 'level_feedback': 'levels/feedbacks/level_feedback_handler' diff --git a/server/levels/level_handler.coffee b/server/levels/level_handler.coffee index ace23c255..ae19b033c 100644 --- a/server/levels/level_handler.coffee +++ b/server/levels/level_handler.coffee @@ -31,6 +31,28 @@ LevelHandler = class LevelHandler extends Handler 'i18nCoverage' 'loadingTip' 'requiresSubscription' + 'disableSpaces' + 'hidesSubmitUntilRun' + 'hidesPlayButton' + 'hidesRunShortcut' + 'hidesHUD' + 'hidesSay' + 'hidesCodeToolbar' + 'hidesRealTimePlayback' + 'backspaceThrottle' + 'lockDefaultCode' + 'moveRightLoopSnippet' + 'realTimeSpeedFactor' + 'autocompleteFontSizePx' + 'requiredCode' + 'suspectCode' + 'requiredGear' + 'restrictedGear' + 'allowedHeroes' + ] + + adminEditableProperties: [ + [] ] postEditableProperties: ['name'] diff --git a/test/server/common.coffee b/test/server/common.coffee index a1a894f32..ddde8dd3b 100644 --- a/test/server/common.coffee +++ b/test/server/common.coffee @@ -26,6 +26,7 @@ GLOBAL.tv4 = require 'tv4' # required for TreemaUtils to work models_path = [ '../../server/analytics/AnalyticsUsersActive' '../../server/articles/Article' + '../../server/campaigns/Campaign' '../../server/levels/Level' '../../server/levels/components/LevelComponent' '../../server/levels/systems/LevelSystem' diff --git a/test/server/functional/campaign_handler.spec.coffee b/test/server/functional/campaign_handler.spec.coffee new file mode 100644 index 000000000..cce63b7ec --- /dev/null +++ b/test/server/functional/campaign_handler.spec.coffee @@ -0,0 +1,77 @@ +require '../common' + +levels = [ + { + name: 'Level 1' + description: 'This is the first level.' + disableSpaces: true + icon: 'somestringyoudontneed.png' + } + { + name: 'Level 2' + description: 'This is the second level.' + requiresSubscription: true + backspaceThrottle: true + } +] + +achievement = { + name: 'Level 1 Complete' +} + +campaign = { + name: 'Campaign' + levels: {} +} + +levelURL = getURL('/db/level') +achievementURL = getURL('/db/achievement') +campaignURL = getURL('/db/campaign') +campaignSchema = require '../../../app/schemas/models/campaign.schema' +campaignLevelProperties = _.keys(campaignSchema.properties.levels.additionalProperties.properties) + +describe '/db/campaign', -> + it 'prepares the db first', (done) -> + clearModels [Achievement, Campaign, Level, User], (err) -> + expect(err).toBeNull() + loginAdmin (admin) -> + levels[0].permissions = levels[1].permissions = [{target: admin._id, access: 'owner'}] + request.post {uri: levelURL, json: levels[0]}, (err, res, body) -> + expect(res.statusCode).toBe(200) + levels[0] = body + request.post {uri: levelURL, json: levels[1]}, (err, res, body) -> + expect(res.statusCode).toBe(200) + levels[1] = body + achievement.related = levels[0].original + achievement.rewards = { levels: [levels[1].original] } + request.post {uri: achievementURL, json: achievement}, (err, res, body) -> + achievement = body + done() + + it 'can create campaigns', (done) -> + for level in levels.reverse() + campaign.levels[level.original] = _.pick level, campaignLevelProperties + request.post {uri: campaignURL, json: campaign}, (err, res, body) -> + expect(res.statusCode).toBe(200) + campaign = body + done() + +describe '/db/campaign/.../levels', -> + it 'fetches the levels in a campaign', (done) -> + url = getURL("/db/campaign/#{campaign._id}/levels") + request.get {uri: url}, (err, res, body) -> + expect(res.statusCode).toBe(200) + body = JSON.parse(body) + expect(body.length).toBe(2) + expect(_.difference(['level-1', 'level-2'],(level.slug for level in body)).length).toBe(0) + done() + +describe '/db/campaign/.../achievements', -> + it 'fetches the achievements in the levels in a campaign', (done) -> + url = getURL("/db/campaign/#{campaign._id}/achievements") + request.get {uri: url}, (err, res, body) -> + expect(res.statusCode).toBe(200) + body = JSON.parse(body) + expect(body.length).toBe(1) + done() + \ No newline at end of file From b63b4d64da50609d577a48a2c3b48c46765a210c Mon Sep 17 00:00:00 2001 From: Scott Erickson <sderickson@gmail.com> Date: Wed, 17 Dec 2014 22:53:04 -0800 Subject: [PATCH 03/15] More work on the CampaignEditorView. Data gets saved to models now. --- app/core/treema-ext.coffee | 19 +- app/schemas/models/campaign.schema.coffee | 21 +- .../editor/campaign/CampaignEditorView.coffee | 203 +++++++++++++++--- server/campaigns/campaign_handler.coffee | 2 +- 4 files changed, 204 insertions(+), 41 deletions(-) diff --git a/app/core/treema-ext.coffee b/app/core/treema-ext.coffee index c29c087a9..35546dc00 100644 --- a/app/core/treema-ext.coffee +++ b/app/core/treema-ext.coffee @@ -313,7 +313,7 @@ class InternationalizationNode extends TreemaNode.nodeMap.object class LatestVersionCollection extends CocoCollection -class LatestVersionReferenceNode extends TreemaNode +module.exports.LatestVersionReferenceNode = class LatestVersionReferenceNode extends TreemaNode searchValueTemplate: '<input placeholder="Search" /><div class="treema-search-results"></div>' valueClass: 'treema-latest-version' url: '/db/article' @@ -383,7 +383,11 @@ class LatestVersionReferenceNode extends TreemaNode m = CocoModel.getReferencedModel(@getData(), @workingSchema) data = @getData() if _.isString data # LatestVersionOriginalReferenceNode just uses original - m = @settings.supermodel.getModelByOriginal(m.constructor, data) + if m.schema().properties.version + m = @settings.supermodel.getModelByOriginal(m.constructor, data) + else + # get by id + m = @settings.supermodel.getModel(m.constructor, data) else m = @settings.supermodel.getModelByOriginalAndMajorVersion(m.constructor, data.original, data.majorVersion) if @instance and not m @@ -434,7 +438,7 @@ class LatestVersionReferenceNode extends TreemaNode selected = @getSelectedResultEl() return not selected.length -class LatestVersionOriginalReferenceNode extends LatestVersionReferenceNode +module.exports.LatestVersionOriginalReferenceNode = class LatestVersionOriginalReferenceNode extends LatestVersionReferenceNode # Just for saving the original, not the major version. saveChanges: -> selected = @getSelectedResultEl() @@ -443,6 +447,15 @@ class LatestVersionOriginalReferenceNode extends LatestVersionReferenceNode @data = fullValue.attributes.original @instance = fullValue +module.exports.IDReferenceNode = class IDReferenceNode extends LatestVersionReferenceNode + # Just for saving the _id + saveChanges: -> + selected = @getSelectedResultEl() + return unless selected.length + fullValue = selected.data('value') + @data = fullValue.attributes._id + @instance = fullValue + class LevelComponentReferenceNode extends LatestVersionReferenceNode # HACK: this list of properties is needed by the thang components edit view and config views. # need a better way to specify this, or keep the search models from bleeding into those diff --git a/app/schemas/models/campaign.schema.coffee b/app/schemas/models/campaign.schema.coffee index 606267155..8820a1189 100644 --- a/app/schemas/models/campaign.schema.coffee +++ b/app/schemas/models/campaign.schema.coffee @@ -83,22 +83,27 @@ _.extend CampaignSchema.properties, { } requiredGear: { type: 'object', additionalProperties: { - type: 'string' # should be an originalID, denormalized on the editor side + type: 'array' + items: { type: 'string', links: [{rel: 'db', href: '/db/thang.type/{($)}/version'}], format: 'latest-version-original-reference' } }} restrictedGear: { type: 'object', additionalProperties: { - type: 'string' # should be an originalID, denormalized on the editor side + type: 'array' + items: { type: 'string', links: [{rel: 'db', href: '/db/thang.type/{($)}/version'}], format: 'latest-version-original-reference' } }} - allowedHeroes: { type: 'array', items: { - type: 'string' # should be an originalID, denormalized on the editor side + allowedHeroes: { type: 'array', items: { + type: 'string', links: [{rel: 'db', href: '/db/thang.type/{($)}/version'}], format: 'latest-version-original-reference' }} #- denormalized from Achievements - unlocks: { type: 'array', items: { + rewards: { type: 'array', items: { type: 'object' + additionalProperties: false properties: - original: { type: 'string' } - type: { enum: ['hero', 'item', 'level'] } - achievement: { type: 'string' } + achievement: { type: 'string', links: [{rel: 'db', href: '/db/achievement/{{$}}'}], format: 'achievement' } + item: { type: 'string', links: [{rel: 'db', href: '/db/thang.type/{($)}/version'}], format: 'latest-version-original-reference' } + hero: { type: 'string', links: [{rel: 'db', href: '/db/thang.type/{($)}/version'}], format: 'latest-version-original-reference' } + level: { type: 'string', links: [{rel: 'db', href: '/db/level/{($)}/version'}], format: 'latest-version-original-reference' } + type: { enum: ['heroes', 'items', 'levels'] } }} #- normal properties diff --git a/app/views/editor/campaign/CampaignEditorView.coffee b/app/views/editor/campaign/CampaignEditorView.coffee index 94d5d7bef..882fa748f 100644 --- a/app/views/editor/campaign/CampaignEditorView.coffee +++ b/app/views/editor/campaign/CampaignEditorView.coffee @@ -1,39 +1,157 @@ RootView = require 'views/core/RootView' Campaign = require 'models/Campaign' Level = require 'models/Level' +Achievement = require 'models/Achievement' +ThangType = require 'models/ThangType' WorldMapView = require 'views/play/WorldMapView' CocoCollection = require 'collections/CocoCollection' +treemaExt = require 'core/treema-ext' +utils = require 'core/utils' + +achievementProject = ['related', 'rewards', 'name', 'slug'] +thangTypeProject = ['name', 'original', 'slug'] module.exports = class CampaignEditorView extends RootView id: "campaign-editor-view" template: require 'templates/editor/campaign/campaign-editor-view' className: 'editor' - constructor: -> - super(arguments...) + constructor: (options, campaignHandle) -> + super(options) - # TODO: move the outputted data to the db, and load the Campaign objects instead - for level in levels - _.extend level, options[level.id] - level.slug = level.id - delete level.id - delete level.nextLevels - level.position = { x: level.x, y: level.y } - delete level.x - delete level.y - if level.unlocksHero - level.unlocks = [{ - original: level.unlocksHero.originalID - type: 'hero' - }] - delete level.unlocksHero - campaign.levels[level.original] = level - @campaign = new Campaign(campaign) + # MIGRATION CODE +# for level in levels +# _.extend level, options[level.id] +# level.slug = level.id +# delete level.id +# delete level.nextLevels +# level.position = { x: level.x, y: level.y } +# delete level.x +# delete level.y +# if level.unlocksHero +# level.unlocks = [{ +# original: level.unlocksHero.originalID +# type: 'hero' +# }] +# delete level.unlocksHero +# campaign.levels[level.original] = level +# @campaign = new Campaign(campaign) #------------------------------------------------ + + @campaign = new Campaign({_id:campaignHandle}) - collection = new CocoCollection({model: Level, url}) - collection.ur + #--------------- temporary migration to change thang type slugs to originals + #- should keep around though for loading the names of items and heroes that are referenced + #- just load names instead of slugs, though + @sluggyThangs = new Backbone.Collection() + @listenToOnce @campaign, 'sync', -> + slugs = [] + for level in _.values(@campaign.get('levels')) + slugs = slugs.concat(_.values(level.requiredGear)) if level.requiredGear + slugs = slugs.concat(_.values(level.restrictedGear)) if level.restrictedGear + slugs = slugs.concat(level.allowedHeroes) if level.allowedHeroes + slugs = _.uniq _.flatten slugs + for slug in slugs + thangType = new ThangType() + thangType.setProjection(thangTypeProject) + if utils.isID slug + thangType.setURL("/db/thang.type/#{slug}/version") + else + thangType.setURL("/db/thang.type/#{slug}") + @supermodel.loadModel(thangType, 'thang') + @sluggyThangs.add(thangType) + #--------------- + @supermodel.loadModel(@campaign, 'campaign') + + @levels = new CocoCollection([], { + model: Level + url: "/db/campaign/#{campaignHandle}/levels" + project: Campaign.denormalizedLevelProperties + }) + @supermodel.loadCollection(@levels, 'levels') + + @achievements = new CocoCollection([], { + model: Achievement + url: "/db/campaign/#{campaignHandle}/achievements" + project: achievementProject + }) + @supermodel.loadCollection(@achievements, 'achievements') + @toSave = new Backbone.Collection() + + onLoaded: -> + campaignLevels = $.extend({}, @campaign.get('levels')) + for level in @levels.models + levelOriginal = level.get('original') + campaignLevel = campaignLevels[levelOriginal] ? {} + + #--------------- temporary migrations + if campaignLevel.restrictedGear + for slot, value of campaignLevel.restrictedGear + if _.isString(value) + campaignLevel.restrictedGear[slot] = [value] + # + if campaignLevel.requiredGear + for slot, value of campaignLevel.requiredGear + if _.isString(value) + campaignLevel.requiredGear[slot] = [value] + # + if campaignLevel.requiredGear + for gear in _.values(campaignLevel.requiredGear) + for slug, index in gear + thang = @sluggyThangs.findWhere({slug: slug}) + continue unless thang + gear[index] = thang.get('original') + # + if campaignLevel.restrictedGear + for gear in _.values(campaignLevel.restrictedGear) + for slug, index in gear + thang = @sluggyThangs.findWhere({slug: slug}) + continue unless thang + gear[index] = thang.get('original') + # + if campaignLevel.allowedHeroes + for slug, index in campaignLevel.allowedHeroes + thang = @sluggyThangs.findWhere({slug: slug}) + continue unless thang + level.allowedHeroes[index] = thang.get('original') + #--------------- + + $.extend campaignLevel, _.omit(level.attributes, '_id') + achievements = @achievements.where {'related': levelOriginal} + rewards = [] + for achievement in achievements + for rewardType, rewardArray of achievement.get('rewards') + for reward in rewardArray + rewardObject = { achievement: achievement.id } + + if rewardType is 'heroes' + rewardObject.hero = reward + thangType = new ThangType({}, {project: thangTypeProject}) + thangType.setURL("/db/thang.type/#{reward}/version") + @supermodel.loadModel(thangType, 'thang') + + if rewardType is 'levels' + rewardObject.level = reward + if not @levels.findWhere({original: reward}) + level = new Level({}, {project: Campaign.denormalizedLevelProperties}) + level.setURL("/db/level/#{reward}/version") + @supermodel.loadModel(level, 'level') + + if rewardType is 'items' + rewardObject.item = reward + thangType = new ThangType({}, {project: thangTypeProject}) + thangType.setURL("/db/thang.type/#{reward}/version") + @supermodel.loadModel(thangType, 'thang') + + rewards.push rewardObject + campaignLevel.rewards = rewards + delete campaignLevel.unlocks + campaignLevels[levelOriginal] = campaignLevel + + @campaign.set('levels', campaignLevels) + super() + getRenderData: -> c = super() c.campaign = @campaign @@ -51,17 +169,47 @@ module.exports = class CampaignEditorView extends RootView nodeClasses: levels: LevelsNode level: LevelNode - + achievement: AchievementNode + supermodel: @supermodel @treema = @$el.find('#campaign-treema').treema treemaOptions @treema.build() @treema.open() - @treema.childrenTreemas.levels.open() + @treema.childrenTreemas.levels?.open() worldMapView = new WorldMapView({supermodel: @supermodel, editorMode: true}, 'dungeon') worldMapView.highlightElement = _.noop # make it stop @insertSubView worldMapView + + onTreemaChanged: (e, nodes) => + for node in nodes + path = node.getPath() + if _.string.startsWith path, '/levels/' + parts = path.split('/') + original = parts[2] + level = @supermodel.getModelByOriginal Level, original + campaignLevel = @treema.get "/levels/#{original}" + if 'rewards' in parts + rewardsData = @ + @updateRewardsForLevel level, campaignLevel.rewards + + for key in Campaign.denormalizedLevelProperties + level.set key, campaignLevel[key] + @toSave.add level + + @toSave.add @campaign + + updateRewardsForLevel: (level, rewards) -> + achievements = @supermodel.getModels(Achievement) + achievements = (a for a in achievements when a.get('related') is level.get('original')) + for achievement in achievements + rewardSubset = (r for r in rewards when r.achievement is achievement._id) + newRewards = {} + newRewards.heroes = _.compact((r.hero for r in rewards)) + newRewards.items = _.compact((r.item for r in rewards)) + newRewards.levels = _.compact((r.level for r in rewards)) + achievement.set 'rewards', newRewards class LevelsNode extends TreemaObjectNode valueClass: 'treema-levels' @@ -73,15 +221,12 @@ class LevelsNode extends TreemaObjectNode childPropertiesAvailable: -> @childSource childSource: (req, res) => - console.log 'calling child source!', req s = new Backbone.Collection([], {model:Level}) s.url = '/db/level' s.fetch({data: {term:req.term, project: Campaign.denormalizedLevelProperties.join(',')}}) s.once 'sync', (collection) -> LevelsNode.levels[level.get('original')] = level for level in collection.models - console.log 'results!', collection.models mapped = ({label: r.get('name'), value: r.get('original')} for r in collection.models) - console.log 'mapped', mapped res(mapped) @@ -92,11 +237,11 @@ class LevelNode extends TreemaObjectNode populateData: -> return if @data.name? - console.log 'how do I do this?', @data, @keyForParent, LevelsNode.levels data = _.pick LevelsNode.levels[@keyForParent].attributes, Campaign.denormalizedLevelProperties _.extend @data, data - console.log 'extended to data', data - console.log 'now data is', @data + +class AchievementNode extends treemaExt.IDReferenceNode + buildSearchURL: (term) -> "#{@url}?term=#{term}&project=#{achievementProject.join(',')}" campaign = { name: 'Dungeon' diff --git a/server/campaigns/campaign_handler.coffee b/server/campaigns/campaign_handler.coffee index f252e734e..cc8217d81 100644 --- a/server/campaigns/campaign_handler.coffee +++ b/server/campaigns/campaign_handler.coffee @@ -36,7 +36,7 @@ CampaignHandler = class CampaignHandler extends Handler return @getRelatedAchievements(req, res, campaign, projection) if relationship is 'achievements' else super(arguments...) - + getRelatedLevels: (req, res, campaign, projection) -> levels = campaign.get('levels') or [] From 23da22a559ab9e84d745e4fe32d8c82e29331f89 Mon Sep 17 00:00:00 2001 From: Scott Erickson <sderickson@gmail.com> Date: Fri, 19 Dec 2014 13:04:04 -0500 Subject: [PATCH 04/15] Added a new CampaignView, cloned from WorldMapView. Will migrate to using db data rather than hardcoded data. --- app/views/play/CampaignView.coffee | 1158 ++++++++++++++++++++++++++++ 1 file changed, 1158 insertions(+) create mode 100644 app/views/play/CampaignView.coffee diff --git a/app/views/play/CampaignView.coffee b/app/views/play/CampaignView.coffee new file mode 100644 index 000000000..fc3fdb910 --- /dev/null +++ b/app/views/play/CampaignView.coffee @@ -0,0 +1,1158 @@ +RootView = require 'views/core/RootView' +template = require 'templates/play/world-map-view' +LevelSession = require 'models/LevelSession' +EarnedAchievement = require 'models/EarnedAchievement' +CocoCollection = require 'collections/CocoCollection' +AudioPlayer = require 'lib/AudioPlayer' +LevelSetupManager = require 'lib/LevelSetupManager' +ThangType = require 'models/ThangType' +MusicPlayer = require 'lib/surface/MusicPlayer' +storage = require 'core/storage' +AuthModal = require 'views/core/AuthModal' +SubscribeModal = require 'views/core/SubscribeModal' +Level = require 'models/Level' + +trackedHourOfCode = false + +class LevelSessionsCollection extends CocoCollection + url: '' + model: LevelSession + + constructor: (model) -> + super() + @url = "/db/user/#{me.id}/level.sessions?project=state.complete,levelID" + +module.exports = class WorldMapView extends RootView + id: 'world-map-view' + template: template + + subscriptions: + 'subscribe-modal:subscribed': 'onSubscribed' + + events: + 'click .map-background': 'onClickMap' + 'click .level a': 'onClickLevel' + 'click .level-info-container .start-level': 'onClickStartLevel' + 'mouseenter .level a': 'onMouseEnterLevel' + 'mouseleave .level a': 'onMouseLeaveLevel' + 'mousemove .map': 'onMouseMoveMap' + 'click #volume-button': 'onToggleVolume' + + constructor: (options, @terrain) -> + if options and application.isIPAdApp # TODO: later only clear the SuperModel if it has received a memory warning (not in app store yet) + options.supermodel = null + @terrain ?= 'dungeon' # or 'forest', 'desert' + super options + options ?= {} + + @campaign = new Campaign({_id:campaignHandle}) + @supermodel.loadModel(@campaign, 'campaign') + + @editorMode = options.editorMode + @nextLevel = @getQueryVariable 'next' + @levelStatusMap = {} + @levelPlayCountMap = {} + @sessions = @supermodel.loadCollection(new LevelSessionsCollection(), 'your_sessions', null, 0).model + + # Temporary attempt to make sure all earned rewards are accounted for. Figure out a better solution... + @earnedAchievements = new CocoCollection([], {url: '/db/earned_achievement', model:EarnedAchievement, project: ['earnedRewards']}) + @listenToOnce @earnedAchievements, 'sync', -> + earned = me.get('earned') + addedSomething = false + for m in @earnedAchievements.models + continue unless loadedEarned = m.get('earnedRewards') + for group in ['heroes', 'levels', 'items'] + continue unless loadedEarned[group] + for reward in loadedEarned[group] + if reward not in earned[group] + console.warn 'Filling in a gap for reward', group, reward + earned[group].push(reward) + addedSomething = true + @supermodel.loadCollection(@earnedAchievements, 'achievements') + + @listenToOnce @sessions, 'sync', @onSessionsLoaded + @getLevelPlayCounts() + $(window).on 'resize', @onWindowResize + @playAmbientSound() + @probablyCachedMusic = storage.load("loaded-menu-music") + musicDelay = if @probablyCachedMusic then 1000 else 10000 + @playMusicTimeout = _.delay (=> @playMusic() unless @destroyed), musicDelay + @hadEverChosenHero = me.get('heroConfig')?.thangType + @listenTo me, 'change:purchased', -> @renderSelectors('#gems-count') + @listenTo me, 'change:spent', -> @renderSelectors('#gems-count') + @listenTo me, 'change:heroConfig', -> @updateHero() + window.tracker?.trackEvent 'Loaded World Map', category: 'World Map', ['Google Analytics'] + + # If it's a new player who didn't appear to come from Hour of Code, we register her here without setting the hourOfCode property. + elapsed = (new Date() - new Date(me.get('dateCreated'))) + if not trackedHourOfCode and not me.get('hourOfCode') and elapsed < 5 * 60 * 1000 + $('body').append($('<img src="http://code.org/api/hour/begin_codecombat.png" style="visibility: hidden;">')) + trackedHourOfCode = true + + @requiresSubscription = not me.isPremium() + + destroy: -> + @setupManager?.destroy() + @$el.find('.ui-draggable').draggable 'destroy' + $(window).off 'resize', @onWindowResize + if ambientSound = @ambientSound + # Doesn't seem to work; stops immediately. + createjs.Tween.get(ambientSound).to({volume: 0.0}, 1500).call -> ambientSound.stop() + @musicPlayer?.destroy() + clearTimeout @playMusicTimeout + super() + + getLevelPlayCounts: -> + return unless me.isAdmin() + success = (levelPlayCounts) => + return if @destroyed + for level in levelPlayCounts + @levelPlayCountMap[level._id] = playtime: level.playtime, sessions: level.sessions + @render() if @fullyRendered + + levelIDs = [] + for campaign in campaigns + for level in campaign.levels + levelIDs.push level.id + levelPlayCountsRequest = @supermodel.addRequestResource 'play_counts', { + url: '/db/level/-/play_counts' + data: {ids: levelIDs} + method: 'POST' + success: success + }, 0 + levelPlayCountsRequest.load() + + onLoaded: -> + return if @fullyRendered + @fullyRendered = true + @render() + @preloadTopHeroes() unless me.get('heroConfig')?.thangType + + onSubscribed: -> + @requiresSubscription = false + @render() + + getRenderData: (context={}) -> + context = super(context) + context.campaign = _.find campaigns, { id: @terrain } + for level in context.campaign.levels + level.x ?= 10 + 80 * Math.random() + level.y ?= 10 + 80 * Math.random() + level.locked = not me.ownsLevel level.original + window.levelUnlocksNotWorking = true if level.locked and level.id is @nextLevel # Temporary + level.locked = false if window.levelUnlocksNotWorking # Temporary; also possible in HeroVictoryModal + level.locked = false if @levelStatusMap[level.id] in ['started', 'complete'] + level.locked = false if me.get('slug') is 'nick' + level.locked = false if @editorMode + level.disabled = false if @levelStatusMap[level.id] in ['started', 'complete'] + level.color = 'rgb(255, 80, 60)' + if level.requiresSubscription + level.color = 'rgb(80, 130, 200)' + if level.unlocksHero + level.unlockedHero = level.unlocksHero.originalID in (me.get('earned')?.heroes or []) + level.hidden = level.locked or level.disabled + + ## put lower levels in last, so in the world map they layer over one another properly. + #context.campaign.levels = (_.sortBy context.campaign.levels, 'y').reverse() + # Actually, there's some logic that depends on the order of iteration of levels to determine + # which one to do next when you're coming here not from a level; can we do this another way? + + context.levelStatusMap = @levelStatusMap + context.levelPlayCountMap = @levelPlayCountMap + context.isIPadApp = application.isIPadApp + context.mapType = _.string.slugify @terrain + context.nextLevel = @nextLevel + context.forestIsAvailable = @startedForestLevel or (Level.levels['defense-of-plainswood'] in (me.get('earned')?.levels or [])) + context.desertIsAvailable = @startedDesertLevel or (Level.levels['the-mighty-sand-yak'] in (me.get('earned')?.levels or [])) + context.requiresSubscription = @requiresSubscription + context.editorMode = @editorMode + context + + afterRender: -> + super() + @onWindowResize() + unless application.isIPadApp + _.defer => @$el?.find('.game-controls .btn').tooltip() # Have to defer or i18n doesn't take effect. + @$el.find('.level').tooltip().each -> + return unless me.isAdmin() + $(@).draggable().on 'dragstop', -> + bg = $('.map-background') + x = ($(@).offset().left - bg.offset().left + $(@).outerWidth() / 2) / bg.width() + y = 1 - ($(@).offset().top - bg.offset().top + $(@).outerHeight() / 2) / bg.height() + console.log "#{$(@).data('level-id')}\n x: #{(100 * x).toFixed(2)}\n y: #{(100 * y).toFixed(2)}\n" + @$el.addClass _.string.slugify @terrain + @updateVolume() + @updateHero() + unless window.currentModal or not @fullyRendered + @highlightElement '.level.next', delay: 500, duration: 60000, rotation: 0, sides: ['top'] + if levelID = @$el.find('.level.next').data('level-id') + @$levelInfo = @$el.find(".level-info-container[data-level-id=#{levelID}]").show() unless @editorMode + pos = @$el.find('.level.next').offset() + @adjustLevelInfoPosition pageX: pos.left, pageY: pos.top + @manuallyPositionedLevelInfoID = levelID + + afterInsert: -> + super() + return unless @getQueryVariable 'signup' + return if me.get('email') + @endHighlight() + authModal = new AuthModal supermodel: @supermodel + authModal.mode = 'signup' + @openModalView authModal + + onSessionsLoaded: (e) -> + if @editorMode + @startedForestLevel = true + @startedDesertLevel = true + return + forestLevels = (f.id for f in forest) + desertLevels = (f.id for f in desert) + for session in @sessions.models + @levelStatusMap[session.get('levelID')] = if session.get('state')?.complete then 'complete' else 'started' + @startedForestLevel = true if session.get('levelID') in forestLevels + @startedDesertLevel = true if session.get('levelID') in desertLevels + if @nextLevel and @levelStatusMap[@nextLevel] is 'complete' + @nextLevel = null + @render() + + onClickMap: (e) -> + @$levelInfo?.hide() + # Easy-ish way of figuring out coordinates for placing level dots. + x = e.offsetX / @$el.find('.map-background').width() + y = (1 - e.offsetY / @$el.find('.map-background').height()) + console.log " x: #{(100 * x).toFixed(2)}\n y: #{(100 * y).toFixed(2)}\n" + + onClickLevel: (e) -> + e.preventDefault() + e.stopPropagation() + @$levelInfo?.hide() + levelElement = $(e.target).parents('.level') + levelID = levelElement.data('level-id') + campaign = _.find campaigns, id: @terrain + level = _.find campaign.levels, id: levelID + if application.isIPadApp + @$levelInfo = @$el.find(".level-info-container[data-level-id=#{levelID}]").show() + @adjustLevelInfoPosition e + @endHighlight() + else + if level.requiresSubscription and @requiresSubscription and not @levelStatusMap[level.id] and not level.adventurer + @openModalView new SubscribeModal() + window.tracker?.trackEvent 'Show subscription modal', category: 'Subscription', label: 'map level clicked', level: levelID + else if $(e.target).attr('disabled') + Backbone.Mediator.publish 'router:navigate', route: '/contribute/adventurer' + return + else if $(e.target).parent().hasClass 'locked' + return + else + @startLevel levelElement + window.tracker?.trackEvent 'Clicked Level', category: 'World Map', levelID: levelID, ['Google Analytics'] + + onClickStartLevel: (e) -> + levelElement = $(e.target).parents('.level-info-container') + @startLevel levelElement + window.tracker?.trackEvent 'Clicked Start Level', category: 'World Map', levelID: levelElement.data('level-id'), ['Google Analytics'] + + startLevel: (levelElement) -> + @setupManager?.destroy() + @setupManager = new LevelSetupManager supermodel: @supermodel, levelID: levelElement.data('level-id'), levelPath: levelElement.data('level-path'), levelName: levelElement.data('level-name'), hadEverChosenHero: @hadEverChosenHero, parent: @ + @setupManager.open() + @$levelInfo?.hide() + + onMouseEnterLevel: (e) -> + return if application.isIPadApp + return if @editorMode + levelID = $(e.target).parents('.level').data('level-id') + return if @manuallyPositionedLevelInfoID and levelID isnt @manuallyPositionedLevelInfoID + @$levelInfo = @$el.find(".level-info-container[data-level-id=#{levelID}]").show() + @adjustLevelInfoPosition e + @endHighlight() + @manuallyPositionedLevelInfoID = false + + onMouseLeaveLevel: (e) -> + return if application.isIPadApp + levelID = $(e.target).parents('.level').data('level-id') + return if @manuallyPositionedLevelInfoID and levelID isnt @manuallyPositionedLevelInfoID + @$el.find(".level-info-container[data-level-id='#{levelID}']").hide() + @manuallyPositionedLevelInfoID = null + @$levelInfo = null + + onMouseMoveMap: (e) -> + return if application.isIPadApp + @adjustLevelInfoPosition e unless @manuallyPositionedLevelInfoID + + adjustLevelInfoPosition: (e) -> + return unless @$levelInfo + @$map ?= @$el.find('.map') + mapOffset = @$map.offset() + mapX = e.pageX - mapOffset.left + mapY = e.pageY - mapOffset.top + margin = 20 + width = @$levelInfo.outerWidth() + @$levelInfo.css('left', Math.min(Math.max(margin, mapX - width / 2), @$map.width() - width - margin)) + height = @$levelInfo.outerHeight() + top = mapY - @$levelInfo.outerHeight() - 60 + if top < 20 + top = mapY + 60 + @$levelInfo.css('top', top) + + onWindowResize: (e) => + mapHeight = iPadHeight = 1536 + mapWidth = {dungeon: 2350, forest: 2500, desert: 2350}[@terrain] or 2350 + aspectRatio = mapWidth / mapHeight + pageWidth = @$el.width() + pageHeight = @$el.height() + widthRatio = pageWidth / mapWidth + heightRatio = pageHeight / mapHeight + # Make sure we can see the whole map, fading to background in one dimension. + if heightRatio <= widthRatio + # Left and right margin + resultingHeight = pageHeight + resultingWidth = resultingHeight * aspectRatio + else + # Top and bottom margin + resultingWidth = pageWidth + resultingHeight = resultingWidth / aspectRatio + resultingMarginX = (pageWidth - resultingWidth) / 2 + resultingMarginY = (pageHeight - resultingHeight) / 2 + @$el.find('.map').css(width: resultingWidth, height: resultingHeight, 'margin-left': resultingMarginX, 'margin-top': resultingMarginY) + + playAmbientSound: -> + return if @ambientSound + return unless file = {dungeon: 'ambient-dungeon', forest: 'ambient-map-grass', desert: 'ambient-desert'}[@terrain] + src = "/file/interface/#{file}#{AudioPlayer.ext}" + unless AudioPlayer.getStatus(src)?.loaded + AudioPlayer.preloadSound src + Backbone.Mediator.subscribeOnce 'audio-player:loaded', @playAmbientSound, @ + return + @ambientSound = createjs.Sound.play src, loop: -1, volume: 0.1 + createjs.Tween.get(@ambientSound).to({volume: 0.5}, 1000) + + playMusic: -> + @musicPlayer = new MusicPlayer() + musicFile = '/music/music-menu' + Backbone.Mediator.publish 'music-player:play-music', play: true, file: musicFile + storage.save("loaded-menu-music", true) unless @probablyCachedMusic + + preloadTopHeroes: -> + for heroID in ['captain', 'knight'] + url = "/db/thang.type/#{ThangType.heroes[heroID]}/version" + continue if @supermodel.getModel url + fullHero = new ThangType() + fullHero.setURL url + @supermodel.loadModel fullHero, 'thang' + + updateVolume: (volume) -> + volume ?= me.get('volume') ? 1.0 + classes = ['vol-off', 'vol-down', 'vol-up'] + button = $('#volume-button', @$el) + button.toggleClass 'vol-off', volume <= 0.0 + button.toggleClass 'vol-down', 0.0 < volume < 1.0 + button.toggleClass 'vol-up', volume >= 1.0 + createjs.Sound.setVolume(if volume is 1 then 0.6 else volume) # Quieter for now until individual sound FX controls work again. + if volume isnt me.get 'volume' + me.set 'volume', volume + me.patch() + + onToggleVolume: (e) -> + button = $(e.target).closest('#volume-button') + classes = ['vol-off', 'vol-down', 'vol-up'] + volumes = [0, 0.4, 1.0] + for oldClass, i in classes + if button.hasClass oldClass + newI = (i + 1) % classes.length + break + else if i is classes.length - 1 # no oldClass + newI = 2 + @updateVolume volumes[newI] + + updateHero: -> + return unless hero = me.get('heroConfig')?.thangType + for slug, original of ThangType.heroes when original is hero + @$el.find('.player-hero-icon').removeClass().addClass('player-hero-icon ' + slug) + return + console.error "WorldMapView hero update couldn't find hero slug for original:", hero + +dungeon = [ + { + name: 'Dungeons of Kithgard' + type: 'hero' + id: 'dungeons-of-kithgard' + original: '5411cb3769152f1707be029c' + description: 'Grab the gem, but touch nothing else. Start here.' + x: 14 + y: 15.5 + nextLevels: + continue: 'gems-in-the-deep' + } + { + name: 'Gems in the Deep' + type: 'hero' + id: 'gems-in-the-deep' + original: '54173c90844506ae0195a0b4' + description: 'Quickly collect the gems; you will need them.' + x: 29 + y: 12 + nextLevels: + continue: 'shadow-guard' + } + { + name: 'Shadow Guard' + type: 'hero' + id: 'shadow-guard' + original: '54174347844506ae0195a0b8' + description: 'Evade the Kithgard minion.' + x: 40.54 + y: 11.03 + nextLevels: + continue: 'forgetful-gemsmith' + } + { + name: 'Kounter Kithwise' + type: 'hero' + id: 'kounter-kithwise' + original: '54527a6257e83800009730c7' + description: 'Practice your evasion skills with more guards.' + x: 35.37 + y: 20.61 + nextLevels: + continue: 'crawlways-of-kithgard' + practice: true + requiresSubscription: true + } + { + name: 'Crawlways of Kithgard' + type: 'hero' + id: 'crawlways-of-kithgard' + original: '545287ef57e83800009730d5' + description: 'Dart in and grab the gem–at the right moment.' + x: 36.48 + y: 29.03 + nextLevels: + continue: 'forgetful-gemsmith' + practice: true + requiresSubscription: true + } + { + name: 'Forgetful Gemsmith' + type: 'hero' + id: 'forgetful-gemsmith' + original: '544a98f62d002f0000fe331a' + description: 'Grab even more gems as you practice moving.' + x: 54.98 + y: 10.53 + nextLevels: + continue: 'true-names' + } + { + name: 'True Names' + type: 'hero' + id: 'true-names' + original: '541875da4c16460000ab990f' + description: 'Learn an enemy\'s true name to defeat it.' + x: 68.44 + y: 10.70 + nextLevels: + continue: 'the-raised-sword' + unlocksHero: { + img: '/file/db/thang.type/53e12be0d042f23505c3023b/portrait.png' + originalID: '53e12be0d042f23505c3023b' + } + } + { + name: 'Favorable Odds' + type: 'hero' + id: 'favorable-odds' + original: '5452972f57e83800009730de' + description: 'Test out your battle skills by defeating more munchkins.' + x: 88.25 + y: 14.92 + nextLevels: + continue: 'the-raised-sword' + practice: true + requiresSubscription: true + } + { + name: 'The Raised Sword' + type: 'hero' + id: 'the-raised-sword' + original: '5418aec24c16460000ab9aa6' + description: 'Learn to equip yourself for combat.' + x: 81.51 + y: 17.92 + nextLevels: + continue: 'haunted-kithmaze' + } + { + name: 'Haunted Kithmaze' + type: 'hero' + id: 'haunted-kithmaze' + original: '545a5914d820eb0000f6dc0a' + description: 'The builders of Kithgard constructed many mazes to confuse travelers.' + x: 78 + y: 29 + nextLevels: + continue: 'the-second-kithmaze' + } + { + name: 'Riddling Kithmaze' + type: 'hero' + id: 'riddling-kithmaze' + original: '5418b9d64c16460000ab9ab4' + description: 'If at first you go astray, change your loop to find the way.' + x: 69.97 + y: 28.03 + nextLevels: + continue: 'descending-further' + practice: true + requiresSubscription: true + } + { + name: 'Descending Further' + type: 'hero' + id: 'descending-further' + original: '5452a84d57e83800009730e4' + description: 'Another day, another maze.' + x: 61.68 + y: 22.80 + nextLevels: + continue: 'the-second-kithmaze' + practice: true + requiresSubscription: true + } + { + name: 'The Second Kithmaze' + type: 'hero' + id: 'the-second-kithmaze' + original: '5418cf256bae62f707c7e1c3' + description: 'Many have tried, few have found their way through this maze.' + x: 54.49 + y: 26.49 + nextLevels: + continue: 'dread-door' + } + { + name: 'Dread Door' + type: 'hero' + id: 'dread-door' + original: '5418d40f4c16460000ab9ac2' + description: 'Behind a dread door lies a chest full of riches.' + x: 60.52 + y: 33.70 + nextLevels: + continue: 'known-enemy' + } + { + name: 'Known Enemy' + type: 'hero' + id: 'known-enemy' + original: '5452adea57e83800009730ee' + description: 'Begin to use variables in your battles.' + x: 67 + y: 39 + nextLevels: + continue: 'master-of-names' + } + { + name: 'Master of Names' + type: 'hero' + id: 'master-of-names' + original: '5452c3ce57e83800009730f7' + description: 'Use your glasses to defend yourself from the Kithmen.' + x: 75 + y: 46 + nextLevels: + continue: 'lowly-kithmen' + } + { + name: 'Lowly Kithmen' + type: 'hero' + id: 'lowly-kithmen' + original: '541b24511ccc8eaae19f3c1f' + description: 'Now that you can see them, they\'re everywhere!' + x: 85 + y: 40 + nextLevels: + continue: 'closing-the-distance' + } + { + name: 'Closing the Distance' + type: 'hero' + id: 'closing-the-distance' + original: '541b288e1ccc8eaae19f3c25' + description: 'Kithmen are not the only ones to stand in your way.' + x: 93 + y: 47 + nextLevels: + continue: 'the-final-kithmaze' + } + { + name: 'Tactical Strike' + type: 'hero' + id: 'tactical-strike' + original: '5452cfa706a59e000067e4f5' + description: 'They\'re, uh, coming right for us! Sneak up behind them.' + x: 83.23 + y: 52.73 + nextLevels: + continue: 'the-final-kithmaze' + practice: true + requiresSubscription: true + } + { + name: 'The Final Kithmaze' + type: 'hero' + id: 'the-final-kithmaze' + original: '541b434e1ccc8eaae19f3c33' + description: 'To escape you must find your way through an Elder Kithman\'s maze.' + x: 86.95 + y: 64.70 + nextLevels: + continue: 'kithgard-gates' + } + { + name: 'The Gauntlet' + type: 'hero' + id: 'the-gauntlet' + original: '5452d8b906a59e000067e4fa' + description: 'Rush for the stairs, battling foes at every turn.' + x: 76.50 + y: 72.69 + nextLevels: + continue: 'kithgard-gates' + practice: true + requiresSubscription: true + } + { + name: 'Kithgard Gates' + type: 'hero' + id: 'kithgard-gates' + original: '541c9a30c6362edfb0f34479' + description: 'Escape the Kithgard dungeons and don\'t let the guardians get you.' + x: 89 + y: 82 + nextLevels: + continue: 'defense-of-plainswood' + } + { + name: 'Cavern Survival' + type: 'hero-ladder' + id: 'cavern-survival' + original: '544437e0645c0c0000c3291d' + description: 'Stay alive longer than your opponent amidst hordes of ogres!' + x: 17.54 + y: 78.39 + adventurer: true + } +] + +forest = [ + { + name: 'Defense of Plainswood' + type: 'hero' + id: 'defense-of-plainswood' + original: '541b67f71ccc8eaae19f3c62' + description: 'Protect the peasants from the pursuing ogres.' + nextLevels: + continue: 'winding-trail' + x: 18 + y: 37 + } + { + name: 'Winding Trail' + type: 'hero' + id: 'winding-trail' + original: '5446cb40ce01c23e05ecf027' + description: 'Stay alive and navigate through the forest.' + nextLevels: + continue: 'patrol-buster' + x: 24 + y: 35 + } + { + name: 'Patrol Buster' + type: 'hero' + id: 'patrol-buster' + original: '5487330d84f7b4dac246d440' + description: 'Defeat ogre patrols with new, selective targeting skills.' + nextLevels: + continue: 'thornbush-farm' + x: 34 + y: 25 + } + { + name: 'Endangered Burl' + type: 'hero' + id: 'endangered-burl' + original: '546e97033f1c1c1be898402b' + description: 'Hunt ogres in the woods, but watch out for lumbering beasts.' + nextLevels: + continue: 'thornbush-farm' + x: 29 + y: 35 + } + { + name: 'Village Guard' + type: 'hero' + id: 'village-guard' + original: '546e91b8a4b7840000ee92dc' + description: 'Defend a village from marauding munchkin mayhem.' + nextLevels: + continue: 'thornbush-farm' + x: 33 + y: 37 + practice: true + requiresSubscription: true + } + { + name: 'Thornbush Farm' + type: 'hero' + id: 'thornbush-farm' + original: '5447030525cce60000745e2a' + description: 'Determine refugee peasant from ogre when defending the farm.' + nextLevels: + continue: 'back-to-back' + x: 37 + y: 40 + } + { + name: 'Back to Back' + type: 'hero' + id: 'back-to-back' + original: '5448330517d7283e051f9b9e' + description: 'Patrol the village entrances, but stay defensive.' + nextLevels: + continue: 'ogre-encampment' + x: 39 + y: 47.5 + } + { + name: 'Ogre Encampment' + type: 'hero' + id: 'ogre-encampment' + original: '5456b3c8d5ada30000525605' + description: 'Recover stolen treasure from an ogre encampment.' + nextLevels: + continue: 'woodland-cleaver' + x: 39 + y: 55 + } + { + name: 'Woodland Cleaver' + type: 'hero' + id: 'woodland-cleaver' + original: '5456bb8dd5ada30000525613' + description: 'Use your new cleave ability to fend off munchkins.' + nextLevels: + continue: 'shield-rush' + x: 39.5 + y: 61 + } + { + name: 'Shield Rush' + type: 'hero' + id: 'shield-rush' + original: '5459570bb4461871053292f5' + description: 'Combine cleave and shield to endure an ogre onslaught.' + nextLevels: + continue: 'peasant-protection' + x: 42 + y: 68 + } + + # Warrior branch + { + name: 'Peasant Protection' + type: 'hero' + id: 'peasant-protection' + original: '545ec477e7f60fd6c55760e9' + description: 'Stay close to Victor.' + nextLevels: + continue: 'munchkin-swarm' + x: 44.5 + y: 75.5 + } + { + name: 'Munchkin Swarm' + type: 'hero' + id: 'munchkin-swarm' + original: '545edba9e7f60fd6c5576133' + description: 'Loot a gigantic chest while surrounded by a swarm of ogre munchkins.' + nextLevels: + continue: 'coinucopia' + x: 49 + y: 81 + } + + # Ranger branch + { + name: 'Munchkin Harvest' + type: 'hero' + id: 'munchkin-harvest' + original: '5470001860f6cc376131525d' + description: 'Join forces with a new hero: Amara Arrowhead.' + nextLevels: + continue: 'swift-dagger' + x: 38 + y: 72 + requiresSubscription: true + unlocksHero: { + img: '/file/db/thang.type/52fc0ed77e01835453bd8f6c/portrait.png' + originalID: '52fc0ed77e01835453bd8f6c' + } + } + { + name: 'Swift Dagger' + type: 'hero' + id: 'swift-dagger' + original: '54701f7860f6cc37613152a1' + description: 'Deal damage from a distance with your new hero.' + nextLevels: + continue: 'shrapnel' + x: 33 + y: 72 + requiresSubscription: true + } + { + name: 'Shrapnel' + type: 'hero' + id: 'shrapnel' + original: '5470291c60f6cc37613152d1' + description: 'Explore the explosive arts.' + nextLevels: + continue: 'coinucopia' + x: 28 + y: 73 + requiresSubscription: true + } + + # Wizard branch + { + name: 'Arcane Ally' + type: 'hero' + id: 'arcane-ally' + original: '5470b98ceb739dbc9d2402c7' + description: 'Stand your ground against large ogres with a new hero: Ms. Hushbaum.' + nextLevels: + continue: 'touch-of-death' + x: 47 + y: 71 + requiresSubscription: true + unlocksHero: { + img: '/file/db/thang.type/52fbf74b7e01835453bd8d8e/portrait.png' + originalID: '529ec584c423d4e83b000014' + } + } + { + name: 'Touch of Death' + type: 'hero' + id: 'touch-of-death' + original: '5470ca33eb739dbc9d2402ee' + description: 'Learn your first spell to siphon life from your foes.' + nextLevels: + continue: 'bonemender' + x: 52 + y: 70 + requiresSubscription: true + } + { + name: 'Bonemender' + type: 'hero' + id: 'bonemender' + original: '5470d013eb739dbc9d240323' + description: 'Cast regeneration on allied soldiers to withstand a siege.' + nextLevels: + continue: 'coinucopia' + x: 58 + y: 67 + requiresSubscription: true + } + + { + name: 'Coinucopia' + type: 'hero' + id: 'coinucopia' + original: '545bb1181e649a4495f887df' + description: 'Start playing in real-time with input flags as you collect gold coins!' + nextLevels: + continue: 'copper-meadows' + x: 56 + y: 82 + } + { + name: 'Copper Meadows' + type: 'hero' + id: 'copper-meadows' + original: '5462491c688f333d05d8af38' + description: 'This level exercises: if/else, object members, variables, flag placement, and collection.' + nextLevels: + continue: 'drop-the-flag' + x: 60 + y: 86 + } + { + name: 'Drop the Flag' + type: 'hero' + id: 'drop-the-flag' + original: '54626472f3c64b7b0598590c' + description: 'This level exercises: flag position, object members.' + nextLevels: + continue: 'rich-forager' + x: 65.5 + y: 91 + } + { + name: 'Deadly Pursuit' + type: 'hero' + id: 'deadly-pursuit' + original: '54626f270cacde3f055434ac' + description: 'This level exercises: if/else, flag placement and timing, item collection.' + nextLevels: + continue: 'rich-forager' + x: 74.5 + y: 92 + requiresSubscription: true + unlocksHero: { + img: '/file/db/thang.type/5466d449417c8b48a9811e83/portrait.png' + originalID: '5466d449417c8b48a9811e83' + } + } + { + name: 'Rich Forager' + type: 'hero' + id: 'rich-forager' + original: '546283ddfdd66af405fa8209' + description: 'This level exercises: if/else if, collection, combat.' + nextLevels: + continue: 'siege-of-stonehold' + x: 80 + y: 88 + unlocksHero: { + img: '/file/db/thang.type/52e9adf7427172ae56002172/portrait.png' + originalID: '52e9adf7427172ae56002172' + } + } + { + name: 'Siege of Stonehold' + type: 'hero' + id: 'siege-of-stonehold' + original: '54712072eb739dbc9d24034b' + description: 'Unlock the desert world, if you are strong enough to win this epic battle!' + nextLevels: + continue: 'the-dunes' + x: 85.5 + y: 83.5 + } + { + name: 'Multiplayer Treasure Grove' + type: 'hero-ladder' + id: 'multiplayer-treasure-grove' + original: '5469643c37600b40e0e09c5b' + description: 'Mix collection, flags, and combat in this multiplayer coin-gathering arena.' + x: 56.5 + y: 20 + } + { + name: 'Dueling Grounds' + type: 'hero-ladder' + id: 'dueling-grounds' + original: '5442ba0e1e835500007eb1c7' + description: 'Battle head-to-head against another hero in this basic beginner combat arena.' + x: 83 + y: 23 + adventurer: true + } +] + +desert = [ + { + name: 'The Dunes' + type: 'hero' + id: 'the-dunes' + original: '5480b62e1bf0b10000711c59' + description: 'Behold, the desert, full of glory, danger, and sand. Lots of sand.' + nextLevels: + continue: 'the-mighty-sand-yak' + x: 8.47 + y: 21.93 + requiresSubscription: true + } + { + name: 'The Mighty Sand Yak' + type: 'hero' + id: 'the-mighty-sand-yak' + original: '5480b9d01bf0b10000711c5f' + description: 'Test your nerves by dodging huge sand yaks on the open dunes!' + nextLevels: + continue: 'oasis' + x: 16.56 + y: 27.77 + requiresSubscription: false + } + { + name: 'Oasis' + type: 'hero' + id: 'oasis' + original: '5480ba761bf0b10000711c64' + description: 'Run a gauntlet of sand yaks to reach oasis and quench your thirst!' + nextLevels: + continue: 'sarven-road' + x: 23.35 + y: 31.60 + requiresSubscription: false + } + { + name: 'Sarven Road' + type: 'hero' + id: 'sarven-road' + original: '548c82360ffdc235e80ef04b' + description: 'Watch out for ogre scouts on the road as you search for water.' + nextLevels: + continue: 'sarven-gaps' + x: 28.36 + y: 24.59 + adventurer: true + requiresSubscription: false + } + { + name: 'Sarven Gaps' + type: 'hero' + id: 'sarven-gaps' + original: '548c8f4a0ffdc235e80ef0a8' + description: 'Keep the oasis safe by building fences to hold back the enemy.' + nextLevels: + continue: 'thunderhooves' + x: 21.13 + y: 9.29 + adventurer: true + requiresSubscription: true + } + { + name: 'Thunderhooves' + type: 'hero' + id: 'thunderhooves' + original: '548c90020ffdc235e80ef0ad' + description: 'Fence off the stampeding sand yaks to reach the next watering hole.' + nextLevels: + continue: 'medical-attention' + x: 35.08 + y: 20.48 + adventurer: true + requiresSubscription: false + } + { + name: 'Medical Attention' + type: 'hero' + id: 'medical-attention' + original: '548ce3300ffdc235e80ef0b2' + description: 'Get help from a helpful wizard while you fend off an ogre attack.' + nextLevels: + continue: 'minesweeper' + x: 42.84 + y: 21.82 + adventurer: true + requiresSubscription: false + } + { + name: 'Minesweeper' + type: 'hero' + id: 'minesweeper' + original: '5490cb7c623b972aa26b25a3' + description: 'Lead a band of hapless peasants through a treacherous canyon while you heroically trigger the mines.' + nextLevels: + continue: 'sarven-sentry' + x: 47.64 + y: 12.40 + adventurer: true + requiresSubscription: true + } + { + name: 'Sarven Sentry' + type: 'hero' + id: 'sarven-sentry' + original: '548cef7f0ffdc235e80ef0cc' + description: 'Coming Soon' + nextLevels: + continue: 'keeping-time' + x: 51.48 + y: 26.09 + adventurer: true + requiresSubscription: false + disabled: not me.isAdmin() + } + { + name: 'Keeping Time' + type: 'hero' + id: 'keeping-time' + original: '548cf1a90ffdc235e80ef0d1' + description: 'Coming Soon' + nextLevels: + continue: 'hoarding-gold' + x: 58.42 + y: 34.14 + adventurer: true + requiresSubscription: false + disabled: not me.isAdmin() + } + { + name: 'Hoarding Gold' + type: 'hero' + id: 'hoarding-gold' + original: '' + description: 'Coming Soon' + nextLevels: + continue: 'decoy-drill' + x: 61.73 + y: 29.51 + adventurer: true + requiresSubscription: false + disabled: not me.isAdmin() + } + { + name: 'Decoy Drill' + type: 'hero' + id: 'decoy-drill' + original: '' + description: 'Coming Soon' + nextLevels: + continue: 'yakstraction' + x: 62.05 + y: 40.44 + adventurer: true + requiresSubscription: false + disabled: not me.isAdmin() + } + { + name: 'Yakstraction' + type: 'hero' + id: 'yakstraction' + original: '' + description: 'Coming Soon' + nextLevels: + continue: 'sarven-brawl' + x: 66.46 + y: 48.87 + adventurer: true + requiresSubscription: true + } + { + name: 'Sarven Brawl' + type: 'hero' + id: 'sarven-brawl' + original: '548cf2850ffdc235e80ef0d6' + description: 'Coming Soon' + #nextLevels: + # continue: '' + x: 69.01 + y: 33.80 + adventurer: true + requiresSubscription: false + disabled: not me.isAdmin() + } + +] + +WorldMapView.campaigns = campaigns = [ + {id: 'dungeon', name: 'Dungeon Campaign', levels: dungeon } + {id: 'forest', name: 'Forest Campaign', levels: forest } + {id: 'desert', name: 'Desert Campaign', levels: desert } +] From a742772b8f3f12dead6fae0feb2c498bd6bfe735 Mon Sep 17 00:00:00 2001 From: Scott Erickson <sderickson@gmail.com> Date: Fri, 19 Dec 2014 13:06:20 -0500 Subject: [PATCH 05/15] Also added campaign view jade and sass file clones. --- app/styles/play/campaign-view.sass | 495 ++++++++++++++++++++++++++ app/templates/play/campaign-view.jade | 106 ++++++ app/views/play/CampaignView.coffee | 5 +- 3 files changed, 604 insertions(+), 2 deletions(-) create mode 100644 app/styles/play/campaign-view.sass create mode 100644 app/templates/play/campaign-view.jade diff --git a/app/styles/play/campaign-view.sass b/app/styles/play/campaign-view.sass new file mode 100644 index 000000000..802206b8a --- /dev/null +++ b/app/styles/play/campaign-view.sass @@ -0,0 +1,495 @@ +@import "app/styles/mixins" +@import "app/styles/bootstrap/variables" + +$mapHeight: 1536 +$forestMapWidth: 2500 +$dungeonMapWidth: 2350 +$desertMapWidth: 2350 +$desertMapSeaBackground: rgba(113, 186, 208, 1) +$desertMapSeaBackgroundTransparent: rgba(113, 186, 208, 0) +$forestMapSeaBackground: rgba(113, 186, 208, 1) +$forestMapSeaBackgroundTransparent: rgba(113, 186, 208, 0) +$dungeonMapCaveBackground: rgba(68, 54, 45, 1) +$dungeonMapCaveBackgroundTransparent: rgba(68, 54, 45, 0) +$levelDotWidth: 2% +$levelDotHeight: $levelDotWidth * $forestMapWidth / $mapHeight +$levelDotZ: $levelDotHeight * 0.25 +$levelDotHoverZ: $levelDotZ * 2 +$levelDotShadowWidth: 0.8 * $levelDotWidth +$levelDotShadowHeight: 0.8 * $levelDotHeight +$levelClickRadius: 40px +$gameControlSize: 80px +$gameControlMargin: 30px + ++keyframes(levelStartedPulse) + from + @include box-shadow(0px 0px 4px #333) + margin-bottom: -$levelDotHeight / 3 + $levelDotZ + 50% + @include box-shadow(0px 0px 22px skyblue) + margin-bottom: -$levelDotHeight / 3 + ($levelDotHoverZ + $levelDotZ) / 2 + to + @include box-shadow(0px 0px 4px #333) + margin-bottom: -$levelDotHeight / 3 + $levelDotZ + +#campaign-view + width: 100% + height: 100% + position: absolute + + .gradient + position: absolute + z-index: 0 + + &.horizontal-gradient + left: 0 + right: 0 + height: 3% + + &.vertical-gradient + top: 0 + bottom: 0 + width: 3% + + &.top-gradient + top: 0 + + &.right-gradient + right: 0 + + &.bottom-gradient + bottom: 0 + + &.left-gradient + left: 0 + + &.desert + background-color: $desertMapSeaBackground + + .top-gradient + background: linear-gradient(to bottom, $desertMapSeaBackground 0%, $desertMapSeaBackgroundTransparent 100%) + + .right-gradient + background: linear-gradient(to left, $desertMapSeaBackground 0%, $desertMapSeaBackgroundTransparent 100%) + + .bottom-gradient + background: linear-gradient(to top, $desertMapSeaBackground 0%, $desertMapSeaBackgroundTransparent 100%) + + .left-gradient + background: linear-gradient(to right, $desertMapSeaBackground 0%, $desertMapSeaBackgroundTransparent 100%) + + &.forest + background-color: $forestMapSeaBackground + + .top-gradient + background: linear-gradient(to bottom, $forestMapSeaBackground 0%, $forestMapSeaBackgroundTransparent 100%) + + .right-gradient + background: linear-gradient(to left, $forestMapSeaBackground 0%, $forestMapSeaBackgroundTransparent 100%) + + .bottom-gradient + background: linear-gradient(to top, $forestMapSeaBackground 0%, $forestMapSeaBackgroundTransparent 100%) + + .left-gradient + background: linear-gradient(to right, $forestMapSeaBackground 0%, $forestMapSeaBackgroundTransparent 100%) + + &.dungeon + background-color: $dungeonMapCaveBackground + + .top-gradient + background: linear-gradient(to bottom, $dungeonMapCaveBackground 0%, $dungeonMapCaveBackgroundTransparent 100%) + + .right-gradient + background: linear-gradient(to left, $dungeonMapCaveBackground 0%, $dungeonMapCaveBackgroundTransparent 100%) + + .bottom-gradient + background: linear-gradient(to top, $dungeonMapCaveBackground 0%, $dungeonMapCaveBackgroundTransparent 100%) + + .left-gradient + background: linear-gradient(to right, $dungeonMapCaveBackground 0%, $dungeonMapCaveBackgroundTransparent 100%) + + .map + position: relative + + .map-background + width: 100% + height: 100% + background-size: 100% + @include user-select(none) + + &.map-dungeon + background-image: url('/images/pages/play/map_dungeon_1920.jpg') + @media screen and ( max-width: 1366px ) + background-image: url('/images/pages/play/map_dungeon_1366.jpg') + + &.map-forest + background-image: url('/images/pages/play/map_forest_1920.jpg') + @media screen and ( max-width: 1366px ) + background-image: url('/images/pages/play/map_forest_1366.jpg') + + &.map-desert + background-image: url('/images/pages/play/map_desert_1920.jpg') + @media screen and ( max-width: 1366px ) + background-image: url('/images/pages/play/map_desert_1366.jpg') + + .level, .level-shadow + position: absolute + border-radius: 100% + -webkit-transform: scaleY(0.75) + transform: scaleY(0.75) + + .level + z-index: 2 + width: $levelDotWidth + height: $levelDotHeight + margin-left: -0.5 * $levelDotWidth + margin-bottom: -$levelDotHeight / 3 + $levelDotZ + border: 2px groove white + @include transition(margin-bottom 0.5s ease) + + &.disabled, &.locked + background-image: url(/images/pages/game-menu/lock.png) + background-size: 75% + background-repeat: no-repeat + background-position: 50% 50% + opacity: 0.7 + + a + cursor: default + + &.next + width: 2 * $levelDotWidth + height: 2 * $levelDotHeight + margin-left: -0.5 * 2 * $levelDotWidth + margin-bottom: -2 * $levelDotHeight / 3 + 2 * $levelDotZ + + &.started, &.next + border: 3px solid lightgreen + @include box-shadow(0px 0px 35px skyblue) + + // Would be cool, but kills performance, since we have to re-render all the time. + //&:not(:hover) + // -webkit-animation-name: levelStartedPulse + // -webkit-animation-duration: 3s + // -webkit-animation-iteration-count: infinite + + &.complete + border: 3px solid gold + @include box-shadow(0px 0px 35px skyblue) + + img.banner + position: absolute + bottom: 38% + left: -50% + width: 200% + pointer-events: none + + img.star + width: 100% + bottom: 7% + position: absolute + pointer-events: none + + .glyphicon-star + position: absolute + color: lightblue + font-size: 21px + left: 1.5px + + &.started .glyphicon-star + left: 0.5px + + img.hero-portrait + width: 120% + position: absolute + bottom: 75% + left: 75% + border: 1px solid black + border-radius: 100% + background: white + + + .level-shadow + z-index: 1 + width: $levelDotShadowWidth + height: $levelDotShadowHeight + margin-left: -0.5 * $levelDotShadowWidth + margin-bottom: -$levelDotShadowHeight / 3 + background-color: black + @include box-shadow(0px 0px 10px black) + @include opacity(0.75) + + &.next + width: 2 * $levelDotShadowWidth + height: 2 * $levelDotShadowHeight + margin-left: -0.5 * 2 * $levelDotShadowWidth + margin-bottom: -2 * $levelDotShadowHeight / 3 + + .level:hover + // TODO: This rotate stops Firefox from flickering, but also disables the scaleY(0.75) + // TODO: The dot looks like it's jumping. + // TODO: -moz-transform: scaleY(0.75) didn't do anything + // TODO: Does not break Chrome's oval. + -moz-transform: rotate(0) + margin-bottom: -$levelDotHeight / 3 + $levelDotHoverZ + @include box-shadow(0px 0px 35px skyblue) + + &.next + margin-bottom: -2 * $levelDotHeight / 3 + 2 * $levelDotHoverZ + + .level + a + display: block + padding: $levelClickRadius + margin-left: -0.5 * $levelClickRadius + margin-top: -0.5 * $levelClickRadius + border-radius: $levelClickRadius + + &.next a + padding: 2 * $levelClickRadius + margin-left: 2 * -0.5 * $levelClickRadius + margin-top: 2 * -0.5 * $levelClickRadius + border-radius: 2 * $levelClickRadius + + .tooltip + z-index: 2 + + .level-info-container + display: none + position: absolute + z-index: 3 + padding: 10px + border-width: 16px 12px + // Using modernizr-mixin for compat detection + @include yep(borderimage) + border-style: solid + border-image: url(/images/level/popover_border_background.png) 16 12 fill round + @include nope(borderimage) + background-color: rgb(247, 242, 218) + + .level-info.complete h3:after + content: " - Complete!" + color: green + + .level-info.started h3:after + content: " - Started" + color: desaturate(green, 50%) + + .level-info + h3 + margin-top: 0 + margin-bottom: 0px + + .level-description + color: black + text-shadow: 0 1px 0 white + + .campaign-label + text-shadow: 0 1px 0 white + + .start-level + display: block + margin: 10px auto 0 auto + width: 200px + + .campaign-switch + color: purple + position: absolute + z-index: 1 + font-size: 2vw + text-shadow: 0 0 0.3vw white, 0 0 0.3vw white + + &:hover + text-decoration: none + + &#desert-link + left: 90% + top: 18.5% + transform: scaleY(-1.5) scaleX(1.5) + + &#forest-back-link + left: 2% + top: 70.5% + transform: rotate(216deg) + + &#forest-link + left: 94.5% + top: 7% + transform: rotate(-35deg) + + &#dungeon-link + left: 9% + top: 54.5% + transform: rotate(180deg) + color: fuchsia + + .game-controls + position: absolute + right: 1% + bottom: 1% + z-index: 3 + + .btn + &:not(:first-child) + margin-left: $gameControlMargin + width: $gameControlSize + height: $gameControlSize + + background: url(/images/pages/play/menu_icons.png) no-repeat + + position: relative + img + position: absolute + left: 0 + top: 0 + width: 100% + height: 100% + + background-size: cover + @include transition(0.5s ease) + @include box-shadow(2px 2px 4px black) + border: 0 + border-radius: 12px + // IE9 shows a blank white button with this MS gradient filter in place + filter: none + + &:hover + @include box-shadow(0 0 12px #bbf) + + &:active + @include box-shadow(0 0 20px white) + + &.heroes + background-position: (-1 * $gameControlSize) 0px + &.achievements + background-position: (-2 * $gameControlSize) 0px + &.account + background-position: (-3 * $gameControlSize) 0px + &.settings + background-position: (-4 * $gameControlSize) 0px + &.gems + background-position: (-5 * $gameControlSize) 0px + + .tooltip + font-size: 24px + + .tooltip-arrow + display: none + + .user-status + position: absolute + bottom: 16px + left: 8px + text-align: center + font-size: 24px + color: white + text-shadow: 0px 2px 1px black, 0px -2px 1px black, -2px 0px 1px black, 2px 0px 1px black + height: 32px + line-height: 32px + + .user-status-line + position: relative + + button.btn.btn-illustrated + margin-left: 10px + min-width: 90px + height: 32px + color: white + + .gem, .player-level-icon, .player-hero-icon + position: absolute + top: 1px + + #gems-count + margin-left: 40px + + .player-level + margin-left: 34px + + .player-name + margin-left: 45px + + $spriteSheetSize: 30px + + .player-level-icon, .player-hero-icon + background: transparent url(/images/pages/play/play-spritesheet.png) + background-size: cover + background-position: (-2 * $spriteSheetSize) 0 + display: inline-block + width: 30px + height: 30px + margin: 0px 2px + + .player-hero-icon + margin-left: 10px + background-position: (-4 * $spriteSheetSize) 0 + + &.knight + background-position: (-5 * $spriteSheetSize) 0 + &.librarian + background-position: (-6 * $spriteSheetSize) 0 + &.ninja + background-position: (-7 * $spriteSheetSize) 0 + &.potion-master + background-position: (-8 * $spriteSheetSize) 0 + &.samurai + background-position: (-9 * $spriteSheetSize) 0 + &.trapper + background-position: (-10 * $spriteSheetSize) 0 + &.forest-archer + background-position: (-11 * $spriteSheetSize) 0 + &.sorcerer + background-position: (-12 * $spriteSheetSize) 0 + + + #volume-button + position: absolute + left: 1% + top: 1% + padding: 3px 8px + @include opacity(0.75) + + &:hover + @include opacity(1.0) + + .glyphicon + display: none + font-size: 32px + + &.vol-up .glyphicon.glyphicon-volume-up + display: inline-block + + &.vol-off .glyphicon.glyphicon-volume-off + display: inline-block + @include opacity(0.50) + &:hover + @include opacity(0.75) + + &.vol-down .glyphicon.glyphicon-volume-down + display: inline-block + + #campaign-status + position: absolute + left: 0 + top: 15px + width: 100% + margin: 0 + text-align: center + color: rgb(254,188,68) + font-size: 30px + text-shadow: black 2px 2px 0, black -2px -2px 0, black 2px -2px 0, black -2px 2px 0, black 2px 0px 0, black 0px -2px 0, black -2px 0px 0, black 0px 2px 0 + + +body:not(.ipad) #campaign-view + .level-info-container + pointer-events: none + + + +body.ipad #campaign-view + // iPad only supports up to Kithgard Gates for now. + .campaign-switch + display: none + + .old-levels + display: none diff --git a/app/templates/play/campaign-view.jade b/app/templates/play/campaign-view.jade new file mode 100644 index 000000000..2a88a63d9 --- /dev/null +++ b/app/templates/play/campaign-view.jade @@ -0,0 +1,106 @@ +.map + .gradient.horizontal-gradient.top-gradient + .gradient.vertical-gradient.right-gradient + .gradient.horizontal-gradient.bottom-gradient + .gradient.vertical-gradient.left-gradient + .map-background(class="map-"+mapType alt="", draggable="false") + + - var seenNext = nextLevel; + each level in campaign.levels + if !level.hidden + - var next = level.id == nextLevel || (!seenNext && levelStatusMap[level.id] != "complete" && !level.locked && !level.disabled && !editorMode); + - seenNext = seenNext || next; + div(style="left: #{level.x}%; bottom: #{level.y}%; background-color: #{level.color}", class="level" + (next ? " next" : "") + (level.disabled ? " disabled" : "") + (level.locked ? " locked" : "") + " " + levelStatusMap[level.id] || "", data-level-id=level.id, title=level.name + (level.disabled ? ' (Coming Soon to Adventurers)' : '')) + if level.unlocksHero && !level.unlockedHero + img.hero-portrait(src=level.unlocksHero.img) + a(href=level.type == 'hero' ? '#' : level.disabled ? "/play" : "/play/#{level.levelPath || 'level'}/#{level.id}", disabled=level.disabled, data-level-id=level.id, data-level-path=level.levelPath || 'level', data-level-name=level.name) + if level.requiresSubscription + img.star(src="/images/pages/play/star.png") + if levelStatusMap[level.id] === 'complete' + img.banner(src="/images/pages/play/level-banner-complete.png") + if levelStatusMap[level.id] === 'started' + img.banner(src="/images/pages/play/level-banner-started.png") + div(style="left: #{level.x}%; bottom: #{level.y}%", class="level-shadow" + (next ? " next" : "") + " " + levelStatusMap[level.id] || "") + .level-info-container(data-level-id=level.id, data-level-path=level.levelPath || 'level', data-level-name=level.name) + div(class="level-info " + (levelStatusMap[level.id] || "")) + h3= level.name + (level.disabled ? " (Coming soon!)" : (level.locked ? " (Locked)" : "")) + .level-description= level.description + if level.disabled + p + span.spr(data-i18n="play.awaiting_levels_adventurer_prefix") We release five levels per week. + a.spr(href="/contribute/adventurer") + strong(data-i18n="play.awaiting_levels_adventurer") Sign up as an Adventurer + span.spl(data-i18n="play.awaiting_levels_adventurer_suffix") to be the first to play new levels. + + - var playCount = levelPlayCountMap[level.id] + if playCount && playCount.sessions > 20 + div + span.spr #{playCount.sessions} + span(data-i18n="play.players") players + span.spr , #{Math.round(playCount.playtime / 3600)} + span(data-i18n="play.hours_played") hours played + .campaign-label(style="color: #{campaign.color}")= campaign.name + if isIPadApp && !level.disabled && !level.locked + button.btn.btn-success.btn-lg.start-level(data-i18n="common.play") Play + if mapType === 'dungeon' && forestIsAvailable + a#forest-link.glyphicon.glyphicon-share-alt.campaign-switch(href="/play/forest", data-i18n="[title]play.campaign_forest") + if mapType === 'forest' + a#dungeon-link.glyphicon.glyphicon-share-alt.campaign-switch(href="/play/dungeon", data-i18n="[title]play.campaign_dungeon") + if desertIsAvailable + a#desert-link.glyphicon.glyphicon-share-alt.campaign-switch(href="/play/desert", data-i18n="[title]play.campaign_desert") + if mapType === 'desert' + a#forest-back-link.glyphicon.glyphicon-share-alt.campaign-switch(href="/play/forest", data-i18n="[title]play.campaign_forest") + + +.game-controls.header-font + button.btn.items(data-toggle='coco-modal', data-target='play/modal/PlayItemsModal', data-i18n="[title]play.items") + button.btn.heroes(data-toggle='coco-modal', data-target='play/modal/PlayHeroesModal', data-i18n="[title]play.heroes") + button.btn.achievements(data-toggle='coco-modal', data-target='play/modal/PlayAchievementsModal', data-i18n="[title]play.achievements") + if me.get('anonymous') === false || me.get('iosIdentifierForVendor') || isIPadApp + button.btn.gems(data-toggle='coco-modal', data-target='play/modal/BuyGemsModal', data-i18n="[title]play.buy_gems") + if me.isAdmin() + button.btn.account(data-toggle='coco-modal', data-target='play/modal/PlayAccountModal', data-i18n="[title]play.account") + button.btn.settings(data-toggle='coco-modal', data-target='play/modal/PlaySettingsModal', data-i18n="[title]play.settings") + else if me.get('anonymous', true) + button.btn.settings(data-toggle='coco-modal', data-target='core/AuthModal', data-i18n="[title]play.settings") + // Don't show these things, they are bad and take us out of the game. Just wait until the new ones work. + //else + // a.btn.achievements(href="/user/#{me.getSlugOrID()}/stats", data-i18n="[title]play.achievements") + // a.btn.account(href="/user/#{me.getSlugOrID()}", data-i18n="[title]play.account") + // a.btn.settings(href='/account', data-i18n="[title]play.settings") + +.user-status.header-font + .user-status-line + span.gem.gem-30 + span#gems-count.spr= me.gems() + span.player-level-icon + span.player-level.spr= me.level() + span.player-hero-icon + if me.get('anonymous') + span.player-name.spr(data-i18n="play.anonymous") Anonymous Player + button.btn.btn-illustrated.login-button.btn-warning(data-i18n="login.log_in") + button.btn.btn-illustrated.signup-button.btn-danger(data-i18n="signup.sign_up") + else + span.player-name.spr= me.get('name') + button#logout-button.btn.btn-illustrated.btn-warning(data-i18n="login.log_out") Log Out + if me.isPremium() + button.btn.btn-illustrated.btn-primary(data-i18n="nav.contact", data-toggle="coco-modal", data-target="core/ContactModal") Contact + + +button.btn.btn-lg.btn-inverse#volume-button(title="Adjust volume") + .glyphicon.glyphicon-volume-off + .glyphicon.glyphicon-volume-down + .glyphicon.glyphicon-volume-up + +//h1#campaign-status +// if mapType == 'dungeon' +// span.spr(data-i18n="play.campaign_dungeon") +// else if mapType == 'forest' +// span.spr(data-i18n="play.campaign_forest") +// | - +// if requiresSubscription +// span.spl(data-i18n="play.subscription_required") +// else if mapType == 'dungeon' +// span.spl(data-i18n="play.free") +// else +// span.spl(data-i18n="play.subscribed") \ No newline at end of file diff --git a/app/views/play/CampaignView.coffee b/app/views/play/CampaignView.coffee index fc3fdb910..24a633d25 100644 --- a/app/views/play/CampaignView.coffee +++ b/app/views/play/CampaignView.coffee @@ -1,5 +1,5 @@ RootView = require 'views/core/RootView' -template = require 'templates/play/world-map-view' +template = require 'templates/play/campaign-view' LevelSession = require 'models/LevelSession' EarnedAchievement = require 'models/EarnedAchievement' CocoCollection = require 'collections/CocoCollection' @@ -23,7 +23,7 @@ class LevelSessionsCollection extends CocoCollection @url = "/db/user/#{me.id}/level.sessions?project=state.complete,levelID" module.exports = class WorldMapView extends RootView - id: 'world-map-view' + id: 'campaign-view' template: template subscriptions: @@ -103,6 +103,7 @@ module.exports = class WorldMapView extends RootView super() getLevelPlayCounts: -> + return # TODO: Either use the campaign object instead of hardcoded data or get the data some other way return unless me.isAdmin() success = (levelPlayCounts) => return if @destroyed From 0cd85d7aba854fe8df9e777836a3834b7aca40f9 Mon Sep 17 00:00:00 2001 From: Scott Erickson <sderickson@gmail.com> Date: Fri, 19 Dec 2014 16:46:01 -0500 Subject: [PATCH 06/15] Got the CampaignView mostly off the hardcoded data. --- app/core/Router.coffee | 2 +- app/models/Campaign.coffee | 1 + app/schemas/models/campaign.schema.coffee | 15 +- app/templates/play/campaign-view.jade | 10 +- .../editor/campaign/CampaignEditorView.coffee | 39 + app/views/play/CampaignView.coffee | 824 +----------------- app/views/play/WorldMapView.coffee | 1 + 7 files changed, 68 insertions(+), 824 deletions(-) diff --git a/app/core/Router.coffee b/app/core/Router.coffee index daad3d754..7cffebca8 100644 --- a/app/core/Router.coffee +++ b/app/core/Router.coffee @@ -85,7 +85,7 @@ module.exports = class CocoRouter extends Backbone.Router 'multiplayer': go('MultiplayerView') 'play-old': go('play/MainPlayView') # This used to be 'play'. - 'play': go('play/WorldMapView') + 'play': go('play/CampaignView') 'play/ladder/:levelID': go('ladder/LadderView') 'play/ladder': go('ladder/MainLadderView') 'play/level/:levelID': go('play/level/PlayLevelView') diff --git a/app/models/Campaign.coffee b/app/models/Campaign.coffee index 7473d2eca..55866cb3c 100644 --- a/app/models/Campaign.coffee +++ b/app/models/Campaign.coffee @@ -7,3 +7,4 @@ module.exports = class Campaign extends CocoModel urlRoot: '/db/campaign' saveBackups: true @denormalizedLevelProperties: _.keys(_.omit(schema.properties.levels.additionalProperties.properties, ['unlocks', 'position'])) + @denormalizedCampaignProperties: ['name', 'i18n', 'description', 'slug'] diff --git a/app/schemas/models/campaign.schema.coffee b/app/schemas/models/campaign.schema.coffee index 8820a1189..23dc3e418 100644 --- a/app/schemas/models/campaign.schema.coffee +++ b/app/schemas/models/campaign.schema.coffee @@ -21,18 +21,23 @@ _.extend CampaignSchema.properties, { backgroundColor: { type: 'string' } backgroundColorTransparent: { type: 'string' } - adjacentCampaigns: { type: 'object', additionalItems: { + adjacentCampaigns: { type: 'object', format: 'campaigns', additionalProperties: { title: 'Campaign' type: 'object' + format: 'campaign' properties: { #- denormalized from other Campaigns, either updated automatically or fetched dynamically - name: { type: 'string' } - i18n: { type: 'object' } + name: { type: 'string', format: 'hidden' } + description: { type: 'string', format: 'hidden' } + i18n: { type: 'object', format: 'hidden' } + original: { type: 'string', format: 'hidden' } + slug: { type: 'string', format: 'hidden' } #- normal properties position: c.point2d() rotation: { type: 'number', format: 'degrees' } color: { type: 'string' } + showIfUnlocked: { type: 'string', links: [{rel: 'db', href: '/db/level/{($)}/version'}], format: 'latest-version-original-reference' } } }} @@ -45,7 +50,6 @@ _.extend CampaignSchema.properties, { # key is the original property properties: { #- denormalized from Level - # TODO: take these properties from the Level schema and put them into schema references, use them here name: { type: 'string', format: 'hidden' } description: { type: 'string', format: 'hidden' } requiresSubscription: { type: 'boolean' } @@ -54,8 +58,6 @@ _.extend CampaignSchema.properties, { original: { type: 'string', format: 'hidden' } adventurer: { type: 'boolean' } practice: { type: 'boolean' } - - # TODO: add these to the level, as well as the 'campaign' property disableSpaces: { type: 'boolean' } hidesSubmitUntilRun: { type: 'boolean' } hidesPlayButton: { type: 'boolean' } @@ -67,7 +69,6 @@ _.extend CampaignSchema.properties, { backspaceThrottle: { type: 'boolean' } lockDefaultCode: { type: 'boolean' } moveRightLoopSnippet: { type: 'boolean' } - realTimeSpeedFactor: { type: 'number' } autocompleteFontSizePx: { type: 'number' } diff --git a/app/templates/play/campaign-view.jade b/app/templates/play/campaign-view.jade index 2a88a63d9..6abd410b6 100644 --- a/app/templates/play/campaign-view.jade +++ b/app/templates/play/campaign-view.jade @@ -5,12 +5,10 @@ .gradient.vertical-gradient.left-gradient .map-background(class="map-"+mapType alt="", draggable="false") - - var seenNext = nextLevel; - each level in campaign.levels + each level in levels if !level.hidden - - var next = level.id == nextLevel || (!seenNext && levelStatusMap[level.id] != "complete" && !level.locked && !level.disabled && !editorMode); - - seenNext = seenNext || next; - div(style="left: #{level.x}%; bottom: #{level.y}%; background-color: #{level.color}", class="level" + (next ? " next" : "") + (level.disabled ? " disabled" : "") + (level.locked ? " locked" : "") + " " + levelStatusMap[level.id] || "", data-level-id=level.id, title=level.name + (level.disabled ? ' (Coming Soon to Adventurers)' : '')) + - var next = nextLevel && level.slug === nextLevel; + div(style="left: #{level.position.x}%; bottom: #{level.position.y}%; background-color: #{level.color}", class="level" + (next ? " next" : "") + (level.disabled ? " disabled" : "") + (level.locked ? " locked" : "") + " " + levelStatusMap[level.id] || "", data-level-id=level.id, title=level.name + (level.disabled ? ' (Coming Soon to Adventurers)' : '')) if level.unlocksHero && !level.unlockedHero img.hero-portrait(src=level.unlocksHero.img) a(href=level.type == 'hero' ? '#' : level.disabled ? "/play" : "/play/#{level.levelPath || 'level'}/#{level.id}", disabled=level.disabled, data-level-id=level.id, data-level-path=level.levelPath || 'level', data-level-name=level.name) @@ -39,7 +37,7 @@ span(data-i18n="play.players") players span.spr , #{Math.round(playCount.playtime / 3600)} span(data-i18n="play.hours_played") hours played - .campaign-label(style="color: #{campaign.color}")= campaign.name + .campaign-label= campaign.get('name') if isIPadApp && !level.disabled && !level.locked button.btn.btn-success.btn-lg.start-level(data-i18n="common.play") Play if mapType === 'dungeon' && forestIsAvailable diff --git a/app/views/editor/campaign/CampaignEditorView.coffee b/app/views/editor/campaign/CampaignEditorView.coffee index 882fa748f..40e97446c 100644 --- a/app/views/editor/campaign/CampaignEditorView.coffee +++ b/app/views/editor/campaign/CampaignEditorView.coffee @@ -169,6 +169,8 @@ module.exports = class CampaignEditorView extends RootView nodeClasses: levels: LevelsNode level: LevelNode + campaigns: CampaignsNode + campaign: CampaignNode achievement: AchievementNode supermodel: @supermodel @@ -239,9 +241,46 @@ class LevelNode extends TreemaObjectNode return if @data.name? data = _.pick LevelsNode.levels[@keyForParent].attributes, Campaign.denormalizedLevelProperties _.extend @data, data + +class CampaignsNode extends TreemaObjectNode + valueClass: 'treema-campaigns' + @campaigns: {} + + buildValueForDisplay: (valEl, data) -> + @buildValueForDisplaySimply valEl, ''+_.size(data) + + childPropertiesAvailable: -> @childSource + + childSource: (req, res) => + s = new Backbone.Collection([], {model:Campaign}) + s.url = '/db/campaign' + s.fetch({data: {term:req.term, project: campaign.denormalizedCampaignProperties}}) + s.once 'sync', (collection) -> + CampaignsNode.campaigns[campaign.id] = campaign for campaign in collection.models + mapped = ({label: r.get('name'), value: r.id} for r in collection.models) + console.log 'campaigns is now', CampaignsNode.campaigns + res(mapped) + + +class CampaignNode extends TreemaObjectNode + valueClass: 'treema-campaign' + buildValueForDisplay: (valEl, data) -> + console.log 'build value for display?', data + @buildValueForDisplaySimply valEl, data.name + + populateData: -> + return if @data.name? + console.log 'key for parent', @keyForParent, CampaignsNode.campaigns[@keyForParent].attributes, Campaign.denormalizedCampaignProperties + data = _.pick CampaignsNode.campaigns[@keyForParent].attributes, Campaign.denormalizedCampaignProperties + console.log 'data?', data + _.extend @data, data + console.log 'extended data', @data class AchievementNode extends treemaExt.IDReferenceNode buildSearchURL: (term) -> "#{@url}?term=#{term}&project=#{achievementProject.join(',')}" + + + campaign = { name: 'Dungeon' diff --git a/app/views/play/CampaignView.coffee b/app/views/play/CampaignView.coffee index 24a633d25..c2a5bf114 100644 --- a/app/views/play/CampaignView.coffee +++ b/app/views/play/CampaignView.coffee @@ -3,6 +3,7 @@ template = require 'templates/play/campaign-view' LevelSession = require 'models/LevelSession' EarnedAchievement = require 'models/EarnedAchievement' CocoCollection = require 'collections/CocoCollection' +Campaign = require 'models/Campaign' AudioPlayer = require 'lib/AudioPlayer' LevelSetupManager = require 'lib/LevelSetupManager' ThangType = require 'models/ThangType' @@ -38,14 +39,13 @@ module.exports = class WorldMapView extends RootView 'mousemove .map': 'onMouseMoveMap' 'click #volume-button': 'onToggleVolume' - constructor: (options, @terrain) -> + constructor: (options, @terrain='dungeon') -> if options and application.isIPAdApp # TODO: later only clear the SuperModel if it has received a memory warning (not in app store yet) options.supermodel = null - @terrain ?= 'dungeon' # or 'forest', 'desert' super options options ?= {} - @campaign = new Campaign({_id:campaignHandle}) + @campaign = new Campaign({_id:@terrain}) @supermodel.loadModel(@campaign, 'campaign') @editorMode = options.editorMode @@ -58,7 +58,6 @@ module.exports = class WorldMapView extends RootView @earnedAchievements = new CocoCollection([], {url: '/db/earned_achievement', model:EarnedAchievement, project: ['earnedRewards']}) @listenToOnce @earnedAchievements, 'sync', -> earned = me.get('earned') - addedSomething = false for m in @earnedAchievements.models continue unless loadedEarned = m.get('earnedRewards') for group in ['heroes', 'levels', 'items'] @@ -67,7 +66,7 @@ module.exports = class WorldMapView extends RootView if reward not in earned[group] console.warn 'Filling in a gap for reward', group, reward earned[group].push(reward) - addedSomething = true + @supermodel.loadCollection(@earnedAchievements, 'achievements') @listenToOnce @sessions, 'sync', @onSessionsLoaded @@ -135,10 +134,10 @@ module.exports = class WorldMapView extends RootView getRenderData: (context={}) -> context = super(context) - context.campaign = _.find campaigns, { id: @terrain } - for level in context.campaign.levels - level.x ?= 10 + 80 * Math.random() - level.y ?= 10 + 80 * Math.random() + context.campaign = @campaign + context.levels = _.values($.extend {}, @campaign.get('levels')) + for level in context.levels + level.position ?= { x: 10, y: 10 } level.locked = not me.ownsLevel level.original window.levelUnlocksNotWorking = true if level.locked and level.id is @nextLevel # Temporary level.locked = false if window.levelUnlocksNotWorking # Temporary; also possible in HeroVictoryModal @@ -153,18 +152,16 @@ module.exports = class WorldMapView extends RootView level.unlockedHero = level.unlocksHero.originalID in (me.get('earned')?.heroes or []) level.hidden = level.locked or level.disabled - ## put lower levels in last, so in the world map they layer over one another properly. - #context.campaign.levels = (_.sortBy context.campaign.levels, 'y').reverse() - # Actually, there's some logic that depends on the order of iteration of levels to determine - # which one to do next when you're coming here not from a level; can we do this another way? + # put lower levels in last, so in the world map they layer over one another properly. + context.campaign.levels = (_.sortBy context.campaign.levels, (l) -> l.position.y).reverse() context.levelStatusMap = @levelStatusMap context.levelPlayCountMap = @levelPlayCountMap context.isIPadApp = application.isIPadApp context.mapType = _.string.slugify @terrain context.nextLevel = @nextLevel - context.forestIsAvailable = @startedForestLevel or (Level.levels['defense-of-plainswood'] in (me.get('earned')?.levels or [])) - context.desertIsAvailable = @startedDesertLevel or (Level.levels['the-mighty-sand-yak'] in (me.get('earned')?.levels or [])) + context.forestIsAvailable = Level.levels['defense-of-plainswood'] in (me.get('earned')?.levels or []) + context.desertIsAvailable = Level.levels['the-mighty-sand-yak'] in (me.get('earned')?.levels or []) context.requiresSubscription = @requiresSubscription context.editorMode = @editorMode context @@ -202,16 +199,9 @@ module.exports = class WorldMapView extends RootView @openModalView authModal onSessionsLoaded: (e) -> - if @editorMode - @startedForestLevel = true - @startedDesertLevel = true - return - forestLevels = (f.id for f in forest) - desertLevels = (f.id for f in desert) + return if @editorMode for session in @sessions.models @levelStatusMap[session.get('levelID')] = if session.get('state')?.complete then 'complete' else 'started' - @startedForestLevel = true if session.get('levelID') in forestLevels - @startedDesertLevel = true if session.get('levelID') in desertLevels if @nextLevel and @levelStatusMap[@nextLevel] is 'complete' @nextLevel = null @render() @@ -229,8 +219,7 @@ module.exports = class WorldMapView extends RootView @$levelInfo?.hide() levelElement = $(e.target).parents('.level') levelID = levelElement.data('level-id') - campaign = _.find campaigns, id: @terrain - level = _.find campaign.levels, id: levelID + level = _.find _.values(campaign.get('levels')), id: levelID if application.isIPadApp @$levelInfo = @$el.find(".level-info-container[data-level-id=#{levelID}]").show() @adjustLevelInfoPosition e @@ -372,788 +361,3 @@ module.exports = class WorldMapView extends RootView @$el.find('.player-hero-icon').removeClass().addClass('player-hero-icon ' + slug) return console.error "WorldMapView hero update couldn't find hero slug for original:", hero - -dungeon = [ - { - name: 'Dungeons of Kithgard' - type: 'hero' - id: 'dungeons-of-kithgard' - original: '5411cb3769152f1707be029c' - description: 'Grab the gem, but touch nothing else. Start here.' - x: 14 - y: 15.5 - nextLevels: - continue: 'gems-in-the-deep' - } - { - name: 'Gems in the Deep' - type: 'hero' - id: 'gems-in-the-deep' - original: '54173c90844506ae0195a0b4' - description: 'Quickly collect the gems; you will need them.' - x: 29 - y: 12 - nextLevels: - continue: 'shadow-guard' - } - { - name: 'Shadow Guard' - type: 'hero' - id: 'shadow-guard' - original: '54174347844506ae0195a0b8' - description: 'Evade the Kithgard minion.' - x: 40.54 - y: 11.03 - nextLevels: - continue: 'forgetful-gemsmith' - } - { - name: 'Kounter Kithwise' - type: 'hero' - id: 'kounter-kithwise' - original: '54527a6257e83800009730c7' - description: 'Practice your evasion skills with more guards.' - x: 35.37 - y: 20.61 - nextLevels: - continue: 'crawlways-of-kithgard' - practice: true - requiresSubscription: true - } - { - name: 'Crawlways of Kithgard' - type: 'hero' - id: 'crawlways-of-kithgard' - original: '545287ef57e83800009730d5' - description: 'Dart in and grab the gem–at the right moment.' - x: 36.48 - y: 29.03 - nextLevels: - continue: 'forgetful-gemsmith' - practice: true - requiresSubscription: true - } - { - name: 'Forgetful Gemsmith' - type: 'hero' - id: 'forgetful-gemsmith' - original: '544a98f62d002f0000fe331a' - description: 'Grab even more gems as you practice moving.' - x: 54.98 - y: 10.53 - nextLevels: - continue: 'true-names' - } - { - name: 'True Names' - type: 'hero' - id: 'true-names' - original: '541875da4c16460000ab990f' - description: 'Learn an enemy\'s true name to defeat it.' - x: 68.44 - y: 10.70 - nextLevels: - continue: 'the-raised-sword' - unlocksHero: { - img: '/file/db/thang.type/53e12be0d042f23505c3023b/portrait.png' - originalID: '53e12be0d042f23505c3023b' - } - } - { - name: 'Favorable Odds' - type: 'hero' - id: 'favorable-odds' - original: '5452972f57e83800009730de' - description: 'Test out your battle skills by defeating more munchkins.' - x: 88.25 - y: 14.92 - nextLevels: - continue: 'the-raised-sword' - practice: true - requiresSubscription: true - } - { - name: 'The Raised Sword' - type: 'hero' - id: 'the-raised-sword' - original: '5418aec24c16460000ab9aa6' - description: 'Learn to equip yourself for combat.' - x: 81.51 - y: 17.92 - nextLevels: - continue: 'haunted-kithmaze' - } - { - name: 'Haunted Kithmaze' - type: 'hero' - id: 'haunted-kithmaze' - original: '545a5914d820eb0000f6dc0a' - description: 'The builders of Kithgard constructed many mazes to confuse travelers.' - x: 78 - y: 29 - nextLevels: - continue: 'the-second-kithmaze' - } - { - name: 'Riddling Kithmaze' - type: 'hero' - id: 'riddling-kithmaze' - original: '5418b9d64c16460000ab9ab4' - description: 'If at first you go astray, change your loop to find the way.' - x: 69.97 - y: 28.03 - nextLevels: - continue: 'descending-further' - practice: true - requiresSubscription: true - } - { - name: 'Descending Further' - type: 'hero' - id: 'descending-further' - original: '5452a84d57e83800009730e4' - description: 'Another day, another maze.' - x: 61.68 - y: 22.80 - nextLevels: - continue: 'the-second-kithmaze' - practice: true - requiresSubscription: true - } - { - name: 'The Second Kithmaze' - type: 'hero' - id: 'the-second-kithmaze' - original: '5418cf256bae62f707c7e1c3' - description: 'Many have tried, few have found their way through this maze.' - x: 54.49 - y: 26.49 - nextLevels: - continue: 'dread-door' - } - { - name: 'Dread Door' - type: 'hero' - id: 'dread-door' - original: '5418d40f4c16460000ab9ac2' - description: 'Behind a dread door lies a chest full of riches.' - x: 60.52 - y: 33.70 - nextLevels: - continue: 'known-enemy' - } - { - name: 'Known Enemy' - type: 'hero' - id: 'known-enemy' - original: '5452adea57e83800009730ee' - description: 'Begin to use variables in your battles.' - x: 67 - y: 39 - nextLevels: - continue: 'master-of-names' - } - { - name: 'Master of Names' - type: 'hero' - id: 'master-of-names' - original: '5452c3ce57e83800009730f7' - description: 'Use your glasses to defend yourself from the Kithmen.' - x: 75 - y: 46 - nextLevels: - continue: 'lowly-kithmen' - } - { - name: 'Lowly Kithmen' - type: 'hero' - id: 'lowly-kithmen' - original: '541b24511ccc8eaae19f3c1f' - description: 'Now that you can see them, they\'re everywhere!' - x: 85 - y: 40 - nextLevels: - continue: 'closing-the-distance' - } - { - name: 'Closing the Distance' - type: 'hero' - id: 'closing-the-distance' - original: '541b288e1ccc8eaae19f3c25' - description: 'Kithmen are not the only ones to stand in your way.' - x: 93 - y: 47 - nextLevels: - continue: 'the-final-kithmaze' - } - { - name: 'Tactical Strike' - type: 'hero' - id: 'tactical-strike' - original: '5452cfa706a59e000067e4f5' - description: 'They\'re, uh, coming right for us! Sneak up behind them.' - x: 83.23 - y: 52.73 - nextLevels: - continue: 'the-final-kithmaze' - practice: true - requiresSubscription: true - } - { - name: 'The Final Kithmaze' - type: 'hero' - id: 'the-final-kithmaze' - original: '541b434e1ccc8eaae19f3c33' - description: 'To escape you must find your way through an Elder Kithman\'s maze.' - x: 86.95 - y: 64.70 - nextLevels: - continue: 'kithgard-gates' - } - { - name: 'The Gauntlet' - type: 'hero' - id: 'the-gauntlet' - original: '5452d8b906a59e000067e4fa' - description: 'Rush for the stairs, battling foes at every turn.' - x: 76.50 - y: 72.69 - nextLevels: - continue: 'kithgard-gates' - practice: true - requiresSubscription: true - } - { - name: 'Kithgard Gates' - type: 'hero' - id: 'kithgard-gates' - original: '541c9a30c6362edfb0f34479' - description: 'Escape the Kithgard dungeons and don\'t let the guardians get you.' - x: 89 - y: 82 - nextLevels: - continue: 'defense-of-plainswood' - } - { - name: 'Cavern Survival' - type: 'hero-ladder' - id: 'cavern-survival' - original: '544437e0645c0c0000c3291d' - description: 'Stay alive longer than your opponent amidst hordes of ogres!' - x: 17.54 - y: 78.39 - adventurer: true - } -] - -forest = [ - { - name: 'Defense of Plainswood' - type: 'hero' - id: 'defense-of-plainswood' - original: '541b67f71ccc8eaae19f3c62' - description: 'Protect the peasants from the pursuing ogres.' - nextLevels: - continue: 'winding-trail' - x: 18 - y: 37 - } - { - name: 'Winding Trail' - type: 'hero' - id: 'winding-trail' - original: '5446cb40ce01c23e05ecf027' - description: 'Stay alive and navigate through the forest.' - nextLevels: - continue: 'patrol-buster' - x: 24 - y: 35 - } - { - name: 'Patrol Buster' - type: 'hero' - id: 'patrol-buster' - original: '5487330d84f7b4dac246d440' - description: 'Defeat ogre patrols with new, selective targeting skills.' - nextLevels: - continue: 'thornbush-farm' - x: 34 - y: 25 - } - { - name: 'Endangered Burl' - type: 'hero' - id: 'endangered-burl' - original: '546e97033f1c1c1be898402b' - description: 'Hunt ogres in the woods, but watch out for lumbering beasts.' - nextLevels: - continue: 'thornbush-farm' - x: 29 - y: 35 - } - { - name: 'Village Guard' - type: 'hero' - id: 'village-guard' - original: '546e91b8a4b7840000ee92dc' - description: 'Defend a village from marauding munchkin mayhem.' - nextLevels: - continue: 'thornbush-farm' - x: 33 - y: 37 - practice: true - requiresSubscription: true - } - { - name: 'Thornbush Farm' - type: 'hero' - id: 'thornbush-farm' - original: '5447030525cce60000745e2a' - description: 'Determine refugee peasant from ogre when defending the farm.' - nextLevels: - continue: 'back-to-back' - x: 37 - y: 40 - } - { - name: 'Back to Back' - type: 'hero' - id: 'back-to-back' - original: '5448330517d7283e051f9b9e' - description: 'Patrol the village entrances, but stay defensive.' - nextLevels: - continue: 'ogre-encampment' - x: 39 - y: 47.5 - } - { - name: 'Ogre Encampment' - type: 'hero' - id: 'ogre-encampment' - original: '5456b3c8d5ada30000525605' - description: 'Recover stolen treasure from an ogre encampment.' - nextLevels: - continue: 'woodland-cleaver' - x: 39 - y: 55 - } - { - name: 'Woodland Cleaver' - type: 'hero' - id: 'woodland-cleaver' - original: '5456bb8dd5ada30000525613' - description: 'Use your new cleave ability to fend off munchkins.' - nextLevels: - continue: 'shield-rush' - x: 39.5 - y: 61 - } - { - name: 'Shield Rush' - type: 'hero' - id: 'shield-rush' - original: '5459570bb4461871053292f5' - description: 'Combine cleave and shield to endure an ogre onslaught.' - nextLevels: - continue: 'peasant-protection' - x: 42 - y: 68 - } - - # Warrior branch - { - name: 'Peasant Protection' - type: 'hero' - id: 'peasant-protection' - original: '545ec477e7f60fd6c55760e9' - description: 'Stay close to Victor.' - nextLevels: - continue: 'munchkin-swarm' - x: 44.5 - y: 75.5 - } - { - name: 'Munchkin Swarm' - type: 'hero' - id: 'munchkin-swarm' - original: '545edba9e7f60fd6c5576133' - description: 'Loot a gigantic chest while surrounded by a swarm of ogre munchkins.' - nextLevels: - continue: 'coinucopia' - x: 49 - y: 81 - } - - # Ranger branch - { - name: 'Munchkin Harvest' - type: 'hero' - id: 'munchkin-harvest' - original: '5470001860f6cc376131525d' - description: 'Join forces with a new hero: Amara Arrowhead.' - nextLevels: - continue: 'swift-dagger' - x: 38 - y: 72 - requiresSubscription: true - unlocksHero: { - img: '/file/db/thang.type/52fc0ed77e01835453bd8f6c/portrait.png' - originalID: '52fc0ed77e01835453bd8f6c' - } - } - { - name: 'Swift Dagger' - type: 'hero' - id: 'swift-dagger' - original: '54701f7860f6cc37613152a1' - description: 'Deal damage from a distance with your new hero.' - nextLevels: - continue: 'shrapnel' - x: 33 - y: 72 - requiresSubscription: true - } - { - name: 'Shrapnel' - type: 'hero' - id: 'shrapnel' - original: '5470291c60f6cc37613152d1' - description: 'Explore the explosive arts.' - nextLevels: - continue: 'coinucopia' - x: 28 - y: 73 - requiresSubscription: true - } - - # Wizard branch - { - name: 'Arcane Ally' - type: 'hero' - id: 'arcane-ally' - original: '5470b98ceb739dbc9d2402c7' - description: 'Stand your ground against large ogres with a new hero: Ms. Hushbaum.' - nextLevels: - continue: 'touch-of-death' - x: 47 - y: 71 - requiresSubscription: true - unlocksHero: { - img: '/file/db/thang.type/52fbf74b7e01835453bd8d8e/portrait.png' - originalID: '529ec584c423d4e83b000014' - } - } - { - name: 'Touch of Death' - type: 'hero' - id: 'touch-of-death' - original: '5470ca33eb739dbc9d2402ee' - description: 'Learn your first spell to siphon life from your foes.' - nextLevels: - continue: 'bonemender' - x: 52 - y: 70 - requiresSubscription: true - } - { - name: 'Bonemender' - type: 'hero' - id: 'bonemender' - original: '5470d013eb739dbc9d240323' - description: 'Cast regeneration on allied soldiers to withstand a siege.' - nextLevels: - continue: 'coinucopia' - x: 58 - y: 67 - requiresSubscription: true - } - - { - name: 'Coinucopia' - type: 'hero' - id: 'coinucopia' - original: '545bb1181e649a4495f887df' - description: 'Start playing in real-time with input flags as you collect gold coins!' - nextLevels: - continue: 'copper-meadows' - x: 56 - y: 82 - } - { - name: 'Copper Meadows' - type: 'hero' - id: 'copper-meadows' - original: '5462491c688f333d05d8af38' - description: 'This level exercises: if/else, object members, variables, flag placement, and collection.' - nextLevels: - continue: 'drop-the-flag' - x: 60 - y: 86 - } - { - name: 'Drop the Flag' - type: 'hero' - id: 'drop-the-flag' - original: '54626472f3c64b7b0598590c' - description: 'This level exercises: flag position, object members.' - nextLevels: - continue: 'rich-forager' - x: 65.5 - y: 91 - } - { - name: 'Deadly Pursuit' - type: 'hero' - id: 'deadly-pursuit' - original: '54626f270cacde3f055434ac' - description: 'This level exercises: if/else, flag placement and timing, item collection.' - nextLevels: - continue: 'rich-forager' - x: 74.5 - y: 92 - requiresSubscription: true - unlocksHero: { - img: '/file/db/thang.type/5466d449417c8b48a9811e83/portrait.png' - originalID: '5466d449417c8b48a9811e83' - } - } - { - name: 'Rich Forager' - type: 'hero' - id: 'rich-forager' - original: '546283ddfdd66af405fa8209' - description: 'This level exercises: if/else if, collection, combat.' - nextLevels: - continue: 'siege-of-stonehold' - x: 80 - y: 88 - unlocksHero: { - img: '/file/db/thang.type/52e9adf7427172ae56002172/portrait.png' - originalID: '52e9adf7427172ae56002172' - } - } - { - name: 'Siege of Stonehold' - type: 'hero' - id: 'siege-of-stonehold' - original: '54712072eb739dbc9d24034b' - description: 'Unlock the desert world, if you are strong enough to win this epic battle!' - nextLevels: - continue: 'the-dunes' - x: 85.5 - y: 83.5 - } - { - name: 'Multiplayer Treasure Grove' - type: 'hero-ladder' - id: 'multiplayer-treasure-grove' - original: '5469643c37600b40e0e09c5b' - description: 'Mix collection, flags, and combat in this multiplayer coin-gathering arena.' - x: 56.5 - y: 20 - } - { - name: 'Dueling Grounds' - type: 'hero-ladder' - id: 'dueling-grounds' - original: '5442ba0e1e835500007eb1c7' - description: 'Battle head-to-head against another hero in this basic beginner combat arena.' - x: 83 - y: 23 - adventurer: true - } -] - -desert = [ - { - name: 'The Dunes' - type: 'hero' - id: 'the-dunes' - original: '5480b62e1bf0b10000711c59' - description: 'Behold, the desert, full of glory, danger, and sand. Lots of sand.' - nextLevels: - continue: 'the-mighty-sand-yak' - x: 8.47 - y: 21.93 - requiresSubscription: true - } - { - name: 'The Mighty Sand Yak' - type: 'hero' - id: 'the-mighty-sand-yak' - original: '5480b9d01bf0b10000711c5f' - description: 'Test your nerves by dodging huge sand yaks on the open dunes!' - nextLevels: - continue: 'oasis' - x: 16.56 - y: 27.77 - requiresSubscription: false - } - { - name: 'Oasis' - type: 'hero' - id: 'oasis' - original: '5480ba761bf0b10000711c64' - description: 'Run a gauntlet of sand yaks to reach oasis and quench your thirst!' - nextLevels: - continue: 'sarven-road' - x: 23.35 - y: 31.60 - requiresSubscription: false - } - { - name: 'Sarven Road' - type: 'hero' - id: 'sarven-road' - original: '548c82360ffdc235e80ef04b' - description: 'Watch out for ogre scouts on the road as you search for water.' - nextLevels: - continue: 'sarven-gaps' - x: 28.36 - y: 24.59 - adventurer: true - requiresSubscription: false - } - { - name: 'Sarven Gaps' - type: 'hero' - id: 'sarven-gaps' - original: '548c8f4a0ffdc235e80ef0a8' - description: 'Keep the oasis safe by building fences to hold back the enemy.' - nextLevels: - continue: 'thunderhooves' - x: 21.13 - y: 9.29 - adventurer: true - requiresSubscription: true - } - { - name: 'Thunderhooves' - type: 'hero' - id: 'thunderhooves' - original: '548c90020ffdc235e80ef0ad' - description: 'Fence off the stampeding sand yaks to reach the next watering hole.' - nextLevels: - continue: 'medical-attention' - x: 35.08 - y: 20.48 - adventurer: true - requiresSubscription: false - } - { - name: 'Medical Attention' - type: 'hero' - id: 'medical-attention' - original: '548ce3300ffdc235e80ef0b2' - description: 'Get help from a helpful wizard while you fend off an ogre attack.' - nextLevels: - continue: 'minesweeper' - x: 42.84 - y: 21.82 - adventurer: true - requiresSubscription: false - } - { - name: 'Minesweeper' - type: 'hero' - id: 'minesweeper' - original: '5490cb7c623b972aa26b25a3' - description: 'Lead a band of hapless peasants through a treacherous canyon while you heroically trigger the mines.' - nextLevels: - continue: 'sarven-sentry' - x: 47.64 - y: 12.40 - adventurer: true - requiresSubscription: true - } - { - name: 'Sarven Sentry' - type: 'hero' - id: 'sarven-sentry' - original: '548cef7f0ffdc235e80ef0cc' - description: 'Coming Soon' - nextLevels: - continue: 'keeping-time' - x: 51.48 - y: 26.09 - adventurer: true - requiresSubscription: false - disabled: not me.isAdmin() - } - { - name: 'Keeping Time' - type: 'hero' - id: 'keeping-time' - original: '548cf1a90ffdc235e80ef0d1' - description: 'Coming Soon' - nextLevels: - continue: 'hoarding-gold' - x: 58.42 - y: 34.14 - adventurer: true - requiresSubscription: false - disabled: not me.isAdmin() - } - { - name: 'Hoarding Gold' - type: 'hero' - id: 'hoarding-gold' - original: '' - description: 'Coming Soon' - nextLevels: - continue: 'decoy-drill' - x: 61.73 - y: 29.51 - adventurer: true - requiresSubscription: false - disabled: not me.isAdmin() - } - { - name: 'Decoy Drill' - type: 'hero' - id: 'decoy-drill' - original: '' - description: 'Coming Soon' - nextLevels: - continue: 'yakstraction' - x: 62.05 - y: 40.44 - adventurer: true - requiresSubscription: false - disabled: not me.isAdmin() - } - { - name: 'Yakstraction' - type: 'hero' - id: 'yakstraction' - original: '' - description: 'Coming Soon' - nextLevels: - continue: 'sarven-brawl' - x: 66.46 - y: 48.87 - adventurer: true - requiresSubscription: true - } - { - name: 'Sarven Brawl' - type: 'hero' - id: 'sarven-brawl' - original: '548cf2850ffdc235e80ef0d6' - description: 'Coming Soon' - #nextLevels: - # continue: '' - x: 69.01 - y: 33.80 - adventurer: true - requiresSubscription: false - disabled: not me.isAdmin() - } - -] - -WorldMapView.campaigns = campaigns = [ - {id: 'dungeon', name: 'Dungeon Campaign', levels: dungeon } - {id: 'forest', name: 'Forest Campaign', levels: forest } - {id: 'desert', name: 'Desert Campaign', levels: desert } -] diff --git a/app/views/play/WorldMapView.coffee b/app/views/play/WorldMapView.coffee index a1f8b3f73..9bd32a836 100644 --- a/app/views/play/WorldMapView.coffee +++ b/app/views/play/WorldMapView.coffee @@ -99,6 +99,7 @@ module.exports = class WorldMapView extends RootView super() getLevelPlayCounts: -> + return return unless me.isAdmin() success = (levelPlayCounts) => return if @destroyed From efc83b88d02633548aa27743b127996ef0243953 Mon Sep 17 00:00:00 2001 From: Scott Erickson <sderickson@gmail.com> Date: Mon, 22 Dec 2014 10:29:29 -0500 Subject: [PATCH 07/15] Hooked up the CampaignView to show adjacent campaigns based on the data. --- app/schemas/models/campaign.schema.coffee | 2 +- .../editor/campaign/campaign-editor-view.jade | 2 +- app/templates/play/campaign-view.jade | 13 ++++--------- .../editor/campaign/CampaignEditorView.coffee | 16 ++++++++-------- app/views/play/CampaignView.coffee | 19 ++++++++++++++++--- 5 files changed, 30 insertions(+), 22 deletions(-) diff --git a/app/schemas/models/campaign.schema.coffee b/app/schemas/models/campaign.schema.coffee index 23dc3e418..77eeddb42 100644 --- a/app/schemas/models/campaign.schema.coffee +++ b/app/schemas/models/campaign.schema.coffee @@ -27,10 +27,10 @@ _.extend CampaignSchema.properties, { format: 'campaign' properties: { #- denormalized from other Campaigns, either updated automatically or fetched dynamically + id: { type: 'string', format: 'hidden' } name: { type: 'string', format: 'hidden' } description: { type: 'string', format: 'hidden' } i18n: { type: 'object', format: 'hidden' } - original: { type: 'string', format: 'hidden' } slug: { type: 'string', format: 'hidden' } #- normal properties diff --git a/app/templates/editor/campaign/campaign-editor-view.jade b/app/templates/editor/campaign/campaign-editor-view.jade index eeab8c239..a0d47fdff 100644 --- a/app/templates/editor/campaign/campaign-editor-view.jade +++ b/app/templates/editor/campaign/campaign-editor-view.jade @@ -38,7 +38,7 @@ block outer_content #campaign-treema #right-column - #world-map-view + #campaign-view #campaign-level-view block footer diff --git a/app/templates/play/campaign-view.jade b/app/templates/play/campaign-view.jade index 6abd410b6..982d41bcb 100644 --- a/app/templates/play/campaign-view.jade +++ b/app/templates/play/campaign-view.jade @@ -40,15 +40,10 @@ .campaign-label= campaign.get('name') if isIPadApp && !level.disabled && !level.locked button.btn.btn-success.btn-lg.start-level(data-i18n="common.play") Play - if mapType === 'dungeon' && forestIsAvailable - a#forest-link.glyphicon.glyphicon-share-alt.campaign-switch(href="/play/forest", data-i18n="[title]play.campaign_forest") - if mapType === 'forest' - a#dungeon-link.glyphicon.glyphicon-share-alt.campaign-switch(href="/play/dungeon", data-i18n="[title]play.campaign_dungeon") - if desertIsAvailable - a#desert-link.glyphicon.glyphicon-share-alt.campaign-switch(href="/play/desert", data-i18n="[title]play.campaign_desert") - if mapType === 'desert' - a#forest-back-link.glyphicon.glyphicon-share-alt.campaign-switch(href="/play/forest", data-i18n="[title]play.campaign_forest") - + + for adjacentCampaign in adjacentCampaigns + a + span.glyphicon.glyphicon-share-alt.campaign-switch(href="/play/"+adjacentCampaign.slug, style=adjacentCampaign.style, title=adjacentCampaign.name) .game-controls.header-font button.btn.items(data-toggle='coco-modal', data-target='play/modal/PlayItemsModal', data-i18n="[title]play.items") diff --git a/app/views/editor/campaign/CampaignEditorView.coffee b/app/views/editor/campaign/CampaignEditorView.coffee index 40e97446c..f5f0a5a52 100644 --- a/app/views/editor/campaign/CampaignEditorView.coffee +++ b/app/views/editor/campaign/CampaignEditorView.coffee @@ -3,7 +3,7 @@ Campaign = require 'models/Campaign' Level = require 'models/Level' Achievement = require 'models/Achievement' ThangType = require 'models/ThangType' -WorldMapView = require 'views/play/WorldMapView' +CampaignView = require 'views/play/CampaignView' CocoCollection = require 'collections/CocoCollection' treemaExt = require 'core/treema-ext' utils = require 'core/utils' @@ -16,7 +16,7 @@ module.exports = class CampaignEditorView extends RootView template: require 'templates/editor/campaign/campaign-editor-view' className: 'editor' - constructor: (options, campaignHandle) -> + constructor: (options, @campaignHandle) -> super(options) # MIGRATION CODE @@ -38,7 +38,7 @@ module.exports = class CampaignEditorView extends RootView # @campaign = new Campaign(campaign) #------------------------------------------------ - @campaign = new Campaign({_id:campaignHandle}) + @campaign = new Campaign({_id:@campaignHandle}) #--------------- temporary migration to change thang type slugs to originals #- should keep around though for loading the names of items and heroes that are referenced @@ -65,14 +65,14 @@ module.exports = class CampaignEditorView extends RootView @levels = new CocoCollection([], { model: Level - url: "/db/campaign/#{campaignHandle}/levels" + url: "/db/campaign/#{@campaignHandle}/levels" project: Campaign.denormalizedLevelProperties }) @supermodel.loadCollection(@levels, 'levels') @achievements = new CocoCollection([], { model: Achievement - url: "/db/campaign/#{campaignHandle}/achievements" + url: "/db/campaign/#{@campaignHandle}/achievements" project: achievementProject }) @supermodel.loadCollection(@achievements, 'achievements') @@ -179,9 +179,9 @@ module.exports = class CampaignEditorView extends RootView @treema.open() @treema.childrenTreemas.levels?.open() - worldMapView = new WorldMapView({supermodel: @supermodel, editorMode: true}, 'dungeon') - worldMapView.highlightElement = _.noop # make it stop - @insertSubView worldMapView + campaignView = new CampaignView({editorMode: true, supermodel: @supermodel}, @campaignHandle) + campaignView.highlightElement = _.noop # make it stop + @insertSubView campaignView onTreemaChanged: (e, nodes) => for node in nodes diff --git a/app/views/play/CampaignView.coffee b/app/views/play/CampaignView.coffee index c2a5bf114..3abdf3ba7 100644 --- a/app/views/play/CampaignView.coffee +++ b/app/views/play/CampaignView.coffee @@ -12,6 +12,7 @@ storage = require 'core/storage' AuthModal = require 'views/core/AuthModal' SubscribeModal = require 'views/core/SubscribeModal' Level = require 'models/Level' +utils = require 'core/utils' trackedHourOfCode = false @@ -46,8 +47,8 @@ module.exports = class WorldMapView extends RootView options ?= {} @campaign = new Campaign({_id:@terrain}) - @supermodel.loadModel(@campaign, 'campaign') - + @campaign = @supermodel.loadModel(@campaign, 'campaign').model + @editorMode = options.editorMode @nextLevel = @getQueryVariable 'next' @levelStatusMap = {} @@ -135,7 +136,7 @@ module.exports = class WorldMapView extends RootView getRenderData: (context={}) -> context = super(context) context.campaign = @campaign - context.levels = _.values($.extend {}, @campaign.get('levels')) + context.levels = _.values($.extend true, {}, @campaign.get('levels')) for level in context.levels level.position ?= { x: 10, y: 10 } level.locked = not me.ownsLevel level.original @@ -164,6 +165,18 @@ module.exports = class WorldMapView extends RootView context.desertIsAvailable = Level.levels['the-mighty-sand-yak'] in (me.get('earned')?.levels or []) context.requiresSubscription = @requiresSubscription context.editorMode = @editorMode + context.adjacentCampaigns = _.filter _.values(_.cloneDeep(@campaign.get('adjacentCampaigns') or {})), (ac) -> + return false if ac.showIfUnlocked and ac.showIfUnlocked not in (me.get('unlocked')?.levels or []) + ac.name = utils.i18n ac, 'name' + ac.description = utils.i18n ac, 'description' + styles = [] + styles.push "color: #{ac.color}" if ac.color + styles.push "transform: rotate(#{ac.rotation}deg)" if ac.rotation + ac.position ?= { x: 10, y: 10 } + styles.push "left: #{ac.position.x}%" + styles.push "top: #{ac.position.y}%" + ac.style = styles.join('; ') + return true context afterRender: -> From a31b385a4da3fe30a2e8ab3cab7bb0498a6d67e1 Mon Sep 17 00:00:00 2001 From: Scott Erickson <sderickson@gmail.com> Date: Mon, 22 Dec 2014 10:54:07 -0500 Subject: [PATCH 08/15] Moving levels and adjacent campaign links around in the CampaignView saves the new position to the CampaignEditorView. --- app/templates/play/campaign-view.jade | 18 +++++++++--------- .../editor/campaign/CampaignEditorView.coffee | 10 ++++++++++ app/views/play/CampaignView.coffee | 7 +++++-- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/app/templates/play/campaign-view.jade b/app/templates/play/campaign-view.jade index 982d41bcb..7abb983eb 100644 --- a/app/templates/play/campaign-view.jade +++ b/app/templates/play/campaign-view.jade @@ -8,19 +8,19 @@ each level in levels if !level.hidden - var next = nextLevel && level.slug === nextLevel; - div(style="left: #{level.position.x}%; bottom: #{level.position.y}%; background-color: #{level.color}", class="level" + (next ? " next" : "") + (level.disabled ? " disabled" : "") + (level.locked ? " locked" : "") + " " + levelStatusMap[level.id] || "", data-level-id=level.id, title=level.name + (level.disabled ? ' (Coming Soon to Adventurers)' : '')) + div(style="left: #{level.position.x}%; bottom: #{level.position.y}%; background-color: #{level.color}", class="level" + (next ? " next" : "") + (level.disabled ? " disabled" : "") + (level.locked ? " locked" : "") + " " + levelStatusMap[level.original] || "", data-level-id=level.original, title=level.name + (level.disabled ? ' (Coming Soon to Adventurers)' : '')) if level.unlocksHero && !level.unlockedHero img.hero-portrait(src=level.unlocksHero.img) - a(href=level.type == 'hero' ? '#' : level.disabled ? "/play" : "/play/#{level.levelPath || 'level'}/#{level.id}", disabled=level.disabled, data-level-id=level.id, data-level-path=level.levelPath || 'level', data-level-name=level.name) + a(href=level.type == 'hero' ? '#' : level.disabled ? "/play" : "/play/#{level.levelPath || 'level'}/#{level.original}", disabled=level.disabled, data-level-id=level.original, data-level-path=level.levelPath || 'level', data-level-name=level.name) if level.requiresSubscription img.star(src="/images/pages/play/star.png") - if levelStatusMap[level.id] === 'complete' + if levelStatusMap[level.original] === 'complete' img.banner(src="/images/pages/play/level-banner-complete.png") - if levelStatusMap[level.id] === 'started' + if levelStatusMap[level.original] === 'started' img.banner(src="/images/pages/play/level-banner-started.png") - div(style="left: #{level.x}%; bottom: #{level.y}%", class="level-shadow" + (next ? " next" : "") + " " + levelStatusMap[level.id] || "") - .level-info-container(data-level-id=level.id, data-level-path=level.levelPath || 'level', data-level-name=level.name) - div(class="level-info " + (levelStatusMap[level.id] || "")) + div(style="left: #{level.x}%; bottom: #{level.y}%", class="level-shadow" + (next ? " next" : "") + " " + levelStatusMap[level.original] || "") + .level-info-container(data-level-id=level.original, data-level-path=level.levelPath || 'level', data-level-name=level.name) + div(class="level-info " + (levelStatusMap[level.original] || "")) h3= level.name + (level.disabled ? " (Coming soon!)" : (level.locked ? " (Locked)" : "")) .level-description= level.description if level.disabled @@ -30,7 +30,7 @@ strong(data-i18n="play.awaiting_levels_adventurer") Sign up as an Adventurer span.spl(data-i18n="play.awaiting_levels_adventurer_suffix") to be the first to play new levels. - - var playCount = levelPlayCountMap[level.id] + - var playCount = levelPlayCountMap[level.original] if playCount && playCount.sessions > 20 div span.spr #{playCount.sessions} @@ -43,7 +43,7 @@ for adjacentCampaign in adjacentCampaigns a - span.glyphicon.glyphicon-share-alt.campaign-switch(href="/play/"+adjacentCampaign.slug, style=adjacentCampaign.style, title=adjacentCampaign.name) + span.glyphicon.glyphicon-share-alt.campaign-switch(href="/play/"+adjacentCampaign.slug, style=adjacentCampaign.style, title=adjacentCampaign.name, data-campaign-id=adjacentCampaign.id) .game-controls.header-font button.btn.items(data-toggle='coco-modal', data-target='play/modal/PlayItemsModal', data-i18n="[title]play.items") diff --git a/app/views/editor/campaign/CampaignEditorView.coffee b/app/views/editor/campaign/CampaignEditorView.coffee index f5f0a5a52..25383a67e 100644 --- a/app/views/editor/campaign/CampaignEditorView.coffee +++ b/app/views/editor/campaign/CampaignEditorView.coffee @@ -181,6 +181,8 @@ module.exports = class CampaignEditorView extends RootView campaignView = new CampaignView({editorMode: true, supermodel: @supermodel}, @campaignHandle) campaignView.highlightElement = _.noop # make it stop + @listenTo campaignView, 'level-moved', @onCampaignLevelMoved + @listenTo campaignView, 'adjacent-campaign-moved', @onAdjacentCampaignMoved @insertSubView campaignView onTreemaChanged: (e, nodes) => @@ -202,6 +204,14 @@ module.exports = class CampaignEditorView extends RootView @toSave.add @campaign + onCampaignLevelMoved: (e) -> + path = "levels/#{e.levelOriginal}/position" + @treema.set path, e.position + + onAdjacentCampaignMoved: (e) -> + path = "adjacentCampaigns/#{e.campaignID}/position" + @treema.set path, e.position + updateRewardsForLevel: (level, rewards) -> achievements = @supermodel.getModels(Achievement) achievements = (a for a in achievements when a.get('related') is level.get('original')) diff --git a/app/views/play/CampaignView.coffee b/app/views/play/CampaignView.coffee index 3abdf3ba7..c529b35e8 100644 --- a/app/views/play/CampaignView.coffee +++ b/app/views/play/CampaignView.coffee @@ -184,13 +184,16 @@ module.exports = class WorldMapView extends RootView @onWindowResize() unless application.isIPadApp _.defer => @$el?.find('.game-controls .btn').tooltip() # Have to defer or i18n doesn't take effect. - @$el.find('.level').tooltip().each -> + view = @ + @$el.find('.level, .campaign-switch').tooltip().each -> return unless me.isAdmin() $(@).draggable().on 'dragstop', -> bg = $('.map-background') x = ($(@).offset().left - bg.offset().left + $(@).outerWidth() / 2) / bg.width() y = 1 - ($(@).offset().top - bg.offset().top + $(@).outerHeight() / 2) / bg.height() - console.log "#{$(@).data('level-id')}\n x: #{(100 * x).toFixed(2)}\n y: #{(100 * y).toFixed(2)}\n" + e = { position: { x: (100 * x), y: (100 * y) }, levelOriginal: $(@).data('level-id'), campaignID: $(@).data('campaign-id') } + view.trigger 'level-moved', e if e.levelOriginal + view.trigger 'adjacent-campaign-moved', e if e.campaignID @$el.addClass _.string.slugify @terrain @updateVolume() @updateHero() From 1db8284236fbbffd233f3bcc8888451cbe3faa3d Mon Sep 17 00:00:00 2001 From: Scott Erickson <sderickson@gmail.com> Date: Mon, 22 Dec 2014 11:09:58 -0500 Subject: [PATCH 09/15] When you select a level on the CampaignView while in edit mode, it selects the level in the CampaignEditView. --- app/views/editor/campaign/CampaignEditorView.coffee | 6 ++++++ app/views/play/CampaignView.coffee | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/views/editor/campaign/CampaignEditorView.coffee b/app/views/editor/campaign/CampaignEditorView.coffee index 25383a67e..cacb22000 100644 --- a/app/views/editor/campaign/CampaignEditorView.coffee +++ b/app/views/editor/campaign/CampaignEditorView.coffee @@ -183,6 +183,7 @@ module.exports = class CampaignEditorView extends RootView campaignView.highlightElement = _.noop # make it stop @listenTo campaignView, 'level-moved', @onCampaignLevelMoved @listenTo campaignView, 'adjacent-campaign-moved', @onAdjacentCampaignMoved + @listenTo campaignView, 'level-clicked', @onCampaignLevelClicked @insertSubView campaignView onTreemaChanged: (e, nodes) => @@ -212,6 +213,11 @@ module.exports = class CampaignEditorView extends RootView path = "adjacentCampaigns/#{e.campaignID}/position" @treema.set path, e.position + onCampaignLevelClicked: (levelOriginal) -> + return unless levelTreema = @treema.childrenTreemas?.levels?.childrenTreemas?[levelOriginal] + levelTreema.select() +# levelTreema.open() + updateRewardsForLevel: (level, rewards) -> achievements = @supermodel.getModels(Achievement) achievements = (a for a in achievements when a.get('related') is level.get('original')) diff --git a/app/views/play/CampaignView.coffee b/app/views/play/CampaignView.coffee index c529b35e8..9acdd39ef 100644 --- a/app/views/play/CampaignView.coffee +++ b/app/views/play/CampaignView.coffee @@ -235,7 +235,9 @@ module.exports = class WorldMapView extends RootView @$levelInfo?.hide() levelElement = $(e.target).parents('.level') levelID = levelElement.data('level-id') - level = _.find _.values(campaign.get('levels')), id: levelID + if @editorMode + return @trigger 'level-clicked', levelID + level = _.find _.values(@campaign.get('levels')), id: levelID if application.isIPadApp @$levelInfo = @$el.find(".level-info-container[data-level-id=#{levelID}]").show() @adjustLevelInfoPosition e From 0d45e4a8893563e3cb75aa502331c0db781c3a88 Mon Sep 17 00:00:00 2001 From: Scott Erickson <sderickson@gmail.com> Date: Mon, 22 Dec 2014 12:06:17 -0500 Subject: [PATCH 10/15] Set up the CampaignEditorView to trigger the CampaignView to re-render when things change. --- .../editor/campaign/CampaignEditorView.coffee | 1205 +++++++++-------- app/views/play/CampaignView.coffee | 3 + 2 files changed, 608 insertions(+), 600 deletions(-) diff --git a/app/views/editor/campaign/CampaignEditorView.coffee b/app/views/editor/campaign/CampaignEditorView.coffee index cacb22000..ac8ceb6cc 100644 --- a/app/views/editor/campaign/CampaignEditorView.coffee +++ b/app/views/editor/campaign/CampaignEditorView.coffee @@ -83,7 +83,8 @@ module.exports = class CampaignEditorView extends RootView campaignLevels = $.extend({}, @campaign.get('levels')) for level in @levels.models levelOriginal = level.get('original') - campaignLevel = campaignLevels[levelOriginal] ? {} + campaignLevel = campaignLevels[levelOriginal] + continue if not campaignLevel #--------------- temporary migrations if campaignLevel.restrictedGear @@ -179,12 +180,12 @@ module.exports = class CampaignEditorView extends RootView @treema.open() @treema.childrenTreemas.levels?.open() - campaignView = new CampaignView({editorMode: true, supermodel: @supermodel}, @campaignHandle) - campaignView.highlightElement = _.noop # make it stop - @listenTo campaignView, 'level-moved', @onCampaignLevelMoved - @listenTo campaignView, 'adjacent-campaign-moved', @onAdjacentCampaignMoved - @listenTo campaignView, 'level-clicked', @onCampaignLevelClicked - @insertSubView campaignView + @campaignView = new CampaignView({editorMode: true, supermodel: @supermodel}, @campaignHandle) + @campaignView.highlightElement = _.noop # make it stop + @listenTo @campaignView, 'level-moved', @onCampaignLevelMoved + @listenTo @campaignView, 'adjacent-campaign-moved', @onAdjacentCampaignMoved + @listenTo @campaignView, 'level-clicked', @onCampaignLevelClicked + @insertSubView @campaignView onTreemaChanged: (e, nodes) => for node in nodes @@ -204,6 +205,8 @@ module.exports = class CampaignEditorView extends RootView @toSave.add level @toSave.add @campaign + @campaign.set key, value for key, value of @treema.data + @campaignView.setCampaign(@campaign) onCampaignLevelMoved: (e) -> path = "levels/#{e.levelOriginal}/position" @@ -242,8 +245,10 @@ class LevelsNode extends TreemaObjectNode s = new Backbone.Collection([], {model:Level}) s.url = '/db/level' s.fetch({data: {term:req.term, project: Campaign.denormalizedLevelProperties.join(',')}}) - s.once 'sync', (collection) -> - LevelsNode.levels[level.get('original')] = level for level in collection.models + s.once 'sync', (collection) => + for level in collection.models + LevelsNode.levels[level.get('original')] = level + @settings.supermodel.registerModel level mapped = ({label: r.get('name'), value: r.get('original')} for r in collection.models) res(mapped) @@ -298,594 +303,594 @@ class AchievementNode extends treemaExt.IDReferenceNode -campaign = { - name: 'Dungeon' - levels: {} -} - - -levels = [ - { - name: 'Dungeons of Kithgard' - type: 'hero' - id: 'dungeons-of-kithgard' - original: '5411cb3769152f1707be029c' - description: 'Grab the gem, but touch nothing else. Start here.' - x: 14 - y: 15.5 - nextLevels: - continue: 'gems-in-the-deep' - } - { - name: 'Gems in the Deep' - type: 'hero' - id: 'gems-in-the-deep' - original: '54173c90844506ae0195a0b4' - description: 'Quickly collect the gems; you will need them.' - x: 29 - y: 12 - nextLevels: - continue: 'shadow-guard' - } - { - name: 'Shadow Guard' - type: 'hero' - id: 'shadow-guard' - original: '54174347844506ae0195a0b8' - description: 'Evade the Kithgard minion.' - x: 40.54 - y: 11.03 - nextLevels: - continue: 'forgetful-gemsmith' - } - { - name: 'Kounter Kithwise' - type: 'hero' - id: 'kounter-kithwise' - original: '54527a6257e83800009730c7' - description: 'Practice your evasion skills with more guards.' - x: 35.37 - y: 20.61 - nextLevels: - continue: 'crawlways-of-kithgard' - practice: true - requiresSubscription: true - } - { - name: 'Crawlways of Kithgard' - type: 'hero' - id: 'crawlways-of-kithgard' - original: '545287ef57e83800009730d5' - description: 'Dart in and grab the gem–at the right moment.' - x: 36.48 - y: 29.03 - nextLevels: - continue: 'forgetful-gemsmith' - practice: true - requiresSubscription: true - } - { - name: 'Forgetful Gemsmith' - type: 'hero' - id: 'forgetful-gemsmith' - original: '544a98f62d002f0000fe331a' - description: 'Grab even more gems as you practice moving.' - x: 54.98 - y: 10.53 - nextLevels: - continue: 'true-names' - } - { - name: 'True Names' - type: 'hero' - id: 'true-names' - original: '541875da4c16460000ab990f' - description: 'Learn an enemy\'s true name to defeat it.' - x: 68.44 - y: 10.70 - nextLevels: - continue: 'the-raised-sword' - unlocksHero: { - img: '/file/db/thang.type/53e12be0d042f23505c3023b/portrait.png' - originalID: '53e12be0d042f23505c3023b' - } - } - { - name: 'Favorable Odds' - type: 'hero' - id: 'favorable-odds' - original: '5452972f57e83800009730de' - description: 'Test out your battle skills by defeating more munchkins.' - x: 88.25 - y: 14.92 - nextLevels: - continue: 'the-raised-sword' - practice: true - requiresSubscription: true - } - { - name: 'The Raised Sword' - type: 'hero' - id: 'the-raised-sword' - original: '5418aec24c16460000ab9aa6' - description: 'Learn to equip yourself for combat.' - x: 81.51 - y: 17.92 - nextLevels: - continue: 'haunted-kithmaze' - } - { - name: 'Haunted Kithmaze' - type: 'hero' - id: 'haunted-kithmaze' - original: '545a5914d820eb0000f6dc0a' - description: 'The builders of Kithgard constructed many mazes to confuse travelers.' - x: 78 - y: 29 - nextLevels: - continue: 'the-second-kithmaze' - } - { - name: 'Riddling Kithmaze' - type: 'hero' - id: 'riddling-kithmaze' - original: '5418b9d64c16460000ab9ab4' - description: 'If at first you go astray, change your loop to find the way.' - x: 69.97 - y: 28.03 - nextLevels: - continue: 'descending-further' - practice: true - requiresSubscription: true - } - { - name: 'Descending Further' - type: 'hero' - id: 'descending-further' - original: '5452a84d57e83800009730e4' - description: 'Another day, another maze.' - x: 61.68 - y: 22.80 - nextLevels: - continue: 'the-second-kithmaze' - practice: true - requiresSubscription: true - } - { - name: 'The Second Kithmaze' - type: 'hero' - id: 'the-second-kithmaze' - original: '5418cf256bae62f707c7e1c3' - description: 'Many have tried, few have found their way through this maze.' - x: 54.49 - y: 26.49 - nextLevels: - continue: 'dread-door' - } - { - name: 'Dread Door' - type: 'hero' - id: 'dread-door' - original: '5418d40f4c16460000ab9ac2' - description: 'Behind a dread door lies a chest full of riches.' - x: 60.52 - y: 33.70 - nextLevels: - continue: 'known-enemy' - } - { - name: 'Known Enemy' - type: 'hero' - id: 'known-enemy' - original: '5452adea57e83800009730ee' - description: 'Begin to use variables in your battles.' - x: 67 - y: 39 - nextLevels: - continue: 'master-of-names' - } - { - name: 'Master of Names' - type: 'hero' - id: 'master-of-names' - original: '5452c3ce57e83800009730f7' - description: 'Use your glasses to defend yourself from the Kithmen.' - x: 75 - y: 46 - nextLevels: - continue: 'lowly-kithmen' - } - { - name: 'Lowly Kithmen' - type: 'hero' - id: 'lowly-kithmen' - original: '541b24511ccc8eaae19f3c1f' - description: 'Now that you can see them, they\'re everywhere!' - x: 85 - y: 40 - nextLevels: - continue: 'closing-the-distance' - } - { - name: 'Closing the Distance' - type: 'hero' - id: 'closing-the-distance' - original: '541b288e1ccc8eaae19f3c25' - description: 'Kithmen are not the only ones to stand in your way.' - x: 93 - y: 47 - nextLevels: - continue: 'the-final-kithmaze' - } - { - name: 'Tactical Strike' - type: 'hero' - id: 'tactical-strike' - original: '5452cfa706a59e000067e4f5' - description: 'They\'re, uh, coming right for us! Sneak up behind them.' - x: 83.23 - y: 52.73 - nextLevels: - continue: 'the-final-kithmaze' - practice: true - requiresSubscription: true - } - { - name: 'The Final Kithmaze' - type: 'hero' - id: 'the-final-kithmaze' - original: '541b434e1ccc8eaae19f3c33' - description: 'To escape you must find your way through an Elder Kithman\'s maze.' - x: 86.95 - y: 64.70 - nextLevels: - continue: 'kithgard-gates' - } - { - name: 'The Gauntlet' - type: 'hero' - id: 'the-gauntlet' - original: '5452d8b906a59e000067e4fa' - description: 'Rush for the stairs, battling foes at every turn.' - x: 76.50 - y: 72.69 - nextLevels: - continue: 'kithgard-gates' - practice: true - requiresSubscription: true - } - { - name: 'Kithgard Gates' - type: 'hero' - id: 'kithgard-gates' - original: '541c9a30c6362edfb0f34479' - description: 'Escape the Kithgard dungeons and don\'t let the guardians get you.' - x: 89 - y: 82 - nextLevels: - continue: 'defense-of-plainswood' - } - { - name: 'Cavern Survival' - type: 'hero-ladder' - id: 'cavern-survival' - original: '544437e0645c0c0000c3291d' - description: 'Stay alive longer than your opponent amidst hordes of ogres!' - x: 17.54 - y: 78.39 - } -] - -options = - 'dungeons-of-kithgard': - disableSpaces: true - hidesSubmitUntilRun: true - hidesPlayButton: true - hidesRunShortcut: true - hidesHUD: true - hidesSay: true - hidesCodeToolbar: true - hidesRealTimePlayback: true - requiredGear: {feet: 'simple-boots'} - restrictedGear: {feet: 'leather-boots'} - requiredCode: ['moveRight'] - 'gems-in-the-deep': - disableSpaces: true - hidesSubmitUntilRun: true - hidesPlayButton: true - hidesRunShortcut: true - hidesHUD: true - hidesSay: true - hidesCodeToolbar: true - hidesRealTimePlayback: true - requiredGear: {feet: 'simple-boots'} - restrictedGear: {feet: 'leather-boots'} - 'shadow-guard': - disableSpaces: true - hidesSubmitUntilRun: true - hidesPlayButton: true - hidesRunShortcut: true - hidesHUD: true - hidesSay: true - hidesCodeToolbar: true - hidesRealTimePlayback: true - requiredGear: {feet: 'simple-boots'} - restrictedGear: {feet: 'leather-boots', 'right-hand': 'simple-sword'} - 'kounter-kithwise': - disableSpaces: true - hidesPlayButton: true - hidesRunShortcut: true - hidesHUD: true - hidesSay: true - hidesCodeToolbar: true - hidesRealTimePlayback: true - requiredGear: {feet: 'simple-boots'} - restrictedGear: {feet: 'leather-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i'} - 'crawlways-of-kithgard': - hidesPlayButton: true - hidesRunShortcut: true - hidesHUD: true - hidesSay: true - hidesCodeToolbar: true - hidesRealTimePlayback: true - requiredGear: {feet: 'simple-boots'} - restrictedGear: {feet: 'leather-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i'} - 'forgetful-gemsmith': - disableSpaces: true - hidesPlayButton: true - hidesRunShortcut: true - hidesHUD: true - hidesSay: true - hidesCodeToolbar: true - hidesRealTimePlayback: true - requiredGear: {feet: 'simple-boots'} - restrictedGear: {feet: 'leather-boots', 'programming-book': 'programmaticon-i'} - 'true-names': - disableSpaces: true - hidesPlayButton: true - hidesRunShortcut: true - hidesHUD: true - hidesSay: true - hidesCodeToolbar: true - hidesRealTimePlayback: true - requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', waist: 'leather-belt'} - restrictedGear: {feet: 'leather-boots', 'programming-book': 'programmaticon-i'} - requiredCode: ['Brak'] - 'favorable-odds': - disableSpaces: true - hidesPlayButton: true - hidesRunShortcut: true - hidesHUD: true - hidesSay: true - hidesCodeToolbar: true - hidesRealTimePlayback: true - requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword'} - restrictedGear: {feet: 'leather-boots', 'programming-book': 'programmaticon-i'} - 'the-raised-sword': - disableSpaces: true - hidesPlayButton: true - hidesRunShortcut: true - hidesHUD: true - hidesSay: true - hidesCodeToolbar: true - hidesRealTimePlayback: true - requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'tarnished-bronze-breastplate'} - restrictedGear: {feet: 'leather-boots', 'programming-book': 'programmaticon-i'} - 'the-first-kithmaze': - hidesRunShortcut: true - hidesHUD: true - hidesSay: true - hidesCodeToolbar: true - hidesRealTimePlayback: true - requiredGear: {feet: 'simple-boots', 'programming-book': 'programmaticon-i'} - restrictedGear: {feet: 'leather-boots'} - requiredCode: ['loop'] - 'haunted-kithmaze': - hidesRunShortcut: true - hidesHUD: true - hidesSay: true - hidesCodeToolbar: true - hidesRealTimePlayback: true - moveRightLoopSnippet: true - requiredGear: {feet: 'simple-boots', 'programming-book': 'programmaticon-i'} - restrictedGear: {feet: 'leather-boots'} - requiredCode: ['loop'] - 'descending-further': - hidesHUD: true - hidesSay: true - hidesCodeToolbar: true - hidesRealTimePlayback: true - requiredGear: {feet: 'simple-boots', 'programming-book': 'programmaticon-i'} - restrictedGear: {feet: 'leather-boots'} - 'the-second-kithmaze': - hidesHUD: true - hidesSay: true - hidesCodeToolbar: true - hidesRealTimePlayback: true - moveRightLoopSnippet: true - requiredGear: {feet: 'simple-boots', 'programming-book': 'programmaticon-i'} - restrictedGear: {feet: 'leather-boots'} - 'dread-door': - hidesHUD: true - hidesSay: true - hidesCodeToolbar: true - hidesRealTimePlayback: true - requiredGear: {'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i'} - restrictedGear: {feet: 'leather-boots'} - 'known-enemy': - hidesHUD: true - hidesSay: true - hidesCodeToolbar: true - hidesRealTimePlayback: true - requiredGear: {'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i', torso: 'tarnished-bronze-breastplate'} - restrictedGear: {feet: 'leather-boots'} - suspectCode: [{name: 'enemy-in-quotes', pattern: '[\'"]enemy'}] # ' - 'master-of-names': - hidesHUD: true - hidesSay: true - hidesCodeToolbar: true - hidesRealTimePlayback: true - requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses', torso: 'tarnished-bronze-breastplate'} - restrictedGear: {feet: 'leather-boots'} - requiredCode: ['findNearestEnemy'] - suspectCode: [{name: 'lone-find-nearest-enemy', pattern: '^[ ]*(self|this|@)?[:.]?findNearestEnemy()'}] - 'lowly-kithmen': - hidesHUD: true - hidesSay: true - hidesCodeToolbar: true - hidesRealTimePlayback: true - requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses', torso: 'tarnished-bronze-breastplate'} - restrictedGear: {feet: 'leather-boots'} - requiredCode: ['findNearestEnemy'] - suspectCode: [{name: 'lone-find-nearest-enemy', pattern: '^[ ]*(self|this|@)?[:.]?findNearestEnemy()'}] - 'closing-the-distance': - hidesHUD: true - hidesSay: true - hidesCodeToolbar: true - hidesRealTimePlayback: true - requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'tarnished-bronze-breastplate', eyes: 'crude-glasses'} - restrictedGear: {feet: 'leather-boots'} - suspectCode: [{name: 'lone-find-nearest-enemy', pattern: '^[ ]*(self|this|@)?[:.]?findNearestEnemy()'}] - 'tactical-strike': - hidesHUD: true - hidesSay: true - hidesCodeToolbar: true - hidesRealTimePlayback: true - requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'tarnished-bronze-breastplate', eyes: 'crude-glasses'} - restrictedGear: {feet: 'leather-boots'} - suspectCode: [{name: 'lone-find-nearest-enemy', pattern: '^[ ]*(self|this|@)?[:.]?findNearestEnemy()'}] - 'the-final-kithmaze': - hidesHUD: true - hidesSay: true - hidesCodeToolbar: true - hidesRealTimePlayback: true - requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'tarnished-bronze-breastplate', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'} - suspectCode: [{name: 'lone-find-nearest-enemy', pattern: '^[ ]*(self|this|@)?[:.]?findNearestEnemy()'}] - 'the-gauntlet': - hidesHUD: true - hidesSay: true - hidesCodeToolbar: true - hidesRealTimePlayback: true - requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'tarnished-bronze-breastplate', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'} - restrictedGear: {feet: 'leather-boots', 'right-hand': 'crude-builders-hammer'} - suspectCode: [{name: 'lone-find-nearest-enemy', pattern: '^[ ]*(self|this|@)?[:.]?findNearestEnemy()'}] - 'kithgard-gates': - hidesSay: true - hidesCodeToolbar: true - hidesRealTimePlayback: true - requiredGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', torso: 'tarnished-bronze-breastplate'} - restrictedGear: {'right-hand': 'simple-sword'} - 'defense-of-plainswood': - hidesRealTimePlayback: true - hidesCodeToolbar: true - requiredGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer'} - restrictedGear: {'right-hand': 'simple-sword'} - 'winding-trail': - hidesRealTimePlayback: true - hidesCodeToolbar: true - requiredGear: {feet: 'leather-boots', 'right-hand': 'crude-builders-hammer'} - restrictedGear: {feet: 'simple-boots', 'right-hand': 'simple-sword'} - 'patrol-buster': - hidesRealTimePlayback: true - hidesCodeToolbar: true - requiredGear: {feet: 'leather-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses'} - restrictedGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-i'} - 'endangered-burl': - hidesRealTimePlayback: true - hidesCodeToolbar: true - requiredGear: {feet: 'leather-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses'} - restrictedGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-i'} - 'village-guard': - hidesCodeToolbar: true - lockDefaultCode: true - requiredGear: {feet: 'leather-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses'} - restrictedGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-i'} - 'thornbush-farm': - hidesCodeToolbar: true - lockDefaultCode: true - requiredGear: {feet: 'leather-boots', 'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses'} - restrictedGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i'} - requiredCode: ['topEnemy'] - 'back-to-back': - hidesCodeToolbar: true - requiredGear: {feet: 'leather-boots', torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses', 'right-hand': 'simple-sword', 'left-hand': 'wooden-shield'} - restrictedGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-i'} - 'ogre-encampment': - requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses', 'right-hand': 'simple-sword', 'left-hand': 'wooden-shield'} - restrictedGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-i'} - 'woodland-cleaver': - requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses', 'right-hand': 'long-sword', 'left-hand': 'wooden-shield', wrists: 'sundial-wristwatch', feet: 'leather-boots'} - restrictedGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i'} - 'shield-rush': - requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'} - restrictedGear: {'left-hand': 'wooden-shield', 'programming-book': 'programmaticon-i'} - -# Warrior branch - 'peasant-protection': - requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'} - restrictedGear: {eyes: 'crude-glasses', 'programming-book': 'programmaticon-i'} - 'munchkin-swarm': - requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'} - restrictedGear: {'programming-book': 'programmaticon-i', eyes: 'crude-glasses'} - -# Ranger branch - 'munchkin-harvest': - requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'} - restrictedGear: {'programming-book': 'programmaticon-i'} - allowedHeroes: ['captain', 'knight', 'samurai'] - 'swift-dagger': - requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'crude-crossbow', 'left-hand': 'crude-dagger', wrists: 'sundial-wristwatch'} - restrictedGear: {eyes: 'crude-glasses', 'programming-book': 'programmaticon-i'} - allowedHeroes: ['ninja', 'trapper', 'forest-archer'] - 'shrapnel': - requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'crude-crossbow', 'left-hand': 'weak-charge', wrists: 'sundial-wristwatch'} - restrictedGear: {eyes: 'crude-glasses', 'left-hand': 'crude-dagger', 'programming-book': 'programmaticon-i'} - allowedHeroes: ['ninja', 'trapper', 'forest-archer'] - -# Wizard branch - 'arcane-ally': - requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'} - restrictedGear: {eyes: 'crude-glasses', 'programming-book': 'programmaticon-i'} - allowedHeroes: ['captain', 'knight', 'samurai'] - 'touch-of-death': - requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'enchanted-stick', 'left-hand': 'unholy-tome-i', wrists: 'sundial-wristwatch'} - restrictedGear: {'programming-book': 'programmaticon-i', eyes: 'crude-glasses'} - allowedHeroes: ['librarian', 'potion-master', 'sorcerer'] - 'bonemender': - requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'enchanted-stick', 'left-hand': 'book-of-life-i', wrists: 'sundial-wristwatch'} - restrictedGear: {'left-hand': 'unholy-tome-i', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'} - requiredCode: ['canCast'] - allowedHeroes: ['librarian', 'potion-master', 'sorcerer'] - - 'coinucopia': - requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags'} - restrictedGear: {'programming-book': 'programmaticon-i', eyes: 'crude-glasses'} - 'copper-meadows': - requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags', eyes: 'wooden-glasses'} - restrictedGear: {'programming-book': 'programmaticon-i', eyes: 'crude-glasses'} - 'drop-the-flag': - requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags', eyes: 'wooden-glasses', 'right-hand': 'crude-builders-hammer'} - restrictedGear: {'right-hand': 'long-sword', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'} - 'deadly-pursuit': - requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags', eyes: 'wooden-glasses', 'right-hand': 'crude-builders-hammer'} - restrictedGear: {'right-hand': 'long-sword', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'} - 'rich-forager': - requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags', eyes: 'wooden-glasses', torso: 'tarnished-bronze-breastplate', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield'} - restrictedGear: {'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'} - 'multiplayer-treasure-grove': - requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags', eyes: 'wooden-glasses', torso: 'tarnished-bronze-breastplate'} - restrictedGear: {'programming-book': 'programmaticon-i', eyes: 'crude-glasses'} - 'siege-of-stonehold': - requiredGear: {} - restrictedGear: {} - -# Desert - 'the-dunes': - requiredGear: {} - restrictedGear: {} - 'the-mighty-sand-yak': - requiredGear: {} - restrictedGear: {} - 'oasis': - requiredGear: {} - restrictedGear: {} +#campaign = { +# name: 'Dungeon' +# levels: {} +#} +# +# +#levels = [ +# { +# name: 'Dungeons of Kithgard' +# type: 'hero' +# id: 'dungeons-of-kithgard' +# original: '5411cb3769152f1707be029c' +# description: 'Grab the gem, but touch nothing else. Start here.' +# x: 14 +# y: 15.5 +# nextLevels: +# continue: 'gems-in-the-deep' +# } +# { +# name: 'Gems in the Deep' +# type: 'hero' +# id: 'gems-in-the-deep' +# original: '54173c90844506ae0195a0b4' +# description: 'Quickly collect the gems; you will need them.' +# x: 29 +# y: 12 +# nextLevels: +# continue: 'shadow-guard' +# } +# { +# name: 'Shadow Guard' +# type: 'hero' +# id: 'shadow-guard' +# original: '54174347844506ae0195a0b8' +# description: 'Evade the Kithgard minion.' +# x: 40.54 +# y: 11.03 +# nextLevels: +# continue: 'forgetful-gemsmith' +# } +# { +# name: 'Kounter Kithwise' +# type: 'hero' +# id: 'kounter-kithwise' +# original: '54527a6257e83800009730c7' +# description: 'Practice your evasion skills with more guards.' +# x: 35.37 +# y: 20.61 +# nextLevels: +# continue: 'crawlways-of-kithgard' +# practice: true +# requiresSubscription: true +# } +# { +# name: 'Crawlways of Kithgard' +# type: 'hero' +# id: 'crawlways-of-kithgard' +# original: '545287ef57e83800009730d5' +# description: 'Dart in and grab the gem–at the right moment.' +# x: 36.48 +# y: 29.03 +# nextLevels: +# continue: 'forgetful-gemsmith' +# practice: true +# requiresSubscription: true +# } +# { +# name: 'Forgetful Gemsmith' +# type: 'hero' +# id: 'forgetful-gemsmith' +# original: '544a98f62d002f0000fe331a' +# description: 'Grab even more gems as you practice moving.' +# x: 54.98 +# y: 10.53 +# nextLevels: +# continue: 'true-names' +# } +# { +# name: 'True Names' +# type: 'hero' +# id: 'true-names' +# original: '541875da4c16460000ab990f' +# description: 'Learn an enemy\'s true name to defeat it.' +# x: 68.44 +# y: 10.70 +# nextLevels: +# continue: 'the-raised-sword' +# unlocksHero: { +# img: '/file/db/thang.type/53e12be0d042f23505c3023b/portrait.png' +# originalID: '53e12be0d042f23505c3023b' +# } +# } +# { +# name: 'Favorable Odds' +# type: 'hero' +# id: 'favorable-odds' +# original: '5452972f57e83800009730de' +# description: 'Test out your battle skills by defeating more munchkins.' +# x: 88.25 +# y: 14.92 +# nextLevels: +# continue: 'the-raised-sword' +# practice: true +# requiresSubscription: true +# } +# { +# name: 'The Raised Sword' +# type: 'hero' +# id: 'the-raised-sword' +# original: '5418aec24c16460000ab9aa6' +# description: 'Learn to equip yourself for combat.' +# x: 81.51 +# y: 17.92 +# nextLevels: +# continue: 'haunted-kithmaze' +# } +# { +# name: 'Haunted Kithmaze' +# type: 'hero' +# id: 'haunted-kithmaze' +# original: '545a5914d820eb0000f6dc0a' +# description: 'The builders of Kithgard constructed many mazes to confuse travelers.' +# x: 78 +# y: 29 +# nextLevels: +# continue: 'the-second-kithmaze' +# } +# { +# name: 'Riddling Kithmaze' +# type: 'hero' +# id: 'riddling-kithmaze' +# original: '5418b9d64c16460000ab9ab4' +# description: 'If at first you go astray, change your loop to find the way.' +# x: 69.97 +# y: 28.03 +# nextLevels: +# continue: 'descending-further' +# practice: true +# requiresSubscription: true +# } +# { +# name: 'Descending Further' +# type: 'hero' +# id: 'descending-further' +# original: '5452a84d57e83800009730e4' +# description: 'Another day, another maze.' +# x: 61.68 +# y: 22.80 +# nextLevels: +# continue: 'the-second-kithmaze' +# practice: true +# requiresSubscription: true +# } +# { +# name: 'The Second Kithmaze' +# type: 'hero' +# id: 'the-second-kithmaze' +# original: '5418cf256bae62f707c7e1c3' +# description: 'Many have tried, few have found their way through this maze.' +# x: 54.49 +# y: 26.49 +# nextLevels: +# continue: 'dread-door' +# } +# { +# name: 'Dread Door' +# type: 'hero' +# id: 'dread-door' +# original: '5418d40f4c16460000ab9ac2' +# description: 'Behind a dread door lies a chest full of riches.' +# x: 60.52 +# y: 33.70 +# nextLevels: +# continue: 'known-enemy' +# } +# { +# name: 'Known Enemy' +# type: 'hero' +# id: 'known-enemy' +# original: '5452adea57e83800009730ee' +# description: 'Begin to use variables in your battles.' +# x: 67 +# y: 39 +# nextLevels: +# continue: 'master-of-names' +# } +# { +# name: 'Master of Names' +# type: 'hero' +# id: 'master-of-names' +# original: '5452c3ce57e83800009730f7' +# description: 'Use your glasses to defend yourself from the Kithmen.' +# x: 75 +# y: 46 +# nextLevels: +# continue: 'lowly-kithmen' +# } +# { +# name: 'Lowly Kithmen' +# type: 'hero' +# id: 'lowly-kithmen' +# original: '541b24511ccc8eaae19f3c1f' +# description: 'Now that you can see them, they\'re everywhere!' +# x: 85 +# y: 40 +# nextLevels: +# continue: 'closing-the-distance' +# } +# { +# name: 'Closing the Distance' +# type: 'hero' +# id: 'closing-the-distance' +# original: '541b288e1ccc8eaae19f3c25' +# description: 'Kithmen are not the only ones to stand in your way.' +# x: 93 +# y: 47 +# nextLevels: +# continue: 'the-final-kithmaze' +# } +# { +# name: 'Tactical Strike' +# type: 'hero' +# id: 'tactical-strike' +# original: '5452cfa706a59e000067e4f5' +# description: 'They\'re, uh, coming right for us! Sneak up behind them.' +# x: 83.23 +# y: 52.73 +# nextLevels: +# continue: 'the-final-kithmaze' +# practice: true +# requiresSubscription: true +# } +# { +# name: 'The Final Kithmaze' +# type: 'hero' +# id: 'the-final-kithmaze' +# original: '541b434e1ccc8eaae19f3c33' +# description: 'To escape you must find your way through an Elder Kithman\'s maze.' +# x: 86.95 +# y: 64.70 +# nextLevels: +# continue: 'kithgard-gates' +# } +# { +# name: 'The Gauntlet' +# type: 'hero' +# id: 'the-gauntlet' +# original: '5452d8b906a59e000067e4fa' +# description: 'Rush for the stairs, battling foes at every turn.' +# x: 76.50 +# y: 72.69 +# nextLevels: +# continue: 'kithgard-gates' +# practice: true +# requiresSubscription: true +# } +# { +# name: 'Kithgard Gates' +# type: 'hero' +# id: 'kithgard-gates' +# original: '541c9a30c6362edfb0f34479' +# description: 'Escape the Kithgard dungeons and don\'t let the guardians get you.' +# x: 89 +# y: 82 +# nextLevels: +# continue: 'defense-of-plainswood' +# } +# { +# name: 'Cavern Survival' +# type: 'hero-ladder' +# id: 'cavern-survival' +# original: '544437e0645c0c0000c3291d' +# description: 'Stay alive longer than your opponent amidst hordes of ogres!' +# x: 17.54 +# y: 78.39 +# } +#] +# +#options = +# 'dungeons-of-kithgard': +# disableSpaces: true +# hidesSubmitUntilRun: true +# hidesPlayButton: true +# hidesRunShortcut: true +# hidesHUD: true +# hidesSay: true +# hidesCodeToolbar: true +# hidesRealTimePlayback: true +# requiredGear: {feet: 'simple-boots'} +# restrictedGear: {feet: 'leather-boots'} +# requiredCode: ['moveRight'] +# 'gems-in-the-deep': +# disableSpaces: true +# hidesSubmitUntilRun: true +# hidesPlayButton: true +# hidesRunShortcut: true +# hidesHUD: true +# hidesSay: true +# hidesCodeToolbar: true +# hidesRealTimePlayback: true +# requiredGear: {feet: 'simple-boots'} +# restrictedGear: {feet: 'leather-boots'} +# 'shadow-guard': +# disableSpaces: true +# hidesSubmitUntilRun: true +# hidesPlayButton: true +# hidesRunShortcut: true +# hidesHUD: true +# hidesSay: true +# hidesCodeToolbar: true +# hidesRealTimePlayback: true +# requiredGear: {feet: 'simple-boots'} +# restrictedGear: {feet: 'leather-boots', 'right-hand': 'simple-sword'} +# 'kounter-kithwise': +# disableSpaces: true +# hidesPlayButton: true +# hidesRunShortcut: true +# hidesHUD: true +# hidesSay: true +# hidesCodeToolbar: true +# hidesRealTimePlayback: true +# requiredGear: {feet: 'simple-boots'} +# restrictedGear: {feet: 'leather-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i'} +# 'crawlways-of-kithgard': +# hidesPlayButton: true +# hidesRunShortcut: true +# hidesHUD: true +# hidesSay: true +# hidesCodeToolbar: true +# hidesRealTimePlayback: true +# requiredGear: {feet: 'simple-boots'} +# restrictedGear: {feet: 'leather-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i'} +# 'forgetful-gemsmith': +# disableSpaces: true +# hidesPlayButton: true +# hidesRunShortcut: true +# hidesHUD: true +# hidesSay: true +# hidesCodeToolbar: true +# hidesRealTimePlayback: true +# requiredGear: {feet: 'simple-boots'} +# restrictedGear: {feet: 'leather-boots', 'programming-book': 'programmaticon-i'} +# 'true-names': +# disableSpaces: true +# hidesPlayButton: true +# hidesRunShortcut: true +# hidesHUD: true +# hidesSay: true +# hidesCodeToolbar: true +# hidesRealTimePlayback: true +# requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', waist: 'leather-belt'} +# restrictedGear: {feet: 'leather-boots', 'programming-book': 'programmaticon-i'} +# requiredCode: ['Brak'] +# 'favorable-odds': +# disableSpaces: true +# hidesPlayButton: true +# hidesRunShortcut: true +# hidesHUD: true +# hidesSay: true +# hidesCodeToolbar: true +# hidesRealTimePlayback: true +# requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword'} +# restrictedGear: {feet: 'leather-boots', 'programming-book': 'programmaticon-i'} +# 'the-raised-sword': +# disableSpaces: true +# hidesPlayButton: true +# hidesRunShortcut: true +# hidesHUD: true +# hidesSay: true +# hidesCodeToolbar: true +# hidesRealTimePlayback: true +# requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'tarnished-bronze-breastplate'} +# restrictedGear: {feet: 'leather-boots', 'programming-book': 'programmaticon-i'} +# 'the-first-kithmaze': +# hidesRunShortcut: true +# hidesHUD: true +# hidesSay: true +# hidesCodeToolbar: true +# hidesRealTimePlayback: true +# requiredGear: {feet: 'simple-boots', 'programming-book': 'programmaticon-i'} +# restrictedGear: {feet: 'leather-boots'} +# requiredCode: ['loop'] +# 'haunted-kithmaze': +# hidesRunShortcut: true +# hidesHUD: true +# hidesSay: true +# hidesCodeToolbar: true +# hidesRealTimePlayback: true +# moveRightLoopSnippet: true +# requiredGear: {feet: 'simple-boots', 'programming-book': 'programmaticon-i'} +# restrictedGear: {feet: 'leather-boots'} +# requiredCode: ['loop'] +# 'descending-further': +# hidesHUD: true +# hidesSay: true +# hidesCodeToolbar: true +# hidesRealTimePlayback: true +# requiredGear: {feet: 'simple-boots', 'programming-book': 'programmaticon-i'} +# restrictedGear: {feet: 'leather-boots'} +# 'the-second-kithmaze': +# hidesHUD: true +# hidesSay: true +# hidesCodeToolbar: true +# hidesRealTimePlayback: true +# moveRightLoopSnippet: true +# requiredGear: {feet: 'simple-boots', 'programming-book': 'programmaticon-i'} +# restrictedGear: {feet: 'leather-boots'} +# 'dread-door': +# hidesHUD: true +# hidesSay: true +# hidesCodeToolbar: true +# hidesRealTimePlayback: true +# requiredGear: {'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i'} +# restrictedGear: {feet: 'leather-boots'} +# 'known-enemy': +# hidesHUD: true +# hidesSay: true +# hidesCodeToolbar: true +# hidesRealTimePlayback: true +# requiredGear: {'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i', torso: 'tarnished-bronze-breastplate'} +# restrictedGear: {feet: 'leather-boots'} +# suspectCode: [{name: 'enemy-in-quotes', pattern: '[\'"]enemy'}] # ' +# 'master-of-names': +# hidesHUD: true +# hidesSay: true +# hidesCodeToolbar: true +# hidesRealTimePlayback: true +# requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses', torso: 'tarnished-bronze-breastplate'} +# restrictedGear: {feet: 'leather-boots'} +# requiredCode: ['findNearestEnemy'] +# suspectCode: [{name: 'lone-find-nearest-enemy', pattern: '^[ ]*(self|this|@)?[:.]?findNearestEnemy()'}] +# 'lowly-kithmen': +# hidesHUD: true +# hidesSay: true +# hidesCodeToolbar: true +# hidesRealTimePlayback: true +# requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses', torso: 'tarnished-bronze-breastplate'} +# restrictedGear: {feet: 'leather-boots'} +# requiredCode: ['findNearestEnemy'] +# suspectCode: [{name: 'lone-find-nearest-enemy', pattern: '^[ ]*(self|this|@)?[:.]?findNearestEnemy()'}] +# 'closing-the-distance': +# hidesHUD: true +# hidesSay: true +# hidesCodeToolbar: true +# hidesRealTimePlayback: true +# requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'tarnished-bronze-breastplate', eyes: 'crude-glasses'} +# restrictedGear: {feet: 'leather-boots'} +# suspectCode: [{name: 'lone-find-nearest-enemy', pattern: '^[ ]*(self|this|@)?[:.]?findNearestEnemy()'}] +# 'tactical-strike': +# hidesHUD: true +# hidesSay: true +# hidesCodeToolbar: true +# hidesRealTimePlayback: true +# requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'tarnished-bronze-breastplate', eyes: 'crude-glasses'} +# restrictedGear: {feet: 'leather-boots'} +# suspectCode: [{name: 'lone-find-nearest-enemy', pattern: '^[ ]*(self|this|@)?[:.]?findNearestEnemy()'}] +# 'the-final-kithmaze': +# hidesHUD: true +# hidesSay: true +# hidesCodeToolbar: true +# hidesRealTimePlayback: true +# requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'tarnished-bronze-breastplate', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'} +# suspectCode: [{name: 'lone-find-nearest-enemy', pattern: '^[ ]*(self|this|@)?[:.]?findNearestEnemy()'}] +# 'the-gauntlet': +# hidesHUD: true +# hidesSay: true +# hidesCodeToolbar: true +# hidesRealTimePlayback: true +# requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'tarnished-bronze-breastplate', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'} +# restrictedGear: {feet: 'leather-boots', 'right-hand': 'crude-builders-hammer'} +# suspectCode: [{name: 'lone-find-nearest-enemy', pattern: '^[ ]*(self|this|@)?[:.]?findNearestEnemy()'}] +# 'kithgard-gates': +# hidesSay: true +# hidesCodeToolbar: true +# hidesRealTimePlayback: true +# requiredGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', torso: 'tarnished-bronze-breastplate'} +# restrictedGear: {'right-hand': 'simple-sword'} +# 'defense-of-plainswood': +# hidesRealTimePlayback: true +# hidesCodeToolbar: true +# requiredGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer'} +# restrictedGear: {'right-hand': 'simple-sword'} +# 'winding-trail': +# hidesRealTimePlayback: true +# hidesCodeToolbar: true +# requiredGear: {feet: 'leather-boots', 'right-hand': 'crude-builders-hammer'} +# restrictedGear: {feet: 'simple-boots', 'right-hand': 'simple-sword'} +# 'patrol-buster': +# hidesRealTimePlayback: true +# hidesCodeToolbar: true +# requiredGear: {feet: 'leather-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses'} +# restrictedGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-i'} +# 'endangered-burl': +# hidesRealTimePlayback: true +# hidesCodeToolbar: true +# requiredGear: {feet: 'leather-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses'} +# restrictedGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-i'} +# 'village-guard': +# hidesCodeToolbar: true +# lockDefaultCode: true +# requiredGear: {feet: 'leather-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses'} +# restrictedGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-i'} +# 'thornbush-farm': +# hidesCodeToolbar: true +# lockDefaultCode: true +# requiredGear: {feet: 'leather-boots', 'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses'} +# restrictedGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i'} +# requiredCode: ['topEnemy'] +# 'back-to-back': +# hidesCodeToolbar: true +# requiredGear: {feet: 'leather-boots', torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses', 'right-hand': 'simple-sword', 'left-hand': 'wooden-shield'} +# restrictedGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-i'} +# 'ogre-encampment': +# requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses', 'right-hand': 'simple-sword', 'left-hand': 'wooden-shield'} +# restrictedGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-i'} +# 'woodland-cleaver': +# requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses', 'right-hand': 'long-sword', 'left-hand': 'wooden-shield', wrists: 'sundial-wristwatch', feet: 'leather-boots'} +# restrictedGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i'} +# 'shield-rush': +# requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'} +# restrictedGear: {'left-hand': 'wooden-shield', 'programming-book': 'programmaticon-i'} +# +## Warrior branch +# 'peasant-protection': +# requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'} +# restrictedGear: {eyes: 'crude-glasses', 'programming-book': 'programmaticon-i'} +# 'munchkin-swarm': +# requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'} +# restrictedGear: {'programming-book': 'programmaticon-i', eyes: 'crude-glasses'} +# +## Ranger branch +# 'munchkin-harvest': +# requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'} +# restrictedGear: {'programming-book': 'programmaticon-i'} +# allowedHeroes: ['captain', 'knight', 'samurai'] +# 'swift-dagger': +# requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'crude-crossbow', 'left-hand': 'crude-dagger', wrists: 'sundial-wristwatch'} +# restrictedGear: {eyes: 'crude-glasses', 'programming-book': 'programmaticon-i'} +# allowedHeroes: ['ninja', 'trapper', 'forest-archer'] +# 'shrapnel': +# requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'crude-crossbow', 'left-hand': 'weak-charge', wrists: 'sundial-wristwatch'} +# restrictedGear: {eyes: 'crude-glasses', 'left-hand': 'crude-dagger', 'programming-book': 'programmaticon-i'} +# allowedHeroes: ['ninja', 'trapper', 'forest-archer'] +# +## Wizard branch +# 'arcane-ally': +# requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'} +# restrictedGear: {eyes: 'crude-glasses', 'programming-book': 'programmaticon-i'} +# allowedHeroes: ['captain', 'knight', 'samurai'] +# 'touch-of-death': +# requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'enchanted-stick', 'left-hand': 'unholy-tome-i', wrists: 'sundial-wristwatch'} +# restrictedGear: {'programming-book': 'programmaticon-i', eyes: 'crude-glasses'} +# allowedHeroes: ['librarian', 'potion-master', 'sorcerer'] +# 'bonemender': +# requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'enchanted-stick', 'left-hand': 'book-of-life-i', wrists: 'sundial-wristwatch'} +# restrictedGear: {'left-hand': 'unholy-tome-i', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'} +# requiredCode: ['canCast'] +# allowedHeroes: ['librarian', 'potion-master', 'sorcerer'] +# +# 'coinucopia': +# requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags'} +# restrictedGear: {'programming-book': 'programmaticon-i', eyes: 'crude-glasses'} +# 'copper-meadows': +# requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags', eyes: 'wooden-glasses'} +# restrictedGear: {'programming-book': 'programmaticon-i', eyes: 'crude-glasses'} +# 'drop-the-flag': +# requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags', eyes: 'wooden-glasses', 'right-hand': 'crude-builders-hammer'} +# restrictedGear: {'right-hand': 'long-sword', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'} +# 'deadly-pursuit': +# requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags', eyes: 'wooden-glasses', 'right-hand': 'crude-builders-hammer'} +# restrictedGear: {'right-hand': 'long-sword', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'} +# 'rich-forager': +# requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags', eyes: 'wooden-glasses', torso: 'tarnished-bronze-breastplate', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield'} +# restrictedGear: {'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'} +# 'multiplayer-treasure-grove': +# requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags', eyes: 'wooden-glasses', torso: 'tarnished-bronze-breastplate'} +# restrictedGear: {'programming-book': 'programmaticon-i', eyes: 'crude-glasses'} +# 'siege-of-stonehold': +# requiredGear: {} +# restrictedGear: {} +# +## Desert +# 'the-dunes': +# requiredGear: {} +# restrictedGear: {} +# 'the-mighty-sand-yak': +# requiredGear: {} +# restrictedGear: {} +# 'oasis': +# requiredGear: {} +# restrictedGear: {} diff --git a/app/views/play/CampaignView.coffee b/app/views/play/CampaignView.coffee index 9acdd39ef..16d829556 100644 --- a/app/views/play/CampaignView.coffee +++ b/app/views/play/CampaignView.coffee @@ -128,6 +128,9 @@ module.exports = class WorldMapView extends RootView @fullyRendered = true @render() @preloadTopHeroes() unless me.get('heroConfig')?.thangType + + setCampaign: (@campaign) -> + @render() onSubscribed: -> @requiresSubscription = false From 0bdec68cfc2997a1d03458e516ecedd7c7f389a9 Mon Sep 17 00:00:00 2001 From: Scott Erickson <sderickson@gmail.com> Date: Mon, 22 Dec 2014 16:21:57 -0500 Subject: [PATCH 11/15] Set up a save button. --- app/models/Campaign.coffee | 2 +- app/schemas/models/level.coffee | 2 + .../editor/campaign/campaign-editor-view.jade | 2 +- .../editor/campaign/save-campaign-modal.jade | 21 ++++++ .../editor/campaign/CampaignEditorView.coffee | 74 ++++++++++++++----- .../editor/campaign/SaveCampaignModal.coffee | 34 +++++++++ server/commons/Handler.coffee | 2 +- server/levels/level_handler.coffee | 2 + 8 files changed, 119 insertions(+), 20 deletions(-) create mode 100644 app/templates/editor/campaign/save-campaign-modal.jade create mode 100644 app/views/editor/campaign/SaveCampaignModal.coffee diff --git a/app/models/Campaign.coffee b/app/models/Campaign.coffee index 55866cb3c..fec933a31 100644 --- a/app/models/Campaign.coffee +++ b/app/models/Campaign.coffee @@ -6,5 +6,5 @@ module.exports = class Campaign extends CocoModel @schema: schema urlRoot: '/db/campaign' saveBackups: true - @denormalizedLevelProperties: _.keys(_.omit(schema.properties.levels.additionalProperties.properties, ['unlocks', 'position'])) + @denormalizedLevelProperties: _.keys(_.omit(schema.properties.levels.additionalProperties.properties, ['unlocks', 'position', 'rewards'])) @denormalizedCampaignProperties: ['name', 'i18n', 'description', 'slug'] diff --git a/app/schemas/models/level.coffee b/app/schemas/models/level.coffee index 18cf6f445..78e81acf5 100644 --- a/app/schemas/models/level.coffee +++ b/app/schemas/models/level.coffee @@ -297,6 +297,8 @@ _.extend LevelSchema.properties, tasks: c.array {title: 'Tasks', description: 'Tasks to be completed for this level.', default: (name: t for t in defaultTasks)}, c.task # Admin flags + adventurer: { type: 'boolean' } + practice: { type: 'boolean' } disableSpaces: { type: 'boolean' } hidesSubmitUntilRun: { type: 'boolean' } hidesPlayButton: { type: 'boolean' } diff --git a/app/templates/editor/campaign/campaign-editor-view.jade b/app/templates/editor/campaign/campaign-editor-view.jade index a0d47fdff..0dd1296c5 100644 --- a/app/templates/editor/campaign/campaign-editor-view.jade +++ b/app/templates/editor/campaign/campaign-editor-view.jade @@ -16,7 +16,7 @@ block header span.glyphicon-home.glyphicon ul.nav.navbar-nav.navbar-right - if authorized + if me.isAdmin() li#save-button a span.glyphicon-floppy-disk.glyphicon diff --git a/app/templates/editor/campaign/save-campaign-modal.jade b/app/templates/editor/campaign/save-campaign-modal.jade new file mode 100644 index 000000000..40633074b --- /dev/null +++ b/app/templates/editor/campaign/save-campaign-modal.jade @@ -0,0 +1,21 @@ +extends /templates/core/modal-base + +block modal-header-content + h3 Save Changes to Campaign + +block modal-body-content + if !modelsToSave.models.length + .alert.alert-info No changes + + for model in modelsToSave.models + .panel.panel-default + .panel-heading + span.panel-title.spr= model.get('name') + span.text-muted= model.constructor.className + .panel-body + .delta-view(data-model-id=model.id) + +block modal-footer + .modal-footer + button(data-dismiss="modal", data-i18n="common.cancel").btn Cancel + button.btn.btn-primary#save-button Save diff --git a/app/views/editor/campaign/CampaignEditorView.coffee b/app/views/editor/campaign/CampaignEditorView.coffee index ac8ceb6cc..b3e7f11d8 100644 --- a/app/views/editor/campaign/CampaignEditorView.coffee +++ b/app/views/editor/campaign/CampaignEditorView.coffee @@ -7,6 +7,8 @@ CampaignView = require 'views/play/CampaignView' CocoCollection = require 'collections/CocoCollection' treemaExt = require 'core/treema-ext' utils = require 'core/utils' +SaveCampaignModal = require './SaveCampaignModal' +RelatedAchievementsCollection = require 'collections/RelatedAchievementsCollection' achievementProject = ['related', 'rewards', 'name', 'slug'] thangTypeProject = ['name', 'original', 'slug'] @@ -15,6 +17,9 @@ module.exports = class CampaignEditorView extends RootView id: "campaign-editor-view" template: require 'templates/editor/campaign/campaign-editor-view' className: 'editor' + + events: + 'click #save-button': 'onClickSaveButton' constructor: (options, @campaignHandle) -> super(options) @@ -78,8 +83,29 @@ module.exports = class CampaignEditorView extends RootView @supermodel.loadCollection(@achievements, 'achievements') @toSave = new Backbone.Collection() + @listenToOnce @campaign, 'sync', @onFundamentalLoaded + @listenToOnce @levels, 'sync', @onFundamentalLoaded + @listenToOnce @achievements, 'sync', @onFundamentalLoaded + + onFundamentalLoaded: -> + # load any levels which + return unless @campaign.loaded and @levels.loaded and @achievements.loaded + for level in _.values(@campaign.get('levels')) + model = @levels.findWhere(original: level.original) + if not model + model = new Level({}) + model.setProjection Campaign.denormalizedLevelProperties + model.setURL("/db/level/#{level.original}/version") + @levels.add @supermodel.loadModel(model, 'level').model + achievements = new RelatedAchievementsCollection level.original + achievements.setProjection achievementProject + @supermodel.loadCollection achievements, 'achievements' + @listenToOnce achievements, 'sync', -> + @achievements.add(achievements.models) + onLoaded: -> + @toSave.add @campaign if @campaign.hasLocalChanges() campaignLevels = $.extend({}, @campaign.get('levels')) for level in @levels.models levelOriginal = level.get('original') @@ -151,12 +177,23 @@ module.exports = class CampaignEditorView extends RootView campaignLevels[levelOriginal] = campaignLevel @campaign.set('levels', campaignLevels) + + for level in _.values campaignLevels + model = @levels.findWhere {original: level.original} + model.set key, level[key] for key in Campaign.denormalizedLevelProperties +# @toSave.add model if model.hasLocalChanges() +# @updateRewardsForLevel model, level.rewards + super() getRenderData: -> c = super() c.campaign = @campaign c + + onClickSaveButton: -> + @toSave.set @toSave.filter (m) -> m.hasLocalChanges() + @openModalView new SaveCampaignModal({}, @toSave) afterRender: -> super() @@ -195,14 +232,11 @@ module.exports = class CampaignEditorView extends RootView original = parts[2] level = @supermodel.getModelByOriginal Level, original campaignLevel = @treema.get "/levels/#{original}" - - if 'rewards' in parts - rewardsData = @ - @updateRewardsForLevel level, campaignLevel.rewards - - for key in Campaign.denormalizedLevelProperties - level.set key, campaignLevel[key] - @toSave.add level + + @updateRewardsForLevel level, campaignLevel.rewards + + level.set key, campaignLevel[key] for key in Campaign.denormalizedLevelProperties + @toSave.add level if level.hasLocalChanges() @toSave.add @campaign @campaign.set key, value for key, value of @treema.data @@ -225,12 +259,23 @@ module.exports = class CampaignEditorView extends RootView achievements = @supermodel.getModels(Achievement) achievements = (a for a in achievements when a.get('related') is level.get('original')) for achievement in achievements - rewardSubset = (r for r in rewards when r.achievement is achievement._id) + rewardSubset = (r for r in rewards when r.achievement is achievement.id) + oldRewards = achievement.get 'rewards' newRewards = {} - newRewards.heroes = _.compact((r.hero for r in rewards)) - newRewards.items = _.compact((r.item for r in rewards)) - newRewards.levels = _.compact((r.level for r in rewards)) + + heroes = _.compact((r.hero for r in rewardSubset)) + newRewards.heroes = heroes if heroes.length + + items = _.compact((r.item for r in rewardSubset)) + newRewards.items = items if items.length + + levels = _.compact((r.level for r in rewardSubset)) + newRewards.levels = levels if levels.length + + newRewards.gems = oldRewards.gems if oldRewards.gems achievement.set 'rewards', newRewards + if achievement.hasLocalChanges() + @toSave.add achievement class LevelsNode extends TreemaObjectNode valueClass: 'treema-levels' @@ -279,23 +324,18 @@ class CampaignsNode extends TreemaObjectNode s.once 'sync', (collection) -> CampaignsNode.campaigns[campaign.id] = campaign for campaign in collection.models mapped = ({label: r.get('name'), value: r.id} for r in collection.models) - console.log 'campaigns is now', CampaignsNode.campaigns res(mapped) class CampaignNode extends TreemaObjectNode valueClass: 'treema-campaign' buildValueForDisplay: (valEl, data) -> - console.log 'build value for display?', data @buildValueForDisplaySimply valEl, data.name populateData: -> return if @data.name? - console.log 'key for parent', @keyForParent, CampaignsNode.campaigns[@keyForParent].attributes, Campaign.denormalizedCampaignProperties data = _.pick CampaignsNode.campaigns[@keyForParent].attributes, Campaign.denormalizedCampaignProperties - console.log 'data?', data _.extend @data, data - console.log 'extended data', @data class AchievementNode extends treemaExt.IDReferenceNode buildSearchURL: (term) -> "#{@url}?term=#{term}&project=#{achievementProject.join(',')}" diff --git a/app/views/editor/campaign/SaveCampaignModal.coffee b/app/views/editor/campaign/SaveCampaignModal.coffee new file mode 100644 index 000000000..69bee3904 --- /dev/null +++ b/app/views/editor/campaign/SaveCampaignModal.coffee @@ -0,0 +1,34 @@ +ModalView = require 'views/core/ModalView' +template = require 'templates/editor/campaign/save-campaign-modal' +DeltaView = require 'views/editor/DeltaView' + +module.exports = class SaveCampaignModal extends ModalView + id: 'save-campaign-modal' + template: template + plain: true + + events: + 'click #save-button': 'onClickSaveButton' + + constructor: (options, @modelsToSave) -> + super(options) + + getRenderData: -> + c = super() + c.modelsToSave = @modelsToSave + c + + afterRender: -> + @$el.find('.delta-view').each((i, el) => + $el = $(el) + model = @modelsToSave.find( id: $el.data('model-id')) + deltaView = new DeltaView({model: model}) + @insertSubView(deltaView, $el) + ) + super() + + onClickSaveButton: -> + @showLoading() + modelsBeingSaved = (model.patch() for model in @modelsToSave.models) + modelsBeingSaved = modelsBeingSaved + $.when(_.compact(modelsBeingSaved)...).done(-> document.location.reload()) \ No newline at end of file diff --git a/server/commons/Handler.coffee b/server/commons/Handler.coffee index 2b3d7b96e..21f24796e 100644 --- a/server/commons/Handler.coffee +++ b/server/commons/Handler.coffee @@ -328,7 +328,7 @@ module.exports = class Handler put: (req, res, id) -> # Client expects PATCH behavior for PUTs # Real PATCHs return incorrect HTTP responses in some environments (e.g. Browserstack, schools) - return @postNewVersion(req, res) if @modelClass.schema.uses_coco_versions + return @sendForbiddenError(res) if @modelClass.schema.uses_coco_versions and not req.user.isAdmin() return @sendBadInputError(res, 'No input.') if _.isEmpty(req.body) return @sendForbiddenError(res) unless @hasAccess(req) @getDocumentForIdOrSlug req.body._id or id, (err, document) => diff --git a/server/levels/level_handler.coffee b/server/levels/level_handler.coffee index 9e739ca91..64cddb9cb 100644 --- a/server/levels/level_handler.coffee +++ b/server/levels/level_handler.coffee @@ -31,6 +31,8 @@ LevelHandler = class LevelHandler extends Handler 'i18nCoverage' 'loadingTip' 'requiresSubscription' + 'adventurer' + 'practice' 'disableSpaces' 'hidesSubmitUntilRun' 'hidesPlayButton' From 1bb76d5902456b7f6be8410aaf3a93bd40b87548 Mon Sep 17 00:00:00 2001 From: Scott Erickson <sderickson@gmail.com> Date: Tue, 23 Dec 2014 09:12:41 -0500 Subject: [PATCH 12/15] Fixed shadow placements. --- app/templates/play/campaign-view.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/templates/play/campaign-view.jade b/app/templates/play/campaign-view.jade index 7abb983eb..6bcb6ad80 100644 --- a/app/templates/play/campaign-view.jade +++ b/app/templates/play/campaign-view.jade @@ -18,7 +18,7 @@ img.banner(src="/images/pages/play/level-banner-complete.png") if levelStatusMap[level.original] === 'started' img.banner(src="/images/pages/play/level-banner-started.png") - div(style="left: #{level.x}%; bottom: #{level.y}%", class="level-shadow" + (next ? " next" : "") + " " + levelStatusMap[level.original] || "") + div(style="left: #{level.position.x}%; bottom: #{level.position.y}%", class="level-shadow" + (next ? " next" : "") + " " + levelStatusMap[level.original] || "") .level-info-container(data-level-id=level.original, data-level-path=level.levelPath || 'level', data-level-name=level.name) div(class="level-info " + (levelStatusMap[level.original] || "")) h3= level.name + (level.disabled ? " (Coming soon!)" : (level.locked ? " (Locked)" : "")) From e6c914f4436aaff2c6a3fcfe785466b80ac7f318 Mon Sep 17 00:00:00 2001 From: Scott Erickson <sderickson@gmail.com> Date: Tue, 23 Dec 2014 09:19:08 -0500 Subject: [PATCH 13/15] Tweaked campaign editor styling. --- app/styles/editor/campaign/campaign-editor-view.sass | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/styles/editor/campaign/campaign-editor-view.sass b/app/styles/editor/campaign/campaign-editor-view.sass index 60e5a1a54..42787d6ce 100644 --- a/app/styles/editor/campaign/campaign-editor-view.sass +++ b/app/styles/editor/campaign/campaign-editor-view.sass @@ -4,12 +4,16 @@ top: 0 bottom: 0 left: 0 - right: 0 + width: 25% margin-right: 1200px + .treema-root + max-height: 100% + overflow: scroll + #right-column position: absolute top: 0 bottom: 0 right: 0 - width: 1200px + width: 60% From b567e737cd1d679d83ac4300a565ef885c139ce0 Mon Sep 17 00:00:00 2001 From: Scott Erickson <sderickson@gmail.com> Date: Tue, 23 Dec 2014 09:19:24 -0500 Subject: [PATCH 14/15] Tweaked campaign editor styling. --- app/styles/editor/campaign/campaign-editor-view.sass | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/styles/editor/campaign/campaign-editor-view.sass b/app/styles/editor/campaign/campaign-editor-view.sass index 42787d6ce..1a128caa6 100644 --- a/app/styles/editor/campaign/campaign-editor-view.sass +++ b/app/styles/editor/campaign/campaign-editor-view.sass @@ -16,4 +16,4 @@ top: 0 bottom: 0 right: 0 - width: 60% + width: 75% From 96c8e035cfbe3d6e7446255d4c907672a5696e90 Mon Sep 17 00:00:00 2001 From: Scott Erickson <sderickson@gmail.com> Date: Tue, 23 Dec 2014 09:42:24 -0500 Subject: [PATCH 15/15] Added a CampaignLevelView stub that appears when you double click a level node or one of its children on the treema. --- .../editor/campaign/campaign-editor-view.sass | 9 +++++++++ .../editor/campaign/campaign-editor-view.jade | 2 +- .../editor/campaign/campaign-level-view.jade | 4 ++++ .../editor/campaign/CampaignEditorView.coffee | 8 ++++++++ .../editor/campaign/CampaignLevelView.coffee | 19 +++++++++++++++++++ 5 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 app/templates/editor/campaign/campaign-level-view.jade create mode 100644 app/views/editor/campaign/CampaignLevelView.coffee diff --git a/app/styles/editor/campaign/campaign-editor-view.sass b/app/styles/editor/campaign/campaign-editor-view.sass index 1a128caa6..cdee91359 100644 --- a/app/styles/editor/campaign/campaign-editor-view.sass +++ b/app/styles/editor/campaign/campaign-editor-view.sass @@ -17,3 +17,12 @@ bottom: 0 right: 0 width: 75% + + #campaign-level-view + position: absolute + top: 0 + left: 0 + right: 0 + bottom: 0 + background-color: white + z-index: 3 \ No newline at end of file diff --git a/app/templates/editor/campaign/campaign-editor-view.jade b/app/templates/editor/campaign/campaign-editor-view.jade index 0dd1296c5..2515ed484 100644 --- a/app/templates/editor/campaign/campaign-editor-view.jade +++ b/app/templates/editor/campaign/campaign-editor-view.jade @@ -39,6 +39,6 @@ block outer_content #right-column #campaign-view - #campaign-level-view + #campaign-level-view.hidden block footer diff --git a/app/templates/editor/campaign/campaign-level-view.jade b/app/templates/editor/campaign/campaign-level-view.jade new file mode 100644 index 000000000..fd2723d52 --- /dev/null +++ b/app/templates/editor/campaign/campaign-level-view.jade @@ -0,0 +1,4 @@ +.jumbotron + .button.close(type="button", aria-hidden="true") × + h1= level.get('name') + p= level.get('description') diff --git a/app/views/editor/campaign/CampaignEditorView.coffee b/app/views/editor/campaign/CampaignEditorView.coffee index b3e7f11d8..58976b6ec 100644 --- a/app/views/editor/campaign/CampaignEditorView.coffee +++ b/app/views/editor/campaign/CampaignEditorView.coffee @@ -9,6 +9,7 @@ treemaExt = require 'core/treema-ext' utils = require 'core/utils' SaveCampaignModal = require './SaveCampaignModal' RelatedAchievementsCollection = require 'collections/RelatedAchievementsCollection' +CampaignLevelView = require './CampaignLevelView' achievementProject = ['related', 'rewards', 'name', 'slug'] thangTypeProject = ['name', 'original', 'slug'] @@ -242,6 +243,13 @@ module.exports = class CampaignEditorView extends RootView @campaign.set key, value for key, value of @treema.data @campaignView.setCampaign(@campaign) + onTreemaDoubleClicked: (e, node) => + path = node.getPath() + return unless _.string.startsWith path, '/levels/' + original = path.split('/')[2] + level = @supermodel.getModelByOriginal Level, original + @insertSubView new CampaignLevelView({}, level) + onCampaignLevelMoved: (e) -> path = "levels/#{e.levelOriginal}/position" @treema.set path, e.position diff --git a/app/views/editor/campaign/CampaignLevelView.coffee b/app/views/editor/campaign/CampaignLevelView.coffee new file mode 100644 index 000000000..b27c02aeb --- /dev/null +++ b/app/views/editor/campaign/CampaignLevelView.coffee @@ -0,0 +1,19 @@ +CocoView = require 'views/core/CocoView' + +module.exports = class CampaignLevelView extends CocoView + id: 'campaign-level-view' + template: require 'templates/editor/campaign/campaign-level-view' + + events: + 'click .close': 'onClickClose' + + constructor: (options, @level) -> + super(options) + + getRenderData: -> + c = super() + c.level = @level + c + + onClickClose: -> + @$el.addClass('hidden') \ No newline at end of file