<commit_before>require('app/styles/play/campaign-view.sass')
RootView = require 'views/core/RootView'
template = require 'templates/play/campaign-view'
LevelSession = require 'models/LevelSession'
EarnedAchievement = require 'models/EarnedAchievement'
CocoCollection = require 'collections/CocoCollection'
Achievements = require 'collections/Achievements'
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 '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'
AnnouncementModal = require 'views/play/modal/AnnouncementModal'
codePlay = require('lib/code-play')
MineModal = require 'views/core/MineModal' # Minecraft modal
CodePlayCreateAccountModal = require 'views/play/modal/CodePlayCreateAccountModal'
api = require 'core/api'
Classroom = require 'models/Classroom'
Course = require 'models/Course'
CourseInstance = require 'models/CourseInstance'
Levels = require 'collections/Levels'
payPal = require('core/services/paypal')
createjs = require 'lib/createjs-parts'
PlayItemsModal = require 'views/play/modal/PlayItemsModal'
PlayHeroesModal = require 'views/play/modal/PlayHeroesModal'
PlayAchievementsModal = require 'views/play/modal/PlayAchievementsModal'
BuyGemsModal = require 'views/play/modal/BuyGemsModal'
ContactModal = require 'views/core/ContactModal'
AnonymousTeacherModal = require 'views/core/AnonymousTeacherModal'
AmazonHocModal = require 'views/play/modal/AmazonHocModal'
require('vendor/scripts/jquery-ui-1.11.1.custom')
require('vendor/styles/jquery-ui-1.11.1.custom.css')
fetchJson = require 'core/api/fetch-json'
HoCModal = require 'views/special_event/HoC2018InterstitialModal.coffee'

require 'lib/game-libraries'

class LevelSessionsCollection extends CocoCollection
  url: ''
  model: LevelSession

  constructor: (model) ->
    super()
    @url = "/db/user/#{me.id}/level.sessions?project=state.complete,levelID,state.difficulty,playtime,state.topScores,codeLanguage,level"

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 #amazon-campaign-logo': 'onClickAmazonCampaign'
    'click #anon-classroom-signup-close': 'onClickAnonClassroomClose'
    'click #anon-classroom-join-btn': 'onClickAnonClassroomJoin'
    'click #anon-classroom-signup-btn': 'onClickAnonClassroomSignup'
    'click .cube-level': 'onSpinningCubeClick' # Minecraft Modal
    'click .map-background': 'onClickMap'
    'click .level': 'onClickLevel'
    'dblclick .level': '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'
    'click .portal .beta-campaign': 'onClickPortalCampaign'
    'click a .campaign-switch': 'onClickCampaignSwitch'
    'mouseenter .portals': 'onMouseEnterPortals'
    'mouseleave .portals': 'onMouseLeavePortals'
    'mousemove .portals': 'onMouseMovePortals'
    'click .poll': 'showPoll'
    'click #brain-pop-replay-btn': 'onClickBrainPopReplayButton'
    'click .premium-menu-icon': 'onClickPremiumButton'
    'click [data-toggle="coco-modal"][data-target="play/modal/PlayItemsModal"]': 'openPlayItemsModal'
    'click [data-toggle="coco-modal"][data-target="play/modal/PlayHeroesModal"]': 'openPlayHeroesModal'
    'click [data-toggle="coco-modal"][data-target="play/modal/PlayAchievementsModal"]': 'openPlayAchievementsModal'
    'click [data-toggle="coco-modal"][data-target="play/modal/BuyGemsModal"]': 'openBuyGemsModal'
    'click [data-toggle="coco-modal"][data-target="core/ContactModal"]': 'openContactModal'
    'click [data-toggle="coco-modal"][data-target="core/CreateAccountModal"]': 'openCreateAccountModal'
    'click [data-toggle="coco-modal"][data-target="core/AnonymousTeacherModal"]': 'openAnonymousTeacherModal'

  shortcuts:
    'shift+s': 'onShiftS'

  constructor: (options, @terrain) ->
    super options
    @terrain = 'picoctf' if window.serverConfig.picoCTF
    @editorMode = options?.editorMode
    @requiresSubscription = not me.isPremium()
    if @editorMode
      @terrain ?= 'dungeon'
    @levelStatusMap = {}
    @levelPlayCountMap = {}
    @levelDifficultyMap = {}
    @levelScoreMap = {}

    if @terrain is "hoc-2018"
      $('body').append($("<img src='https://code.org/api/hour/begin_codecombat_play.png' style='visibility: hidden;'>"))

    if utils.getQueryVariable('hour_of_code')
      if me.isStudent() or me.isTeacher()
        if @terrain is 'dungeon'
          newCampaign = 'intro'
          api.users.getCourseInstances({ userID: me.id, campaignSlug: newCampaign }, { data: { project: '_id' } })
          .then (courseInstances) =>
            if courseInstances.length
              courseInstanceID = _.first(courseInstances)._id
              application.router.navigate("/play/#{newCampaign}?course-instance=#{courseInstanceID}", { trigger: true, replace: true })
            else
              application.router.navigate((if me.isStudent() then '/students' else '/teachers'), {trigger: true, replace: true})
              noty({text: 'Please create or join a classroom first', layout: 'topCenter', timeout: 8000, type: 'success'})
          return
      if @terrain is 'game-dev-hoc'
        window.tracker?.trackEvent 'Start HoC Campaign', label: 'game-dev-hoc'
      me.set('hourOfCode', true)
      me.patch()
      pixelCode = switch @terrain
        when 'game-dev-hoc' then 'code_combat_gamedev'
        when 'game-dev-hoc-2' then 'code_combat_build_arcade'
        else 'code_combat'
      $('body').append($("<img src='https://code.org/api/hour/begin_#{pixelCode}.png' style='visibility: hidden;'>"))
    else if me.isTeacher() and not utils.getQueryVariable('course-instance') and
        not application.getHocCampaign() and not @terrain is "hoc-2018"
      # redirect teachers away from home campaigns
      application.router.navigate('/teachers', { trigger: true, replace: true })
      return
    else if location.pathname is '/paypal/subscribe-callback'
      @payPalToken = utils.getQueryVariable('token')
      api.users.executeBillingAgreement({userID: me.id, token: @payPalToken})
      .then (billingAgreement) =>
        value = Math.round(parseFloat(billingAgreement?.plan?.payment_definitions?[0].amount?.value ? 0) * 100)
        application.tracker?.trackEvent 'Finished subscription purchase', { value, service: 'paypal' }
        noty({text: $.i18n.t('subscribe.confirmation'), layout: 'topCenter', timeout: 8000})
        me.fetch(cache: false, success: => @render?())
      .catch (err) =>
        console.error(err)

    if window.serverConfig.picoCTF
      @supermodel.addRequestResource(url: '/picoctf/problems', success: (@picoCTFProblems) =>).load()
    else
      unless @editorMode
        @sessions = @supermodel.loadCollection(new LevelSessionsCollection(), 'your_sessions', {cache: false}, 1).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})

    if utils.getQueryVariable('course-instance')?
      @courseLevelsFake = {}
      @courseInstanceID = utils.getQueryVariable('course-instance')
      @courseInstance = new CourseInstance(_id: @courseInstanceID)
      jqxhr = @courseInstance.fetch()
      @supermodel.trackRequest(jqxhr)
      new Promise(jqxhr.then).then(=>
        courseID = @courseInstance.get('courseID')

        @course = new Course(_id: courseID)
        @supermodel.trackRequest @course.fetch()
        if @courseInstance.get('classroomID')
          classroomID = @courseInstance.get('classroomID')
          @classroom = new Classroom(_id: classroomID)
          @supermodel.trackRequest @classroom.fetch()
          @listenToOnce @classroom, 'sync', =>
            @updateClassroomSessions()
            @render()
            @courseInstance.sessions = new CocoCollection([], {
              url: @courseInstance.url() + '/course-level-sessions/' + me.id,
              model: LevelSession
            })
            @supermodel.loadCollection(@courseInstance.sessions, {
              data: { project: 'state.complete,level.original,playtime,changed,state.topScores' }
            })
            @courseInstance.sessions.comparator = 'changed'
            @listenToOnce @courseInstance.sessions, 'sync', =>
              @courseStats = @classroom.statsForSessions(@courseInstance.sessions, @course.id)
              @render()
            @courseLevels = new Levels()
            @supermodel.trackRequest @courseLevels.fetchForClassroomAndCourse(classroomID, courseID, {
              data: { project: 'concepts,practice,assessment,primerLanguage,type,slug,name,original,description,shareable,i18n' }
            })
            @listenToOnce @courseLevels, 'sync', =>
              existing = @campaign.get('levels')
              courseLevels = @courseLevels.toArray()
              classroomCourse = _.find(currentView.classroom.get('courses'), {_id:currentView.course.id})
              levelPositions = {}
              for level in classroomCourse.levels
                levelPositions[level.original] = level.position if level.position
              for k,v of courseLevels
                idx = v.get('original')
                if not existing[idx]
                  # a level which has been removed from the campaign but is saved in the course
                  @courseLevelsFake[idx] = v.toJSON()
                else
                  @courseLevelsFake[idx] = existing[idx]
                  # carry over positions stored in course, if there are any
                  if levelPositions[idx]
                    @courseLevelsFake[idx].position = levelPositions[idx]
                @courseLevelsFake[idx].courseIdx = parseInt(k)
                @courseLevelsFake[idx].requiresSubscription = false
              # Fill in missing positions, for courses which have levels that no longer exist in campaigns
              for k,v of courseLevels
                k = parseInt(k)
                idx = v.get('original')
                if not @courseLevelsFake[idx].position
                  prevLevel = courseLevels[k-1]
                  nextLevel = courseLevels[k+1]
                  if prevLevel && nextLevel
                    prevIdx = prevLevel.get('original')
                    nextIdx = nextLevel.get('original')
                    prevPosition = @courseLevelsFake[prevIdx].position
                    nextPosition = @courseLevelsFake[nextIdx].position
                  if prevPosition && nextPosition
                    # split the diff between the previous, next levels
                    @courseLevelsFake[idx].position = {
                      x: (prevPosition.x + nextPosition.x)/2
                      y: (prevPosition.y + nextPosition.y)/2
                    }
                  else
                    # otherwise just line them up along the bottom
                    x = 10 + (k / courseLevels.length) * 80
                    @courseLevelsFake[idx].position = { x, y: 10 }
              @render()
      )

    @listenToOnce @campaign, 'sync', @getLevelPlayCounts
    $(window).on 'resize', @onWindowResize
    @probablyCachedMusic = storage.load("loaded-menu-music")
    musicDelay = if @probablyCachedMusic then 1000 else 10000
    delayMusicStart = => _.delay (=> @playMusic() unless @destroyed), musicDelay
    @playMusicTimeout = delayMusicStart()
    @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()

    if utils.getQueryVariable('hour_of_code') or @terrain is "hoc-2018"
      if not sessionStorage.getItem(@terrain)
        sessionStorage.setItem(@terrain, "seen-modal")
        clearTimeout(@playMusicTimeout)
        setTimeout(=>
            @openModalView new HoCModal({
              showVideo: @terrain is "hoc-2018",
              onDestroy: delayMusicStart,
            })
        , 0)

    window.tracker?.trackEvent 'Loaded World Map', category: 'World Map', label: @terrain

  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'

  openPlayItemsModal: (e) ->
    e.stopPropagation()
    @openModalView new PlayItemsModal()

  openPlayHeroesModal: (e) ->
    e.stopPropagation()
    @openModalView new PlayHeroesModal()

  openPlayAchievementsModal: (e) ->
    e.stopPropagation()
    @openModalView new PlayAchievementsModal()

  openBuyGemsModal: (e) ->
    e.stopPropagation()
    @openModalView new BuyGemsModal()

  openContactModal: (e) ->
    e.stopPropagation()
    @openModalView new ContactModal()

  openCreateAccountModal: (e) ->
    e.stopPropagation()
    @openModalView new CreateAccountModal()

  openAnonymousTeacherModal: (e) ->
    e.stopPropagation()
    @openModalView new AnonymousTeacherModal()
    @endHighlight()

  onClickAmazonCampaign: (e) ->
    window.tracker?.trackEvent 'Click Amazon Modal Button'
    @openModalView new AmazonHocModal hideCongratulation: true

  onClickAnonClassroomClose: -> @$el.find('#anonymous-classroom-signup-dialog')?.hide()

  onClickAnonClassroomJoin: ->
    classCode = @$el.find('#anon-classroom-signup-code')?.val()
    return if _.isEmpty(classCode)
    window.tracker?.trackEvent 'Anonymous Classroom Signup Modal Join Class', category: 'Signup', classCode
    application.router.navigate("/students?_cc=#{classCode}", { trigger: true })

  onClickAnonClassroomSignup: ->
    window.tracker?.trackEvent 'Anonymous Classroom Signup Modal Create Teacher', category: 'Signup'
    @openModalView(new CreateAccountModal({startOnPath: 'teacher'}))

  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 @getLevels())
    levelPlayCountsRequest = @supermodel.addRequestResource 'play_counts', {
      url: '/db/level/-/play_counts'
      data: {ids: levelSlugs}
      method: 'POST'
      success: success
    }, 0
    levelPlayCountsRequest.load()

  onLoaded: ->
    if @isClassroom()
      @updateClassroomSessions()
    else
      unless @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

    @buildLevelScoreMap() unless @editorMode
    # HoC: Fake us up a "mode" for HeroVictoryModal to return hero without levels realizing they're in a copycat campaign, or clear it if we started playing.
    application.setHocCampaign(if @campaign?.get('type') is 'hoc' then @campaign.get('slug') else '')

    return if @fullyRendered
    @fullyRendered = true
    @render()
    @checkForUnearnedAchievements()
    @preloadTopHeroes() unless me.get('heroConfig')?.thangType
    @$el.find('#campaign-status').delay(4000).animate({top: "-=58"}, 1000) if @terrain in ['forest', 'desert']
    if @campaign and @isRTL utils.i18n(@campaign.attributes, 'fullName')
      @$('.campaign-name').attr('dir', 'rtl')
    if not me.get('hourOfCode') and @terrain
      if features.codePlay
        if me.get('anonymous') and me.get('lastLevel') is 'true-names' and me.level() < 5
          @openModalView new CodePlayCreateAccountModal()
      else if me.get('name') and me.get('lastLevel') in ['forgetful-gemsmith', 'signs-and-portents', 'true-names'] 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()
    else
      @maybeShowPendingAnnouncement()

    # Minecraft Modal:
    #@maybeShowMinecraftModal() # Disable for now


  updateClassroomSessions: ->
    if @classroom
      classroomLevels = @classroom.getLevels()
      @classroomLevelMap = _.zipObject(classroomLevels.map((l) -> l.get('original')), classroomLevels.models)
      defaultLanguage = @classroom.get('aceConfig').language
      for session in @sessions.slice()
        classroomLevel = @classroomLevelMap[session.get('level').original]
        if not classroomLevel
          continue
        expectedLanguage = classroomLevel.get('primerLanguage') or defaultLanguage
        if session.get('codeLanguage') isnt expectedLanguage
          # console.log("Inside remove session")
          @sessions.remove(session)
          continue
      unless @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
       
  buildLevelScoreMap: ->
    for session in @sessions.models
      levels = @getLevels()
      return unless levels
      levelOriginal = session.get('level')?.original
      continue unless levelOriginal
      level = levels[levelOriginal]
      topScore = _.first(LevelSession.getTopScores({session: session.toJSON(), level}))
      @levelScoreMap[levelOriginal] = topScore

  # Minecraft Modal:
  maybeShowMinecraftModal: ->
    return false if me.freeOnly()
    userQualifiesForMinecraftModal = (user) ->
      return true if user.isAdmin()
      return false if user.isPremium()
      return false if user.isAnonymous()
      return user.get('testGroupNumber') % 5 is 1

    return unless userQualifiesForMinecraftModal(me)
    if @campaign and @campaign.get('levels')
      levels = @campaign.get('levels')
      level = _.find(levels, {slug: "the-second-kithmaze"})
      if level and @levelStatusMap['the-second-kithmaze'] is 'complete' and /^en/i.test(me.get('preferredLanguage', true))
        $(".cube-level").show()

  # Minecraft Modal:
  onSpinningCubeClick: (e) ->
    window.tracker?.trackEvent "Mine Explored", engageAction: "campaign_level_click"
    @openModalView new MineModal()

  setCampaign: (@campaign) ->
    @render()

  onSubscribed: ->
    @requiresSubscription = false
    @render()

  getRenderData: (context={}) ->
    context = super(context)
    context.campaign = @campaign
    context.levels = _.values($.extend true, {}, @getLevels() ? {})
    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
    if me.freeOnly()
      context.levels = _.reject context.levels, (level) ->
        return false if features.codePlay and codePlay.canPlay(level.slug)
        return level.requiresSubscription
    if features.brainPop
      context.levels = _.filter context.levels, (level) ->
        level.slug in ['dungeons-of-kithgard', 'gems-in-the-deep', 'shadow-guard', 'true-names']
    @annotateLevels(context.levels)
    count = @countLevels context.levels
    if @courseStats?
      context.levelsCompleted = @courseStats.levels.numDone
      context.levelsTotal = @courseStats.levels.size
    else
      context.levelsCompleted = count.completed
      context.levelsTotal = count.total

    @determineNextLevel context.levels if @sessions?.loaded or @editorMode
    # 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 me.isStudent() or me.isTeacher()
      if ac.showIfUnlocked and not @editorMode
        return false if _.isString(ac.showIfUnlocked) and ac.showIfUnlocked not in me.levels()
        return false if _.isArray(ac.showIfUnlocked) and _.intersection(ac.showIfUnlocked, me.levels()).length <= 0
      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') ? {})
          if me.level() < 12 and campaign.get('slug') is 'dungeon' and not @editorMode
            reject = if me.getFourthLevelGroup() is 'signs-and-portents' then 'forgetful-gemsmith' else 'signs-and-portents'
            levels = _.reject levels, slug: reject
          if me.freeOnly()
            levels = _.reject levels, (level) ->
              return false if features.codePlay and codePlay.canPlay(level.slug)
              return level.requiresSubscription
          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') ? {}
          if _.isString(ac.showIfUnlocked)
            _.find(@campaigns.models, id: acID)?.locked = false if ac.showIfUnlocked in me.levels()
          else if _.isArray(ac.showIfUnlocked)
            _.find(@campaigns.models, id: acID)?.locked = false if _.intersection(ac.showIfUnlocked, me.levels()).length > 0

    if @terrain and _.string.contains(@terrain, 'hoc') and me.isTeacher()
      context.showGameDevAlert = true

    context

  afterRender: ->
    super()
    @onWindowResize()<commit_msg>Allow 'enter' to submit class code on CampaignView (#5057)<commit_after>require('app/styles/play/campaign-view.sass')
RootView = require 'views/core/RootView'
template = require 'templates/play/campaign-view'
LevelSession = require 'models/LevelSession'
EarnedAchievement = require 'models/EarnedAchievement'
CocoCollection = require 'collections/CocoCollection'
Achievements = require 'collections/Achievements'
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 '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'
AnnouncementModal = require 'views/play/modal/AnnouncementModal'
codePlay = require('lib/code-play')
MineModal = require 'views/core/MineModal' # Minecraft modal
CodePlayCreateAccountModal = require 'views/play/modal/CodePlayCreateAccountModal'
api = require 'core/api'
Classroom = require 'models/Classroom'
Course = require 'models/Course'
CourseInstance = require 'models/CourseInstance'
Levels = require 'collections/Levels'
payPal = require('core/services/paypal')
createjs = require 'lib/createjs-parts'
PlayItemsModal = require 'views/play/modal/PlayItemsModal'
PlayHeroesModal = require 'views/play/modal/PlayHeroesModal'
PlayAchievementsModal = require 'views/play/modal/PlayAchievementsModal'
BuyGemsModal = require 'views/play/modal/BuyGemsModal'
ContactModal = require 'views/core/ContactModal'
AnonymousTeacherModal = require 'views/core/AnonymousTeacherModal'
AmazonHocModal = require 'views/play/modal/AmazonHocModal'
require('vendor/scripts/jquery-ui-1.11.1.custom')
require('vendor/styles/jquery-ui-1.11.1.custom.css')
fetchJson = require 'core/api/fetch-json'
HoCModal = require 'views/special_event/HoC2018InterstitialModal.coffee'

require 'lib/game-libraries'

class LevelSessionsCollection extends CocoCollection
  url: ''
  model: LevelSession

  constructor: (model) ->
    super()
    @url = "/db/user/#{me.id}/level.sessions?project=state.complete,levelID,state.difficulty,playtime,state.topScores,codeLanguage,level"

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 #amazon-campaign-logo': 'onClickAmazonCampaign'
    'click #anon-classroom-signup-close': 'onClickAnonClassroomClose'
    'click #anon-classroom-join-btn': 'onClickAnonClassroomJoin'
    'click #anon-classroom-signup-btn': 'onClickAnonClassroomSignup'
    'click .cube-level': 'onSpinningCubeClick' # Minecraft Modal
    'click .map-background': 'onClickMap'
    'click .level': 'onClickLevel'
    'dblclick .level': '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'
    'click .portal .beta-campaign': 'onClickPortalCampaign'
    'click a .campaign-switch': 'onClickCampaignSwitch'
    'mouseenter .portals': 'onMouseEnterPortals'
    'mouseleave .portals': 'onMouseLeavePortals'
    'mousemove .portals': 'onMouseMovePortals'
    'click .poll': 'showPoll'
    'click #brain-pop-replay-btn': 'onClickBrainPopReplayButton'
    'click .premium-menu-icon': 'onClickPremiumButton'
    'click [data-toggle="coco-modal"][data-target="play/modal/PlayItemsModal"]': 'openPlayItemsModal'
    'click [data-toggle="coco-modal"][data-target="play/modal/PlayHeroesModal"]': 'openPlayHeroesModal'
    'click [data-toggle="coco-modal"][data-target="play/modal/PlayAchievementsModal"]': 'openPlayAchievementsModal'
    'click [data-toggle="coco-modal"][data-target="play/modal/BuyGemsModal"]': 'openBuyGemsModal'
    'click [data-toggle="coco-modal"][data-target="core/ContactModal"]': 'openContactModal'
    'click [data-toggle="coco-modal"][data-target="core/CreateAccountModal"]': 'openCreateAccountModal'
    'click [data-toggle="coco-modal"][data-target="core/AnonymousTeacherModal"]': 'openAnonymousTeacherModal'

  shortcuts:
    'shift+s': 'onShiftS'

  constructor: (options, @terrain) ->
    super options
    @terrain = 'picoctf' if window.serverConfig.picoCTF
    @editorMode = options?.editorMode
    @requiresSubscription = not me.isPremium()
    if @editorMode
      @terrain ?= 'dungeon'
    @levelStatusMap = {}
    @levelPlayCountMap = {}
    @levelDifficultyMap = {}
    @levelScoreMap = {}

    if @terrain is "hoc-2018"
      $('body').append($("<img src='https://code.org/api/hour/begin_codecombat_play.png' style='visibility: hidden;'>"))

    if utils.getQueryVariable('hour_of_code')
      if me.isStudent() or me.isTeacher()
        if @terrain is 'dungeon'
          newCampaign = 'intro'
          api.users.getCourseInstances({ userID: me.id, campaignSlug: newCampaign }, { data: { project: '_id' } })
          .then (courseInstances) =>
            if courseInstances.length
              courseInstanceID = _.first(courseInstances)._id
              application.router.navigate("/play/#{newCampaign}?course-instance=#{courseInstanceID}", { trigger: true, replace: true })
            else
              application.router.navigate((if me.isStudent() then '/students' else '/teachers'), {trigger: true, replace: true})
              noty({text: 'Please create or join a classroom first', layout: 'topCenter', timeout: 8000, type: 'success'})
          return
      if @terrain is 'game-dev-hoc'
        window.tracker?.trackEvent 'Start HoC Campaign', label: 'game-dev-hoc'
      me.set('hourOfCode', true)
      me.patch()
      pixelCode = switch @terrain
        when 'game-dev-hoc' then 'code_combat_gamedev'
        when 'game-dev-hoc-2' then 'code_combat_build_arcade'
        else 'code_combat'
      $('body').append($("<img src='https://code.org/api/hour/begin_#{pixelCode}.png' style='visibility: hidden;'>"))
    else if me.isTeacher() and not utils.getQueryVariable('course-instance') and
        not application.getHocCampaign() and not @terrain is "hoc-2018"
      # redirect teachers away from home campaigns
      application.router.navigate('/teachers', { trigger: true, replace: true })
      return
    else if location.pathname is '/paypal/subscribe-callback'
      @payPalToken = utils.getQueryVariable('token')
      api.users.executeBillingAgreement({userID: me.id, token: @payPalToken})
      .then (billingAgreement) =>
        value = Math.round(parseFloat(billingAgreement?.plan?.payment_definitions?[0].amount?.value ? 0) * 100)
        application.tracker?.trackEvent 'Finished subscription purchase', { value, service: 'paypal' }
        noty({text: $.i18n.t('subscribe.confirmation'), layout: 'topCenter', timeout: 8000})
        me.fetch(cache: false, success: => @render?())
      .catch (err) =>
        console.error(err)

    if window.serverConfig.picoCTF
      @supermodel.addRequestResource(url: '/picoctf/problems', success: (@picoCTFProblems) =>).load()
    else
      unless @editorMode
        @sessions = @supermodel.loadCollection(new LevelSessionsCollection(), 'your_sessions', {cache: false}, 1).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})

    if utils.getQueryVariable('course-instance')?
      @courseLevelsFake = {}
      @courseInstanceID = utils.getQueryVariable('course-instance')
      @courseInstance = new CourseInstance(_id: @courseInstanceID)
      jqxhr = @courseInstance.fetch()
      @supermodel.trackRequest(jqxhr)
      new Promise(jqxhr.then).then(=>
        courseID = @courseInstance.get('courseID')

        @course = new Course(_id: courseID)
        @supermodel.trackRequest @course.fetch()
        if @courseInstance.get('classroomID')
          classroomID = @courseInstance.get('classroomID')
          @classroom = new Classroom(_id: classroomID)
          @supermodel.trackRequest @classroom.fetch()
          @listenToOnce @classroom, 'sync', =>
            @updateClassroomSessions()
            @render()
            @courseInstance.sessions = new CocoCollection([], {
              url: @courseInstance.url() + '/course-level-sessions/' + me.id,
              model: LevelSession
            })
            @supermodel.loadCollection(@courseInstance.sessions, {
              data: { project: 'state.complete,level.original,playtime,changed,state.topScores' }
            })
            @courseInstance.sessions.comparator = 'changed'
            @listenToOnce @courseInstance.sessions, 'sync', =>
              @courseStats = @classroom.statsForSessions(@courseInstance.sessions, @course.id)
              @render()
            @courseLevels = new Levels()
            @supermodel.trackRequest @courseLevels.fetchForClassroomAndCourse(classroomID, courseID, {
              data: { project: 'concepts,practice,assessment,primerLanguage,type,slug,name,original,description,shareable,i18n' }
            })
            @listenToOnce @courseLevels, 'sync', =>
              existing = @campaign.get('levels')
              courseLevels = @courseLevels.toArray()
              classroomCourse = _.find(currentView.classroom.get('courses'), {_id:currentView.course.id})
              levelPositions = {}
              for level in classroomCourse.levels
                levelPositions[level.original] = level.position if level.position
              for k,v of courseLevels
                idx = v.get('original')
                if not existing[idx]
                  # a level which has been removed from the campaign but is saved in the course
                  @courseLevelsFake[idx] = v.toJSON()
                else
                  @courseLevelsFake[idx] = existing[idx]
                  # carry over positions stored in course, if there are any
                  if levelPositions[idx]
                    @courseLevelsFake[idx].position = levelPositions[idx]
                @courseLevelsFake[idx].courseIdx = parseInt(k)
                @courseLevelsFake[idx].requiresSubscription = false
              # Fill in missing positions, for courses which have levels that no longer exist in campaigns
              for k,v of courseLevels
                k = parseInt(k)
                idx = v.get('original')
                if not @courseLevelsFake[idx].position
                  prevLevel = courseLevels[k-1]
                  nextLevel = courseLevels[k+1]
                  if prevLevel && nextLevel
                    prevIdx = prevLevel.get('original')
                    nextIdx = nextLevel.get('original')
                    prevPosition = @courseLevelsFake[prevIdx].position
                    nextPosition = @courseLevelsFake[nextIdx].position
                  if prevPosition && nextPosition
                    # split the diff between the previous, next levels
                    @courseLevelsFake[idx].position = {
                      x: (prevPosition.x + nextPosition.x)/2
                      y: (prevPosition.y + nextPosition.y)/2
                    }
                  else
                    # otherwise just line them up along the bottom
                    x = 10 + (k / courseLevels.length) * 80
                    @courseLevelsFake[idx].position = { x, y: 10 }
              @render()
      )

    @listenToOnce @campaign, 'sync', @getLevelPlayCounts
    $(window).on 'resize', @onWindowResize
    @probablyCachedMusic = storage.load("loaded-menu-music")
    musicDelay = if @probablyCachedMusic then 1000 else 10000
    delayMusicStart = => _.delay (=> @playMusic() unless @destroyed), musicDelay
    @playMusicTimeout = delayMusicStart()
    @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()

    if utils.getQueryVariable('hour_of_code') or @terrain is "hoc-2018"
      if not sessionStorage.getItem(@terrain)
        sessionStorage.setItem(@terrain, "seen-modal")
        clearTimeout(@playMusicTimeout)
        setTimeout(=>
            @openModalView new HoCModal({
              showVideo: @terrain is "hoc-2018",
              onDestroy: delayMusicStart,
            })
        , 0)

    window.tracker?.trackEvent 'Loaded World Map', category: 'World Map', label: @terrain

  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'

  openPlayItemsModal: (e) ->
    e.stopPropagation()
    @openModalView new PlayItemsModal()

  openPlayHeroesModal: (e) ->
    e.stopPropagation()
    @openModalView new PlayHeroesModal()

  openPlayAchievementsModal: (e) ->
    e.stopPropagation()
    @openModalView new PlayAchievementsModal()

  openBuyGemsModal: (e) ->
    e.stopPropagation()
    @openModalView new BuyGemsModal()

  openContactModal: (e) ->
    e.stopPropagation()
    @openModalView new ContactModal()

  openCreateAccountModal: (e) ->
    e.stopPropagation()
    @openModalView new CreateAccountModal()

  openAnonymousTeacherModal: (e) ->
    e.stopPropagation()
    @openModalView new AnonymousTeacherModal()
    @endHighlight()

  onClickAmazonCampaign: (e) ->
    window.tracker?.trackEvent 'Click Amazon Modal Button'
    @openModalView new AmazonHocModal hideCongratulation: true

  onClickAnonClassroomClose: -> @$el.find('#anonymous-classroom-signup-dialog')?.hide()

  onClickAnonClassroomJoin: ->
    classCode = @$el.find('#anon-classroom-signup-code')?.val()
    return if _.isEmpty(classCode)
    window.tracker?.trackEvent 'Anonymous Classroom Signup Modal Join Class', category: 'Signup', classCode
    application.router.navigate("/students?_cc=#{classCode}", { trigger: true })

  onClickAnonClassroomSignup: ->
    window.tracker?.trackEvent 'Anonymous Classroom Signup Modal Create Teacher', category: 'Signup'
    @openModalView(new CreateAccountModal({startOnPath: 'teacher'}))

  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 @getLevels())
    levelPlayCountsRequest = @supermodel.addRequestResource 'play_counts', {
      url: '/db/level/-/play_counts'
      data: {ids: levelSlugs}
      method: 'POST'
      success: success
    }, 0
    levelPlayCountsRequest.load()

  onLoaded: ->
    if @isClassroom()
      @updateClassroomSessions()
    else
      unless @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

    @buildLevelScoreMap() unless @editorMode
    # HoC: Fake us up a "mode" for HeroVictoryModal to return hero without levels realizing they're in a copycat campaign, or clear it if we started playing.
    application.setHocCampaign(if @campaign?.get('type') is 'hoc' then @campaign.get('slug') else '')

    return if @fullyRendered
    @fullyRendered = true
    @render()
    @checkForUnearnedAchievements()
    @preloadTopHeroes() unless me.get('heroConfig')?.thangType
    @$el.find('#campaign-status').delay(4000).animate({top: "-=58"}, 1000) if @terrain in ['forest', 'desert']
    if @campaign and @isRTL utils.i18n(@campaign.attributes, 'fullName')
      @$('.campaign-name').attr('dir', 'rtl')
    if not me.get('hourOfCode') and @terrain
      if features.codePlay
        if me.get('anonymous') and me.get('lastLevel') is 'true-names' and me.level() < 5
          @openModalView new CodePlayCreateAccountModal()
      else if me.get('name') and me.get('lastLevel') in ['forgetful-gemsmith', 'signs-and-portents', 'true-names'] 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()
    else
      @maybeShowPendingAnnouncement()

    # Minecraft Modal:
    #@maybeShowMinecraftModal() # Disable for now


  updateClassroomSessions: ->
    if @classroom
      classroomLevels = @classroom.getLevels()
      @classroomLevelMap = _.zipObject(classroomLevels.map((l) -> l.get('original')), classroomLevels.models)
      defaultLanguage = @classroom.get('aceConfig').language
      for session in @sessions.slice()
        classroomLevel = @classroomLevelMap[session.get('level').original]
        if not classroomLevel
          continue
        expectedLanguage = classroomLevel.get('primerLanguage') or defaultLanguage
        if session.get('codeLanguage') isnt expectedLanguage
          # console.log("Inside remove session")
          @sessions.remove(session)
          continue
      unless @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
       
  buildLevelScoreMap: ->
    for session in @sessions.models
      levels = @getLevels()
      return unless levels
      levelOriginal = session.get('level')?.original
      continue unless levelOriginal
      level = levels[levelOriginal]
      topScore = _.first(LevelSession.getTopScores({session: session.toJSON(), level}))
      @levelScoreMap[levelOriginal] = topScore

  # Minecraft Modal:
  maybeShowMinecraftModal: ->
    return false if me.freeOnly()
    userQualifiesForMinecraftModal = (user) ->
      return true if user.isAdmin()
      return false if user.isPremium()
      return false if user.isAnonymous()
      return user.get('testGroupNumber') % 5 is 1

    return unless userQualifiesForMinecraftModal(me)
    if @campaign and @campaign.get('levels')
      levels = @campaign.get('levels')
      level = _.find(levels, {slug: "the-second-kithmaze"})
      if level and @levelStatusMap['the-second-kithmaze'] is 'complete' and /^en/i.test(me.get('preferredLanguage', true))
        $(".cube-level").show()

  # Minecraft Modal:
  onSpinningCubeClick: (e) ->
    window.tracker?.trackEvent "Mine Explored", engageAction: "campaign_level_click"
    @openModalView new MineModal()

  setCampaign: (@campaign) ->
    @render()

  onSubscribed: ->
    @requiresSubscription = false
    @render()

  getRenderData: (context={}) ->
    context = super(context)
    context.campaign = @campaign
    context.levels = _.values($.extend true, {}, @getLevels() ? {})
    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
    if me.freeOnly()
      context.levels = _.reject context.levels, (level) ->
        return false if features.codePlay and codePlay.canPlay(level.slug)
        return level.requiresSubscription
    if features.brainPop
      context.levels = _.filter context.levels, (level) ->
        level.slug in ['dungeons-of-kithgard', 'gems-in-the-deep', 'shadow-guard', 'true-names']
    @annotateLevels(context.levels)
    count = @countLevels context.levels
    if @courseStats?
      context.levelsCompleted = @courseStats.levels.numDone
      context.levelsTotal = @courseStats.levels.size
    else
      context.levelsCompleted = count.completed
      context.levelsTotal = count.total

    @determineNextLevel context.levels if @sessions?.loaded or @editorMode
    # 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 me.isStudent() or me.isTeacher()
      if ac.showIfUnlocked and not @editorMode
        return false if _.isString(ac.showIfUnlocked) and ac.showIfUnlocked not in me.levels()
        return false if _.isArray(ac.showIfUnlocked) and _.intersection(ac.showIfUnlocked, me.levels()).length <= 0
      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') ? {})
          if me.level() < 12 and campaign.get('slug') is 'dungeon' and not @editorMode
            reject = if me.getFourthLevelGroup() is 'signs-and-portents' then 'forgetful-gemsmith' else 'signs-and-portents'
            levels = _.reject levels, slug: reject
          if me.freeOnly()
            levels = _.reject levels, (level) ->
              return false if features.codePlay and codePlay.canPlay(level.slug)
              return level.requiresSubscription
          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') ? {}
          if _.isString(ac.showIfUnlocked)
            _.find(@campaigns.models, id: acID)?.locked = false if ac.showIfUnlocked in me.levels()
          else if _.isArray(ac.showIfUnlocked)
            _.find(@campaigns.models, id: acID)?.locked = false if _.intersection(ac.showIfUnlocked, me.levels()).length > 0

    if @terrain and _.string.contains(@terrain, 'hoc') and me.isTeacher()
      context.showGameDevAlert = true

    context

  afterRender: ->
    super()
    @onWindowResize()

    $('#anon-classroom-signup-code').keydown (event) ->
      if (event.keyCode == 13)
        # click join classroom button if enter is pressed in the text box
        $("#anon-classroom-join-btn").click()

{'fraction_non_alphanumeric': 0.12292200099361791, 'fraction_numerical': 0.005006305652157298, 'mean_word_length': 3.325921639940486, 'pattern_counts': {'":': 0, '<': 17, '<?xml version=': 0, '>': 114, 'https://': 4, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3498609', 'n_tokens_mistral': 16105, 'n_tokens_neox': 15315, 'n_words': 3371}