diff --git a/app/assets/images/pages/play/portal-background.png b/app/assets/images/pages/play/portal-background.png new file mode 100644 index 000000000..4a9d33933 Binary files /dev/null and b/app/assets/images/pages/play/portal-background.png differ diff --git a/app/assets/images/pages/play/portal-campaigns.png b/app/assets/images/pages/play/portal-campaigns.png new file mode 100644 index 000000000..3d9806dd1 Binary files /dev/null and b/app/assets/images/pages/play/portal-campaigns.png differ diff --git a/app/assets/javascripts/run-tests.js b/app/assets/javascripts/run-tests.js index 913f71fe3..a32c38e6e 100644 --- a/app/assets/javascripts/run-tests.js +++ b/app/assets/javascripts/run-tests.js @@ -1,8 +1,10 @@ // Helper for running tests through Karma. // Hooks into the test view logic for running tests. + +window.userObject = {_id:'1'} initialize = require('core/initialize'); initialize.init(); console.debug = function() {}; // Karma conf doesn't seem to work? Debug messages are still emitted when they shouldn't be. TestView = require('views/TestView'); -TestView.runTests(); \ No newline at end of file +TestView.runTests(); diff --git a/app/core/initialize.coffee b/app/core/initialize.coffee index 3d42b664a..0ad7b49da 100644 --- a/app/core/initialize.coffee +++ b/app/core/initialize.coffee @@ -43,6 +43,8 @@ init = -> handleNormalUrls() setUpMoment() # Set up i18n for moment +module.exports.init = init + handleNormalUrls = -> # http://artsy.github.com/blog/2012/06/25/replacing-hashbang-routes-with-pushstate/ $(document).on 'click', "a[href^='/']", (event) -> diff --git a/app/locale/en.coffee b/app/locale/en.coffee index ee3fbc1f5..ad19489b6 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -399,10 +399,6 @@ thank_you: "Thank you for supporting CodeCombat." sorry_to_see_you_go: "Sorry to see you go! Please let us know what we could have done better." unsubscribe_feedback_placeholder: "O, what have we done?" - levels: "Get more practice with bonus levels!" - heroes: "More powerful heroes!" - gems: "3500 bonus gems every month!" - items: "Over 250 bonus items!" parent_button: "Ask your parent" parent_email_description: "We'll email them so they can buy you a CodeCombat subscription." parent_email_input_invalid: "Email address invalid." @@ -416,7 +412,6 @@ parents_blurb1: "With CodeCombat, your child learns by writing real code. They start by learning simple commands, and progress to more advanced topics." parents_blurb2: "For $9.99 USD/mo, they get new challenges every week and personal email support from professional programmers." parents_blurb3: "No Risk: 100% money back guarantee, easy 1-click unsubscribe." - subscribe_button: "Subscribe" stripe_description: "Monthly Subscription" subscription_required_to_play: "You'll need a subscription to play this level." unlock_help_videos: "Subscribe to unlock all video tutorials." @@ -1003,6 +998,7 @@ play_counts: "Play Counts" feedback: "Feedback" payment_info: "Payment Info" + campaigns: "Campaigns" delta: added: "Added" diff --git a/app/models/User.coffee b/app/models/User.coffee index 2dcc1dcc6..4239d3d90 100644 --- a/app/models/User.coffee +++ b/app/models/User.coffee @@ -148,6 +148,14 @@ module.exports = class User extends CocoModel application.tracker.identify leaderboardsGroup: @leaderboardsGroup unless me.isAdmin() @leaderboardsGroup + getShowsPortal: -> + return @showsPortal if @showsPortal? + group = me.get('testGroupNumber') + @showsPortal = if group < 128 then true else false + @showsPortal = true if me.isAdmin() + application.tracker.identify showsPortal: @showsPortal unless me.isAdmin() + @showsPortal + getVideoTutorialStylesIndex: (numVideos=0)-> # A/B Testing video tutorial styles # Not a constant number of videos available (e.g. could be 0, 1, 3, or 4 currently) diff --git a/app/schemas/models/achievement.coffee b/app/schemas/models/achievement.coffee index 289b5e2bd..98d4767ad 100644 --- a/app/schemas/models/achievement.coffee +++ b/app/schemas/models/achievement.coffee @@ -91,5 +91,6 @@ AchievementSchema.definitions = {} AchievementSchema.definitions['mongoQueryOperator'] = MongoQueryOperatorSchema AchievementSchema.definitions['mongoFindQuery'] = MongoFindQuerySchema c.extendTranslationCoverageProperties AchievementSchema +c.extendPatchableProperties AchievementSchema module.exports = AchievementSchema diff --git a/app/schemas/models/campaign.schema.coffee b/app/schemas/models/campaign.schema.coffee index 34225115a..2c97dccc1 100644 --- a/app/schemas/models/campaign.schema.coffee +++ b/app/schemas/models/campaign.schema.coffee @@ -124,5 +124,6 @@ _.extend CampaignSchema.properties, { c.extendBasicProperties CampaignSchema, 'campaign' c.extendTranslationCoverageProperties CampaignSchema +c.extendPatchableProperties CampaignSchema module.exports = CampaignSchema diff --git a/app/schemas/models/patch.coffee b/app/schemas/models/patch.coffee index 918644c8b..e2e056af9 100644 --- a/app/schemas/models/patch.coffee +++ b/app/schemas/models/patch.coffee @@ -1,6 +1,6 @@ c = require './../schemas' -patchables = ['level', 'thang_type', 'level_system', 'level_component', 'article'] +patchables = ['level', 'thang_type', 'level_system', 'level_component', 'article', 'achievement', 'campaign'] PatchSchema = c.object({title: 'Patch', required: ['target', 'delta', 'commitMessage']}, { delta: {title: 'Delta', type: ['array', 'object']} diff --git a/app/styles/play/campaign-view.sass b/app/styles/play/campaign-view.sass index 444248910..a01a9a9d5 100644 --- a/app/styles/play/campaign-view.sass +++ b/app/styles/play/campaign-view.sass @@ -478,6 +478,80 @@ $gameControlMargin: 30px .particle-man z-index: 2 + .portal + position: relative + width: 100% + height: 100% + background: transparent url(/images/pages/play/portal-background.png) + display: flex + align-items: center + justify-content: center + + .portals + $campaignWidth: 317px + $campaignHeight: 634px + $campaignHoverScale: 1.2 + width: 6 * $campaignWidth + height: $campaignHeight * $campaignHoverScale + flex-wrap: nowrap + display: flex + overflow: hidden + + .campaign + width: $campaignWidth + height: $campaignHeight + margin-top: $campaignHeight * ($campaignHoverScale - 1) / 2 + background: transparent url(/images/pages/play/portal-campaigns.png) no-repeat 0 0 + display: inline-block + flex-shrink: 0 + position: relative + cursor: pointer + // http://easings.net/#easeOutBack plus tweaked a bit: http://cubic-bezier.com/#.11,.67,.08,1.42 + @include transition(0.25s cubic-bezier(0.11, 0.67, 0.8, 1.42)) + + &:hover + @include scale($campaignHoverScale) + + &.silhouette + @include filter(contrast(50%) brightness(65%)) + pointer-events: none + + &.locked + @include filter(contrast(80%) brightness(80%)) + pointer-events: none + + &.forest + background-position: (-1 * $campaignWidth) 0 + &.desert + background-position: (-2 * $campaignWidth) 0 + &.mountain + background-position: (-3 * $campaignWidth) 0 + &.ice + background-position: (-4 * $campaignWidth) 0 + &.volcano + background-position: (-5 * $campaignWidth) 0 + + .campaign-label + position: absolute + top: 55% + width: 100% + text-align: center + + .campaign-name, .levels-completed, .campaign-locked + margin: 0 + color: rgb(232, 217, 87) + 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 + z-index: 30 + pointer-events: none + + .levels-completed + font-size: 22px + + .play-button + margin-top: 30px + min-width: 100px + + body.ipad #campaign-view // iPad only supports up to Kithgard Gates for now. diff --git a/app/templates/core/subscribe-modal.jade b/app/templates/core/subscribe-modal.jade index 3071b2c5e..e1621dee6 100644 --- a/app/templates/core/subscribe-modal.jade +++ b/app/templates/core/subscribe-modal.jade @@ -65,7 +65,7 @@ span.glyphicon.glyphicon-ok #parents-info(data-i18n="subscribe.parents") - button.btn.btn-lg.btn-illustrated.purchase-button(data-i18n="subscribe.subscribe_button") + button.btn.btn-lg.btn-illustrated.purchase-button(data-i18n="subscribe.subscribe_title") button.btn.btn-lg.btn-illustrated.parent-button(data-i18n="subscribe.parent_button") if state === 'declined' diff --git a/app/templates/play/campaign-view.jade b/app/templates/play/campaign-view.jade index 227a24da3..7b8a0c3fd 100644 --- a/app/templates/play/campaign-view.jade +++ b/app/templates/play/campaign-view.jade @@ -1,54 +1,77 @@ -.map - .gradient.horizontal-gradient.top-gradient - .gradient.vertical-gradient.right-gradient - .gradient.horizontal-gradient.bottom-gradient - .gradient.vertical-gradient.left-gradient - .map-background(alt="", draggable="false") +if campaign + .map + .gradient.horizontal-gradient.top-gradient + .gradient.vertical-gradient.right-gradient + .gradient.horizontal-gradient.bottom-gradient + .gradient.vertical-gradient.left-gradient + .map-background(alt="", draggable="false") - each level in levels - if !level.hidden - div(style="left: #{level.position.x}%; bottom: #{level.position.y}%; background-color: #{level.color}", class="level" + (level.next ? " next" : "") + (level.disabled ? " disabled" : "") + (level.locked ? " locked" : "") + " " + (levelStatusMap[level.slug] || ""), data-level-slug=level.slug, data-level-original=level.original, title=i18n(level, 'name') + (level.disabled ? ' (Coming Soon to Adventurers)' : '')) - if level.unlocksHero && (!level.purchasedHero || editorMode) - img.hero-portrait(src="/file/db/thang.type/#{level.unlocksHero}/portrait.png") - a(href=level.type == 'hero' ? '#' : level.disabled ? "/play" : "/play/#{level.levelPath || 'level'}/#{level.slug}", disabled=level.disabled, data-level-slug=level.slug, 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.slug] === 'complete' - img.banner(src="/images/pages/play/level-banner-complete.png") - if levelStatusMap[level.slug] === 'started' - img.banner(src="/images/pages/play/level-banner-started.png") - div(style="left: #{level.position.x}%; bottom: #{level.position.y}%", class="level-shadow" + (level.next ? " next" : "") + " " + (levelStatusMap[level.slug] || "")) - .level-info-container(data-level-slug=level.slug, data-level-path=level.levelPath || 'level', data-level-name=level.name) - - var playCount = levelPlayCountMap[level.slug] - div(class="level-info " + (levelStatusMap[level.slug] || "") + (level.requiresSubscription ? " premium" : "")) - .level-status - h3= i18n(level, 'name') + (level.disabled ? " (Coming soon!)" : (level.locked ? " (Locked)" : "")) - - var description = i18n(level, 'description') || level.description || "" - .level-description!= marked(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. - - if !level.disabled && !level.locked - if playCount && playCount.sessions - .play-counts.hidden - span.spl.spr= playCount.sessions - span(data-i18n="play.players") players - span.spr , #{Math.round(playCount.playtime / 3600)} - span(data-i18n="play.hours_played") hours played - if levelStatusMap[level.slug] === 'complete' - button.btn.btn-warning.btn.btn-lg.btn-illustrated.view-solutions(data-level-slug=level.slug) - span(data-i18n="leaderboard.scores") - button.btn.btn-success.btn.btn-lg.btn-illustrated.start-level(data-i18n="common.play") Play - else if level.unlocksHero && !level.purchasedHero - img.hero-portrait(src="/file/db/thang.type/#{level.unlocksHero}/portrait.png", style="left: #{level.position.x}%; bottom: #{level.position.y}%;") - - for adjacentCampaign in adjacentCampaigns - a(href=(editorMode ? "/editor/campaign/" : "/play/") + adjacentCampaign.slug) - span.glyphicon.glyphicon-share-alt.campaign-switch(style=adjacentCampaign.style, title=adjacentCampaign.name, data-campaign-id=adjacentCampaign.id) + each level in levels + if !level.hidden + div(style="left: #{level.position.x}%; bottom: #{level.position.y}%; background-color: #{level.color}", class="level" + (level.next ? " next" : "") + (level.disabled ? " disabled" : "") + (level.locked ? " locked" : "") + " " + (levelStatusMap[level.slug] || ""), data-level-slug=level.slug, data-level-original=level.original, title=i18n(level, 'name') + (level.disabled ? ' (Coming Soon to Adventurers)' : '')) + if level.unlocksHero && (!level.purchasedHero || editorMode) + img.hero-portrait(src="/file/db/thang.type/#{level.unlocksHero}/portrait.png") + a(href=level.type == 'hero' ? '#' : level.disabled ? "/play" : "/play/#{level.levelPath || 'level'}/#{level.slug}", disabled=level.disabled, data-level-slug=level.slug, 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.slug] === 'complete' + img.banner(src="/images/pages/play/level-banner-complete.png") + if levelStatusMap[level.slug] === 'started' + img.banner(src="/images/pages/play/level-banner-started.png") + div(style="left: #{level.position.x}%; bottom: #{level.position.y}%", class="level-shadow" + (level.next ? " next" : "") + " " + (levelStatusMap[level.slug] || "")) + .level-info-container(data-level-slug=level.slug, data-level-path=level.levelPath || 'level', data-level-name=level.name) + - var playCount = levelPlayCountMap[level.slug] + div(class="level-info " + (levelStatusMap[level.slug] || "") + (level.requiresSubscription ? " premium" : "")) + .level-status + h3= i18n(level, 'name') + (level.disabled ? " (Coming soon!)" : (level.locked ? " (Locked)" : "")) + - var description = i18n(level, 'description') || level.description || "" + .level-description!= marked(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. + + if !level.disabled && !level.locked + if playCount && playCount.sessions + .play-counts.hidden + span.spl.spr= playCount.sessions + span(data-i18n="play.players") players + span.spr , #{Math.round(playCount.playtime / 3600)} + span(data-i18n="play.hours_played") hours played + if levelStatusMap[level.slug] === 'complete' + button.btn.btn-warning.btn.btn-lg.btn-illustrated.view-solutions(data-level-slug=level.slug) + span(data-i18n="leaderboard.scores") + button.btn.btn-success.btn.btn-lg.btn-illustrated.start-level(data-i18n="common.play") Play + else if level.unlocksHero && !level.purchasedHero + img.hero-portrait(src="/file/db/thang.type/#{level.unlocksHero}/portrait.png", style="left: #{level.position.x}%; bottom: #{level.position.y}%;") + + for adjacentCampaign in adjacentCampaigns + a(href=(editorMode ? "/editor/campaign/" : "/play/") + adjacentCampaign.slug) + span.glyphicon.glyphicon-share-alt.campaign-switch(style=adjacentCampaign.style, title=adjacentCampaign.name, data-campaign-id=adjacentCampaign.id) + +else + .portal + .portals + for campaignSlug in ['dungeon', 'forest', 'desert', 'mountain', 'ice', 'volcano'] + - var campaign = campaigns[campaignSlug]; + div(class="campaign #{campaignSlug}" + (campaign ? "" : " silhouette") + (campaign && campaign.locked ? " locked" : ""), data-campaign-slug=campaignSlug) + .campaign-label + h2.campaign-name + if campaign + span= i18n(campaign.attributes, 'fullName') + else + span ??? + if campaign && campaign.levelsTotal + h3.levels-completed + span= campaign.levelsCompleted + | / + span= campaign.levelsTotal + if campaign && campaign.locked + h3.campaign-locked(data-i18n="play.locked") Locked + else if campaign + btn(data-i18n="common.play").btn.btn-illustrated.btn-lg.btn-success.play-button .game-controls.header-font button.btn.items(data-toggle='coco-modal', data-target='play/modal/PlayItemsModal', data-i18n="[title]play.items") @@ -85,7 +108,7 @@ button.btn.btn-lg.btn-inverse#volume-button(data-i18n="[title]play.adjust_volume .glyphicon.glyphicon-volume-down .glyphicon.glyphicon-volume-up -if campaign.loaded +if campaign && campaign.loaded h1#campaign-status .campaign-status-background .campaign-name diff --git a/app/views/core/CocoView.coffee b/app/views/core/CocoView.coffee index 73835d28e..b94359362 100644 --- a/app/views/core/CocoView.coffee +++ b/app/views/core/CocoView.coffee @@ -241,7 +241,7 @@ module.exports = class CocoView extends Backbone.View showLoading: ($el=@$el) -> $el.find('>').addClass('hidden') - $el.append loadingScreenTemplate() + $el.append(loadingScreenTemplate()).i18n() @_lastLoading = $el hideLoading: -> diff --git a/app/views/i18n/I18NEditModelView.coffee b/app/views/i18n/I18NEditModelView.coffee index 112f74350..89971cad5 100644 --- a/app/views/i18n/I18NEditModelView.coffee +++ b/app/views/i18n/I18NEditModelView.coffee @@ -140,9 +140,14 @@ module.exports = class I18NEditModelView extends RootView return _.isArray(delta.o) and delta.o.length is 1 and 'i18n' in delta.dataPath ) + commitMessage = "Diplomat submission for lang #{@selectedLanguage}: #{flattened.length} change(s)." + save = false if @savedBefore + if save modelToSave = @model.cloneNewMinorVersion() modelToSave.updateI18NCoverage() if modelToSave.get('i18nCoverage') + if @modelClass.schema.properties.commitMessage + modelToSave.set 'commitMessage', commitMessage else modelToSave = new Patch() @@ -151,17 +156,21 @@ module.exports = class I18NEditModelView extends RootView 'collection': _.string.underscored @model.constructor.className 'id': @model.id } - - if @modelClass.schema.properties.commitMessage - commitMessage = "Diplomat submission for lang #{@selectedLanguage}: #{flattened.length} change(s)." modelToSave.set 'commitMessage', commitMessage errors = modelToSave.validate() button = $(e.target) button.attr('disabled', 'disabled') return button.text('Failed to Submit Changes') if errors - res = modelToSave.save(null, {type: 'POST'}) # Override PUT so we can trigger postNewVersion logic + type = 'PUT' + if @modelClass.schema.properties.version or (not save) + # Override PUT so we can trigger postNewVersion logic + # or you're POSTing a Patch + type = 'POST' + res = modelToSave.save(null, {type: type}) return button.text('Failed to Submit Changes') unless res button.text('Submitting...') res.error => button.text('Error Submitting Changes') - res.success => button.text('Submit Changes') + res.success => + @savedBefore = true + button.text('Submit Changes') diff --git a/app/views/play/CampaignView.coffee b/app/views/play/CampaignView.coffee index cc8108b44..cf64953ff 100644 --- a/app/views/play/CampaignView.coffee +++ b/app/views/play/CampaignView.coffee @@ -27,6 +27,11 @@ class LevelSessionsCollection extends CocoCollection super() @url = "/db/user/#{me.id}/level.sessions?project=state.complete,levelID" +class CampaignsCollection extends CocoCollection + url: '/db/campaign' + model: Campaign + project: ['name', 'fullName', 'i18n'] + module.exports = class CampaignView extends RootView id: 'campaign-view' template: template @@ -41,18 +46,29 @@ module.exports = class CampaignView extends RootView 'click .level-info-container .start-level': 'onClickStartLevel' 'click .level-info-container .view-solutions': 'onClickViewSolutions' 'click #volume-button': 'onToggleVolume' + 'click .portal .campaign': 'onClickPortalCampaign' + 'mouseenter .portals': 'onMouseEnterPortals' + 'mouseleave .portals': 'onMouseLeavePortals' + 'mousemove .portals': 'onMouseMovePortals' - constructor: (options, @terrain='dungeon') -> + constructor: (options, @terrain) -> super options - options ?= {} - - @campaign = new Campaign({_id:@terrain}) - @campaign = @supermodel.loadModel(@campaign, 'campaign').model - - @editorMode = options.editorMode + @editorMode = options?.editorMode + if @editorMode + @terrain ?= 'dungeon' + else unless me.getShowsPortal() + @terrain ?= 'dungeon' @levelStatusMap = {} @levelPlayCountMap = {} @sessions = @supermodel.loadCollection(new LevelSessionsCollection(), 'your_sessions', null, 0).model + @listenToOnce @sessions, 'sync', @onSessionsLoaded + unless @terrain + @campaigns = @supermodel.loadCollection(new CampaignsCollection(), 'campaigns', null, 0).model + @listenToOnce @campaigns, 'sync', @onCampaignsLoaded + return + + @campaign = new Campaign({_id:@terrain}) + @campaign = @supermodel.loadModel(@campaign, 'campaign').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']}) @@ -69,7 +85,6 @@ module.exports = class CampaignView extends RootView @supermodel.loadCollection(@earnedAchievements, 'achievements') - @listenToOnce @sessions, 'sync', @onSessionsLoaded @listenToOnce @campaign, 'sync', @getLevelPlayCounts $(window).on 'resize', @onWindowResize @probablyCachedMusic = storage.load("loaded-menu-music") @@ -99,6 +114,7 @@ module.exports = class CampaignView extends RootView @musicPlayer?.destroy() clearTimeout @playMusicTimeout @particleMan?.destroy() + clearInterval @portalScrollInterval super() getLevelPlayCounts: -> @@ -136,31 +152,16 @@ module.exports = class CampaignView extends RootView getRenderData: (context={}) -> context = super(context) context.campaign = @campaign - context.levels = _.values($.extend true, {}, @campaign.get('levels')) - context.levelsCompleted = context.levelsTotal = 0 - for level in context.levels - level.position ?= { x: 10, y: 10 } - level.locked = not me.ownsLevel level.original - level.locked = false if @levelStatusMap[level.slug] in ['started', 'complete'] - level.locked = false if @editorMode - level.locked = false if @campaign.get('name') is 'Auditions' - level.disabled = true if level.adminOnly and @levelStatusMap[level.slug] not in ['started', 'complete'] - level.color = 'rgb(255, 80, 60)' - if level.requiresSubscription - level.color = 'rgb(80, 130, 200)' - if unlocksHero = _.find(level.rewards, 'hero')?.hero - level.unlocksHero = unlocksHero - if level.unlocksHero - level.purchasedHero = level.unlocksHero in (me.get('purchased')?.heroes or []) - level.hidden = level.locked - unless level.disabled - ++context.levelsTotal - ++context.levelsCompleted if @levelStatusMap[level.slug] is 'complete' + context.levels = _.values($.extend true, {}, @campaign?.get('levels') ? {}) + @annotateLevel level for level in context.levels + count = @countLevels context.levels + context.levelsCompleted = count.completed + context.levelsTotal = count.total - @determineNextLevel context.levels if @sessions.loaded + @determineNextLevel context.levels if @sessions?.loaded # put lower levels in last, so in the world map they layer over one another properly. context.levels = (_.sortBy context.levels, (l) -> l.position.y).reverse() - @campaign.renderedLevels = context.levels + @campaign.renderedLevels = context.levels if @campaign context.levelStatusMap = @levelStatusMap context.levelPlayCountMap = @levelPlayCountMap @@ -168,7 +169,7 @@ module.exports = class CampaignView extends RootView context.mapType = _.string.slugify @terrain context.requiresSubscription = @requiresSubscription context.editorMode = @editorMode - context.adjacentCampaigns = _.filter _.values(_.cloneDeep(@campaign.get('adjacentCampaigns') or {})), (ac) => + context.adjacentCampaigns = _.filter _.values(_.cloneDeep(@campaign?.get('adjacentCampaigns') or {})), (ac) => return false if ac.showIfUnlocked and (ac.showIfUnlocked not in me.levels()) and not @editorMode ac.name = utils.i18n ac, 'name' styles = [] @@ -181,6 +182,26 @@ module.exports = class CampaignView extends RootView return true context.marked = marked context.i18n = utils.i18n + + if @campaigns + context.campaigns = {} + for campaign in @campaigns.models + context.campaigns[campaign.get('slug')] = campaign + if @sessions.loaded + levels = _.values($.extend true, {}, campaign.get('levels') ? {}) + count = @countLevels levels + campaign.levelsTotal = count.total + campaign.levelsCompleted = count.completed + if campaign.get('slug') is 'dungeon' + campaign.locked = false + else unless campaign.levelsTotal + campaign.locked = true + else + campaign.locked = true + for campaign in @campaigns.models + for acID, ac of campaign.get('adjacentCampaigns') ? {} + _.find(@campaigns.models, id: acID)?.locked = false if ac.showIfUnlocked in me.levels() + context afterRender: -> @@ -203,7 +224,7 @@ module.exports = class CampaignView extends RootView unless window.currentModal or not @fullyRendered @highlightElement '.level.next', delay: 500, duration: 60000, rotation: 0, sides: ['top'] if @editorMode - for level in @campaign.renderedLevels + for level in @campaign?.renderedLevels ? [] for nextLevelOriginal in level.nextLevels ? [] if nextLevel = _.find(@campaign.renderedLevels, original: nextLevelOriginal) @createLine level.position, nextLevel.position @@ -220,6 +241,32 @@ module.exports = class CampaignView extends RootView authModal.mode = 'signup' @openModalView authModal + annotateLevel: (level) -> + level.position ?= { x: 10, y: 10 } + level.locked = not me.ownsLevel level.original + level.locked = false if @levelStatusMap[level.slug] in ['started', 'complete'] + level.locked = false if @editorMode + level.locked = false if @campaign?.get('name') is 'Auditions' + level.disabled = true if level.adminOnly and @levelStatusMap[level.slug] not in ['started', 'complete'] + level.color = 'rgb(255, 80, 60)' + if level.requiresSubscription + level.color = 'rgb(80, 130, 200)' + if unlocksHero = _.find(level.rewards, 'hero')?.hero + level.unlocksHero = unlocksHero + if level.unlocksHero + level.purchasedHero = level.unlocksHero in (me.get('purchased')?.heroes or []) + level.hidden = level.locked + level + + countLevels: (levels) -> + count = total: 0, completed: 0 + for level in levels + @annotateLevel level unless level.locked? # Annotate if we haven't already. + unless level.disabled + ++count.total + ++count.completed if @levelStatusMap[level.slug] is 'complete' + count + showLeaderboard: (levelSlug) -> #levelSlug ?= 'siege-of-stonehold' # Testing leaderboardModal = new LeaderboardModal supermodel: @supermodel, levelSlug: levelSlug @@ -249,7 +296,7 @@ module.exports = class CampaignView extends RootView line.append($('