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' CreateAccountModal = require 'views/core/CreateAccountModal' 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' CourseInstance = require 'models/CourseInstance' 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,playtime" class CampaignsCollection extends CocoCollection # We don't send all of levels, just the parts needed in countLevels url: '/db/campaign/-/overworld?project=slug,adjacentCampaigns,name,fullName,description,i18n,color,levels' model: Campaign 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 .level-info-container .course-version button': 'onClickCourseVersion' '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 @terrain = 'picoctf' if window.serverConfig.picoCTF @editorMode = options?.editorMode if @editorMode @terrain ?= 'dungeon' @levelStatusMap = {} @levelPlayCountMap = {} @levelDifficultyMap = {} if window.serverConfig.picoCTF @supermodel.addRequestResource(url: '/picoctf/problems', success: (@picoCTFProblems) =>).load() else @sessions = @supermodel.loadCollection(new LevelSessionsCollection(), 'your_sessions', {cache: false}, 0).model @listenToOnce @sessions, 'sync', @onSessionsLoaded unless @terrain @campaigns = @supermodel.loadCollection(new CampaignsCollection(), 'campaigns', null, 1).model @listenToOnce @campaigns, 'sync', @onCampaignsLoaded return @campaign = new Campaign({_id:@terrain}) @campaign = @supermodel.loadModel(@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 @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() showLoading: ($el) -> unless @campaign @$el.find('.game-controls, .user-status').addClass 'hidden' @$el.find('.portal .campaign-name span').text $.i18n.t 'common.loading' hideLoading: -> unless @campaign @$el.find('.game-controls, .user-status').removeClass 'hidden' 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 CreateAccountModal supermodel: @supermodel, showSignupRationale: true 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.picoCTF = window.serverConfig.picoCTF 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 CreateAccountModal supermodel: @supermodel authModal.mode = 'signup' @openModalView authModal showAds: -> return false # No ads for now. if application.isProduction() && !me.isPremium() && !me.isTeacher() && !window.serverConfig.picoCTF return me.getCampaignAdsGroup() is 'leaderboard-ads' false annotateLevel: (level) -> level.position ?= { x: 10, y: 10 } level.locked = not me.ownsLevel level.original level.locked = true if level.slug is 'kithgard-mastery' and @calculateExperienceScore() is 0 level.locked = false if @levelStatusMap[level.slug] in ['started', 'complete'] level.locked = false if @editorMode level.locked = false if @campaign?.get('name') in ['Auditions', 'Intro'] level.locked = false if me.isInGodMode() #level.locked = false if level.slug is 'robot-ragnarok' 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 []) if window.serverConfig.picoCTF if problem = _.find(@picoCTFProblems or [], pid: level.picoCTFProblem) level.locked = false if problem.unlocked or level.slug is 'digital-graffiti' #level.locked = false # Testing to see all levels level.description = """ ### #{problem.name} #{level.description or problem.description} #{problem.category} - #{problem.score} points """ level.color = 'rgb(80, 130, 200)' if problem.solved level.hidden = level.locked if level.concepts?.length level.displayConcepts = level.concepts maxConcepts = 6 if level.displayConcepts.length > maxConcepts level.displayConcepts = level.displayConcepts.slice -maxConcepts 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 dontPointTo = ['lost-viking', 'kithgard-mastery'] # Challenge levels we don't want most players bashing heads against subscriptionPrompts = [{slug: 'boom-and-bust', unless: 'defense-of-plainswood'}] for level in levels # Iterate through all levels in order and look to find the first unlocked one that meets all our criteria for being pointed out as the next level. level.nextLevels = (reward.level for reward in level.rewards ? [] when reward.level) unless foundNext for nextLevelOriginal in level.nextLevels nextLevel = _.find levels, original: nextLevelOriginal # If it's a challenge level, we efficiently determine whether we actually do want to point it out. if nextLevel and nextLevel.slug is 'kithgard-mastery' and not nextLevel.locked and not @levelStatusMap[nextLevel.slug] and @calculateExperienceScore() >= 3 unless (timesPointedOut = storage.load("pointed-out-#{nextLevel.slug}") or 0) > 3 # We may determineNextLevel more than once per render, so we can't just do this once. But we do give up after a couple highlights. dontPointTo = _.without dontPointTo, nextLevel.slug storage.save "pointed-out-#{nextLevel.slug}", timesPointedOut + 1 # Should we point this level out? 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 _.any(subscriptionPrompts, (prompt) => nextLevel.slug is prompt.slug and not @levelStatusMap[prompt.unless]) ) 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 calculateExperienceScore: -> adultPoint = me.get('ageRange') in ['18-24', '25-34', '35-44', '45-100'] # They have to have answered the poll for this, likely after Shadow Guard. speedPoints = 0 for [levelSlug, speedThreshold] in [['dungeons-of-kithgard', 50], ['gems-in-the-deep', 55], ['shadow-guard', 55], ['forgetful-gemsmith', 40], ['true-names', 40]] if _.find(@sessions?.models, (session) -> session.get('levelID') is levelSlug)?.attributes.playtime <= speedThreshold ++speedPoints experienceScore = adultPoint + speedPoints # 0-6 score of how likely we think they are to be experienced and ready for Kithgard Mastery return experienceScore 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 = $('
').appendTo('.map').addClass('next-level-line').css(transform: transform, width: length + '%', left: o1.x + '%', bottom: (o1.y + 0.5) + '%') line.append($('
')).append($('
')) applyCampaignStyles: -> return unless @campaign?.loaded if (backgrounds = @campaign.get 'backgroundImage') and backgrounds.length backgrounds = _.sortBy backgrounds, 'width' backgrounds.reverse() rules = [] for background, i in backgrounds rule = "#campaign-view .map-background { background-image: url(/file/#{background.image}); }" rule = "@media screen and (max-width: #{background.width}px) { #{rule} }" if i rules.push rule utils.injectCSS rules.join('\n') if backgroundColor = @campaign.get 'backgroundColor' backgroundColorTransparent = @campaign.get 'backgroundColorTransparent' @$el.css 'background-color', backgroundColor for pos in ['top', 'right', 'bottom', 'left'] @$el.find(".#{pos}-gradient").css 'background-image', "linear-gradient(to #{pos}, #{backgroundColorTransparent} 0%, #{backgroundColor} 100%)" @playAmbientSound() testParticles: -> return unless @campaign?.loaded and $.browser.chrome # Sometimes this breaks in non-Chrome browsers, according to A/B tests. @particleMan ?= new ParticleMan() @particleMan.removeEmitters() @particleMan.attach @$el.find('.map') for level in @campaign.renderedLevels ? {} particleKey = ['level', @terrain.replace('-branching-test', '')] particleKey.push level.type if level.type and not (level.type in ['hero', 'course']) particleKey.push 'replayable' if level.replayable particleKey.push 'premium' if level.requiresSubscription particleKey.push 'gate' if level.slug in ['kithgard-gates', 'siege-of-stonehold', 'clash-of-clones', 'summits-gate'] particleKey.push 'hero' if level.unlocksHero and not level.unlockedHero #particleKey.push 'item' if level.slug is 'robot-ragnarok' # TODO: generalize continue if particleKey.length is 2 # Don't show basic levels continue unless level.hidden or _.intersection(particleKey, ['item', 'hero-ladder', 'replayable', 'game-dev']).length @particleMan.addEmitter level.position.x / 100, level.position.y / 100, particleKey.join('-') onMouseEnterPortals: (e) -> return unless @campaigns?.loaded and @sessions?.loaded @portalScrollInterval = setInterval @onMouseMovePortals, 100 @onMouseMovePortals e onMouseLeavePortals: (e) -> return unless @portalScrollInterval clearInterval @portalScrollInterval @portalScrollInterval = null onMouseMovePortals: (e) => return unless @portalScrollInterval $portal = @$el.find('.portal') $portals = @$el.find('.portals') if e @portalOffsetX = Math.round Math.max 0, e.clientX - $portal.offset().left bodyWidth = $('body').innerWidth() fraction = @portalOffsetX / bodyWidth return if 0.2 < fraction < 0.8 direction = if fraction < 0.5 then 1 else -1 magnitude = 0.2 * bodyWidth * (if direction is -1 then fraction - 0.8 else 0.2 - fraction) / 0.2 portalsWidth = 1902 # TODO: if we add campaigns or change margins, this will get out of date... scrollTo = $portals.offset().left + direction * magnitude scrollTo = Math.max bodyWidth - portalsWidth, scrollTo scrollTo = Math.min 0, scrollTo $portals.stop().animate {marginLeft: scrollTo}, 100, 'linear' onSessionsLoaded: (e) -> return if @editorMode for session in @sessions.models unless @levelStatusMap[session.get('levelID')] is 'complete' # Don't overwrite a complete session with an incomplete one @levelStatusMap[session.get('levelID')] = if session.get('state')?.complete then 'complete' else 'started' @levelDifficultyMap[session.get('levelID')] = session.get('state').difficulty if session.get('state')?.difficulty @render() @loadUserPollsRecord() unless me.get('anonymous') or window.serverConfig.picoCTF onCampaignsLoaded: (e) -> @render() preloadLevel: (levelSlug) -> levelURL = "/db/level/#{levelSlug}" level = new Level().setURL levelURL level = @supermodel.loadModel(level, null, 0).model sessionURL = "/db/level/#{levelSlug}/session" @preloadedSession = new LevelSession().setURL sessionURL @listenToOnce @preloadedSession, 'sync', @onSessionPreloaded @preloadedSession = @supermodel.loadModel(@preloadedSession, {cache: false}).model @preloadedSession.levelSlug = levelSlug onSessionPreloaded: (session) -> session.url = -> '/db/level.session/' + @id levelElement = @$el.find('.level-info-container:visible') return unless session.levelSlug is levelElement.data 'level-slug' return unless difficulty = session.get('state')?.difficulty badge = $("#{difficulty}") levelElement.find('.start-level .badge').remove() levelElement.find('.start-level').append badge onClickMap: (e) -> @$levelInfo?.hide() if @sessions?.models.length < 3 # Restore the next level higlight for very new players who might otherwise get lost. @highlightElement '.level.next', delay: 500, duration: 60000, rotation: 0, sides: ['top'] onClickLevel: (e) -> e.preventDefault() e.stopPropagation() @$levelInfo?.hide() levelElement = $(e.target).parents('.level') levelSlug = levelElement.data('level-slug') levelOriginal = levelElement.data('level-original') if @editorMode return @trigger 'level-clicked', levelOriginal @$levelInfo = @$el.find(".level-info-container[data-level-slug=#{levelSlug}]").show() @checkForCourseOption levelOriginal @adjustLevelInfoPosition e @endHighlight() @preloadLevel levelSlug onDoubleClickLevel: (e) -> return unless @editorMode levelElement = $(e.target).parents('.level') levelOriginal = levelElement.data('level-original') @trigger 'level-double-clicked', levelOriginal onClickStartLevel: (e) -> levelElement = $(e.target).parents('.level-info-container') levelSlug = levelElement.data('level-slug') levelOriginal = levelElement.data('level-original') level = _.find _.values(@campaign.get('levels')), slug: levelSlug requiresSubscription = level.requiresSubscription or (me.isOnPremiumServer() and not (level.slug in ['dungeons-of-kithgard', 'gems-in-the-deep', 'shadow-guard', 'forgetful-gemsmith', 'signs-and-portents', 'true-names'])) canPlayAnyway = not @requiresSubscription or level.adventurer or @levelStatusMap[level.slug] if requiresSubscription and not canPlayAnyway @openModalView new SubscribeModal() # TODO: Added levelID on 2/9/16. Remove level property and associated AnalyticsLogEvent 'properties.level' index later. window.tracker?.trackEvent 'Show subscription modal', category: 'Subscription', label: 'map level clicked', level: levelSlug, levelID: levelSlug else @startLevel levelElement window.tracker?.trackEvent 'Clicked Start Level', category: 'World Map', levelID: levelSlug onClickCourseVersion: (e) -> levelSlug = $(e.target).parents('.level-info-container').data 'level-slug' courseID = $(e.target).parents('.course-version').data 'course-id' courseInstanceID = $(e.target).parents('.course-version').data 'course-instance-id' url = "/play/level/#{levelSlug}?course=#{courseID}&course-instance=#{courseInstanceID}" Backbone.Mediator.publish 'router:navigate', route: url startLevel: (levelElement) -> @setupManager?.destroy() levelSlug = levelElement.data 'level-slug' session = @preloadedSession if @preloadedSession?.loaded and @preloadedSession.levelSlug is levelSlug @setupManager = new LevelSetupManager supermodel: @supermodel, levelID: levelSlug, levelPath: levelElement.data('level-path'), levelName: levelElement.data('level-name'), hadEverChosenHero: @hadEverChosenHero, parent: @, session: session unless @setupManager?.navigatingToPlay @$levelInfo.find('.level-info, .progress').toggleClass('hide') @listenToOnce @setupManager, 'open', -> @$levelInfo?.find('.level-info, .progress').toggleClass('hide') @$levelInfo?.hide() @setupManager.open() onClickViewSolutions: (e) -> levelElement = $(e.target).parents('.level-info-container') levelSlug = levelElement.data('level-slug') level = _.find _.values(@campaign.get('levels')), slug: levelSlug if level.type in ['hero-ladder', 'course-ladder'] Backbone.Mediator.publish 'router:navigate', route: "/play/ladder/#{levelSlug}", viewClass: 'views/ladder/LadderView', viewArgs: [{supermodel: @supermodel}, levelSlug] else @showLeaderboard levelSlug 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 < 100 top = mapY + 60 @$levelInfo.css('top', top) onWindowResize: (e) => mapHeight = iPadHeight = 1536 mapWidth = {dungeon: 2350, forest: 2500, auditions: 2500, desert: 2350, mountain: 2422, glacier: 2421}[@terrain] or 2350 aspectRatio = mapWidth / mapHeight pageWidth = @$el.width() pageHeight = @$el.height() pageHeight -= adContainerHeight if adContainerHeight = $('.ad-container').outerHeight() 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) @testParticles() if @particleMan playAmbientSound: -> return unless me.get 'volume' return if @ambientSound return unless file = @campaign?.get('ambientSound')?[AudioPlayer.ext.substr 1] src = "/file/#{file}" 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 checkForCourseOption: (levelOriginal) -> return unless me.get('courseInstances')?.length @courseOptionsChecked ?= {} return if @courseOptionsChecked[levelOriginal] @courseOptionsChecked[levelOriginal] = true courseInstances = new CocoCollection [], url: "/db/course_instance/-/find_by_level/#{levelOriginal}", model: CourseInstance courseInstances.comparator = (ci) -> return -(ci.get('members') ? []).length @supermodel.loadCollection courseInstances, 'course_instances' @listenToOnce courseInstances, 'sync', => return if @destroyed return unless courseInstance = courseInstances.models[0] @$el.find(".course-version[data-level-original='#{levelOriginal}']").removeClass('hidden').data('course-id': courseInstance.get('courseID'), 'course-instance-id': courseInstance.id) preloadTopHeroes: -> return if window.serverConfig.picoCTF 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 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() @playAmbientSound() if volume 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] onClickBack: (e) -> Backbone.Mediator.publish 'router:navigate', route: "/play" viewClass: CampaignView viewArgs: [{supermodel: @supermodel}] onClickClearStorage: (e) -> localStorage.clear() noty { text: 'Local storage cleared. Reload to view the original campaign.' layout: 'topCenter' timeout: 5000 type: 'information' } 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 "CampaignView hero update couldn't find hero slug for original:", hero onClickPortalCampaign: (e) -> campaign = $(e.target).closest('.campaign') return if campaign.is('.locked') or campaign.is('.silhouette') campaignSlug = campaign.data('campaign-slug') Backbone.Mediator.publish 'router:navigate', route: "/play/#{campaignSlug}" viewClass: CampaignView viewArgs: [{supermodel: @supermodel}, campaignSlug] loadUserPollsRecord: -> url = "/db/user.polls.record/-/user/#{me.id}" @userPollsRecord = new UserPollsRecord().setURL url onRecordSync = -> return if @destroyed @userPollsRecord.url = -> '/db/user.polls.record/' + @id lastVoted = new Date(@userPollsRecord.get('changed') or 0) interval = new Date() - lastVoted if interval > 22 * 60 * 60 * 1000 # Wait almost a day before showing the next poll @loadPoll() else console.log 'Poll will be ready in', (22 * 60 * 60 * 1000 - interval) / (60 * 60 * 1000), 'hours.' @listenToOnce @userPollsRecord, 'sync', onRecordSync @userPollsRecord = @supermodel.loadModel(@userPollsRecord, null, 0).model onRecordSync.call @ if @userPollsRecord.loaded loadPoll: -> url = "/db/poll/#{@userPollsRecord.id}/next" @poll = new Poll().setURL url onPollSync = -> return if @destroyed @poll.url = -> '/db/poll/' + @id _.delay (=> @activatePoll?()), 1000 onPollError = (poll, response, request) -> if response.status is 404 console.log 'There are no more polls left.' else console.error "Couldn't load poll:", response.status, response.statusText delete @poll @listenToOnce @poll, 'sync', onPollSync @listenToOnce @poll, 'error', onPollError @poll = @supermodel.loadModel(@poll, null, 0).model onPollSync.call @ if @poll.loaded activatePoll: -> pollTitle = utils.i18n @poll.attributes, 'name' $pollButton = @$el.find('button.poll').removeClass('hidden').addClass('highlighted').attr(title: pollTitle).addClass('has-tooltip').tooltip title: pollTitle if me.get('lastLevel') is 'shadow-guard' @showPoll() else $pollButton.tooltip 'show' showPoll: -> pollModal = new PollModal supermodel: @supermodel, poll: @poll, userPollsRecord: @userPollsRecord @openModalView pollModal $pollButton = @$el.find 'button.poll' pollModal.on 'vote-updated', -> $pollButton.removeClass('highlighted').tooltip 'hide'