RootView = require 'views/core/RootView' 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' MusicPlayer = require 'lib/surface/MusicPlayer' storage = require 'core/storage' AuthModal = require 'views/core/AuthModal' SubscribeModal = require 'views/core/SubscribeModal' LeaderboardModal = require 'views/play/modal/LeaderboardModal' Level = require 'models/Level' utils = require 'core/utils' require 'vendor/three' ParticleMan = require 'core/ParticleMan' ShareProgressModal = require 'views/play/modal/ShareProgressModal' UserPollsRecord = require 'models/UserPollsRecord' Poll = require 'models/Poll' PollModal = require 'views/play/modal/PollModal' storage = require 'core/storage' trackedHourOfCode = false class LevelSessionsCollection extends CocoCollection url: '' model: LevelSession constructor: (model) -> super() @url = "/db/user/#{me.id}/level.sessions?project=state.complete,levelID,state.difficulty" class CampaignsCollection extends CocoCollection url: '/db/campaign' model: Campaign project: ['name', 'fullName', 'description', 'i18n'] module.exports = class CampaignView extends RootView id: 'campaign-view' template: template subscriptions: 'subscribe-modal:subscribed': 'onSubscribed' events: 'click .map-background': 'onClickMap' 'click .level a': 'onClickLevel' 'dblclick .level a': 'onDoubleClickLevel' 'click .level-info-container .start-level': 'onClickStartLevel' 'click .level-info-container .view-solutions': 'onClickViewSolutions' 'click #volume-button': 'onToggleVolume' 'click #back-button': 'onClickBack' 'click #clear-storage-button': 'onClickClearStorage' 'click .portal .campaign': 'onClickPortalCampaign' 'mouseenter .portals': 'onMouseEnterPortals' 'mouseleave .portals': 'onMouseLeavePortals' 'mousemove .portals': 'onMouseMovePortals' 'click .poll': 'showPoll' constructor: (options, @terrain) -> super options @editorMode = options?.editorMode if @editorMode @terrain ?= 'dungeon' @levelStatusMap = {} @levelPlayCountMap = {} @levelDifficultyMap = {} @sessions = @supermodel.loadCollection(new LevelSessionsCollection(), 'your_sessions', {cache: false}, 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.saveBackups = @editorMode @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']}) @listenToOnce @earnedAchievements, 'sync', -> earned = me.get('earned') 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) @supermodel.loadCollection(@earnedAchievements, 'achievements', {cache: false}) @listenToOnce @campaign, 'sync', @getLevelPlayCounts $(window).on 'resize', @onWindowResize @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:earned', -> @renderSelectors('#gems-count') @listenTo me, 'change:heroConfig', -> @updateHero() window.tracker?.trackEvent 'Loaded World Map', category: 'World Map', label: @terrain # 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($('')) trackedHourOfCode = true @requiresSubscription = not me.isPremium() destroy: -> @setupManager?.destroy() @$el.find('.ui-draggable').off().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 @particleMan?.destroy() clearInterval @portalScrollInterval super() getLevelPlayCounts: -> return unless me.isAdmin() return # TODO: get rid of all this? It's redundant with new campaign editor analytics, unless we want to show player counts on leaderboards buttons. success = (levelPlayCounts) => return if @destroyed for level in levelPlayCounts @levelPlayCountMap[level._id] = playtime: level.playtime, sessions: level.sessions @render() if @fullyRendered levelSlugs = (level.slug for levelID, level of @campaign.get 'levels') levelPlayCountsRequest = @supermodel.addRequestResource 'play_counts', { url: '/db/level/-/play_counts' data: {ids: levelSlugs} method: 'POST' success: success }, 0 levelPlayCountsRequest.load() onLoaded: -> return if @fullyRendered @fullyRendered = true @render() @preloadTopHeroes() unless me.get('heroConfig')?.thangType @$el.find('#campaign-status').delay(4000).animate({top: "-=58"}, 1000) unless @terrain is 'dungeon' if @terrain and me.get('anonymous') and me.get('lastLevel') is 'shadow-guard' and me.level() < 4 @openModalView new AuthModal supermodel: @supermodel, showSignupRationale: true, mode: 'signup' else if @terrain and me.get('name') and me.get('lastLevel') in ['forgetful-gemsmith', 'signs-and-portents'] and me.level() < 5 and not (me.get('ageRange') in ['18-24', '25-34', '35-44', '45-100']) and not storage.load('sent-parent-email') and not me.isPremium() @openModalView new ShareProgressModal() setCampaign: (@campaign) -> @render() onSubscribed: -> @requiresSubscription = false @render() getRenderData: (context={}) -> context = super(context) context.campaign = @campaign context.levels = _.values($.extend true, {}, @campaign?.get('levels') ? {}) if me.level() < 12 and @terrain is 'dungeon' and not @editorMode reject = if me.getFourthLevelGroup() is 'signs-and-portents' then 'forgetful-gemsmith' else 'signs-and-portents' context.levels = _.reject context.levels, slug: reject @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 # 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 if @campaign context.levelStatusMap = @levelStatusMap context.levelDifficultyMap = @levelDifficultyMap context.levelPlayCountMap = @levelPlayCountMap context.isIPadApp = application.isIPadApp context.mapType = _.string.slugify @terrain 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.levels()) and not @editorMode ac.name = utils.i18n ac, 'name' 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.marked = marked context.i18n = utils.i18n if @campaigns context.campaigns = {} for campaign in @campaigns.models when campaign.get('slug') isnt 'auditions' 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: -> super() @onWindowResize() unless application.isIPadApp _.defer => @$el?.find('.game-controls .btn:not(.poll)').addClass('has-tooltip').tooltip() # Have to defer or i18n doesn't take effect. view = @ @$el.find('.level, .campaign-switch').addClass('has-tooltip').tooltip().each -> return unless me.isAdmin() and view.editorMode $(@).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() e = { position: { x: (100 * x), y: (100 * y) }, levelOriginal: $(@).data('level-original'), campaignID: $(@).data('campaign-id') } view.trigger 'level-moved', e if e.levelOriginal view.trigger 'adjacent-campaign-moved', e if e.campaignID @updateVolume() @updateHero() 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 nextLevelOriginal in level.nextLevels ? [] if nextLevel = _.find(@campaign.renderedLevels, original: nextLevelOriginal) @createLine level.position, nextLevel.position @showLeaderboard @options.justBeatLevel?.get('slug') if @options.showLeaderboard# or true # Testing @applyCampaignStyles() @testParticles() afterInsert: -> super() return unless @getQueryVariable 'signup' return if me.get('email') @endHighlight() authModal = new AuthModal supermodel: @supermodel 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.locked = false if me.isInGodMode() level.locked = false if level.slug is 'apocalypse' level.disabled = true if level.adminOnly and @levelStatusMap[level.slug] not in ['started', 'complete'] level.disabled = false if me.isInGodMode() 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, levelIndex in levels @annotateLevel level unless level.locked? # Annotate if we haven't already. unless level.disabled unlockedInSameCampaign = levelIndex < 5 # First few are always counted (probably unlocked in previous campaign) for otherLevel in levels when not unlockedInSameCampaign and otherLevel isnt level for reward in (otherLevel.rewards ? []) when reward.level unlockedInSameCampaign ||= reward.level is level.original ++count.total if unlockedInSameCampaign or not level.locked ++count.completed if @levelStatusMap[level.slug] is 'complete' count showLeaderboard: (levelSlug) -> leaderboardModal = new LeaderboardModal supermodel: @supermodel, levelSlug: levelSlug @openModalView leaderboardModal determineNextLevel: (levels) -> foundNext = false for level in levels level.nextLevels = (reward.level for reward in level.rewards ? [] when reward.level) unless foundNext for nextLevelOriginal in level.nextLevels nextLevel = _.find levels, original: nextLevelOriginal dontPointTo = ['lost-viking','kithgard-mastery'] if nextLevel and not nextLevel.locked and not nextLevel.disabled and @levelStatusMap[nextLevel.slug] isnt 'complete' and nextLevel.slug not in dontPointTo and not nextLevel.replayable and ( me.isPremium() or not nextLevel.requiresSubscription or nextLevel.slug is 'apocalypse' or (nextLevel.slug is 'favorable-odds' and not @levelStatusMap['the-raised-sword']) ) nextLevel.next = true foundNext = true break if not foundNext and levels[0] and not levels[0].locked and @levelStatusMap[levels[0].slug] isnt 'complete' levels[0].next = true createLine: (o1, o2) -> p1 = x: o1.x, y: 0.66 * o1.y + 0.5 p2 = x: o2.x, y: 0.66 * o2.y + 0.5 length = Math.sqrt((p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y)) angle = Math.atan2(p1.y - p2.y, p2.x - p1.x) * 180 / Math.PI transform = "rotate(#{angle}deg)" line = $('