RootView = require 'views/core/RootView' template = require 'templates/play/play-level-view' {me} = require 'core/auth' ThangType = require 'models/ThangType' utils = require 'core/utils' storage = require 'core/storage' {createAetherOptions} = require 'lib/aether_utils' # tools Surface = require 'lib/surface/Surface' God = require 'lib/God' GoalManager = require 'lib/world/GoalManager' ScriptManager = require 'lib/scripts/ScriptManager' LevelBus = require 'lib/LevelBus' LevelLoader = require 'lib/LevelLoader' LevelSession = require 'models/LevelSession' Level = require 'models/Level' LevelComponent = require 'models/LevelComponent' Article = require 'models/Article' Camera = require 'lib/surface/Camera' AudioPlayer = require 'lib/AudioPlayer' Simulator = require 'lib/simulator/Simulator' GameUIState = require 'models/GameUIState' # subviews LevelLoadingView = require './LevelLoadingView' ProblemAlertView = require './tome/ProblemAlertView' TomeView = require './tome/TomeView' ChatView = require './LevelChatView' HUDView = require './LevelHUDView' LevelDialogueView = require './LevelDialogueView' ControlBarView = require './ControlBarView' LevelPlaybackView = require './LevelPlaybackView' GoalsView = require './LevelGoalsView' LevelFlagsView = require './LevelFlagsView' GoldView = require './LevelGoldView' DuelStatsView = require './DuelStatsView' VictoryModal = require './modal/VictoryModal' HeroVictoryModal = require './modal/HeroVictoryModal' CourseVictoryModal = require './modal/CourseVictoryModal' PicoCTFVictoryModal = require './modal/PicoCTFVictoryModal' InfiniteLoopModal = require './modal/InfiniteLoopModal' LevelSetupManager = require 'lib/LevelSetupManager' ContactModal = require 'views/core/ContactModal' HintsView = require './HintsView' HintsState = require './HintsState' WebSurfaceView = require './WebSurfaceView' PROFILE_ME = false module.exports = class PlayLevelView extends RootView id: 'level-view' template: template cache: false shortcutsEnabled: true isEditorPreview: false subscriptions: 'level:set-volume': 'onSetVolume' 'level:show-victory': 'onShowVictory' 'level:restart': 'onRestartLevel' 'level:highlight-dom': 'onHighlightDOM' 'level:end-highlight-dom': 'onEndHighlight' 'level:focus-dom': 'onFocusDom' 'level:disable-controls': 'onDisableControls' 'level:enable-controls': 'onEnableControls' 'god:world-load-progress-changed': 'onWorldLoadProgressChanged' 'god:new-world-created': 'onNewWorld' 'god:streaming-world-updated': 'onNewWorld' 'god:infinite-loop': 'onInfiniteLoop' 'level:reload-from-data': 'onLevelReloadFromData' 'level:reload-thang-type': 'onLevelReloadThangType' 'level:started': 'onLevelStarted' 'level:loading-view-unveiling': 'onLoadingViewUnveiling' 'level:loading-view-unveiled': 'onLoadingViewUnveiled' 'level:loaded': 'onLevelLoaded' 'level:session-loaded': 'onSessionLoaded' 'playback:real-time-playback-started': 'onRealTimePlaybackStarted' 'playback:real-time-playback-ended': 'onRealTimePlaybackEnded' 'ipad:memory-warning': 'onIPadMemoryWarning' 'store:item-purchased': 'onItemPurchased' events: 'click #level-done-button': 'onDonePressed' 'click #stop-real-time-playback-button': -> Backbone.Mediator.publish 'playback:stop-real-time-playback', {} 'click #fullscreen-editor-background-screen': (e) -> Backbone.Mediator.publish 'tome:toggle-maximize', {} 'click .contact-link': 'onContactClicked' shortcuts: 'ctrl+s': 'onCtrlS' 'esc': 'onEscapePressed' # Initial Setup ############################################################# constructor: (options, @levelID) -> console.profile?() if PROFILE_ME super options @courseID = options.courseID or @getQueryVariable 'course' @courseInstanceID = options.courseInstanceID or @getQueryVariable 'course-instance' @isEditorPreview = @getQueryVariable 'dev' @sessionID = @getQueryVariable 'session' @observing = @getQueryVariable 'observing' @opponentSessionID = @getQueryVariable('opponent') @opponentSessionID ?= @options.opponent @gameUIState = new GameUIState() $(window).on 'resize', @onWindowResize application.tracker?.enableInspectletJS(@levelID) if @isEditorPreview @supermodel.shouldSaveBackups = (model) -> # Make sure to load possibly changed things from localStorage. model.constructor.className in ['Level', 'LevelComponent', 'LevelSystem', 'ThangType'] f = => @load() unless @levelLoader # Wait to see if it's just given to us through setLevel. setTimeout f, 100 else @load() application.tracker?.trackEvent 'Started Level Load', category: 'Play Level', level: @levelID, label: @levelID unless @observing setLevel: (@level, givenSupermodel) -> @supermodel.models = givenSupermodel.models @supermodel.collections = givenSupermodel.collections @supermodel.shouldSaveBackups = givenSupermodel.shouldSaveBackups serializedLevel = @level.serialize {@supermodel, @session, @otherSession, headless: false, sessionless: false} @god?.setLevel serializedLevel if @world @world.loadFromLevel serializedLevel, false else @load() load: -> @loadStartTime = new Date() levelLoaderOptions = supermodel: @supermodel, levelID: @levelID, sessionID: @sessionID, opponentSessionID: @opponentSessionID, team: @getQueryVariable('team'), observing: @observing, courseID: @courseID if me.isSessionless() levelLoaderOptions.fakeSessionConfig = {} @levelLoader = new LevelLoader levelLoaderOptions @listenToOnce @levelLoader, 'world-necessities-loaded', @onWorldNecessitiesLoaded @listenTo @levelLoader, 'world-necessity-load-failed', @onWorldNecessityLoadFailed onLevelLoaded: (e) -> return if @destroyed unless e.level.isType('web-dev') @god = new God({ @gameUIState indefiniteLength: e.level.isType('game-dev') }) @setupGod() if @waitingToSetUpGod trackLevelLoadEnd: -> return if @isEditorPreview @loadEndTime = new Date() @loadDuration = @loadEndTime - @loadStartTime console.debug "Level unveiled after #{(@loadDuration / 1000).toFixed(2)}s" unless @observing application.tracker?.trackEvent 'Finished Level Load', category: 'Play Level', label: @levelID, level: @levelID, loadDuration: @loadDuration application.tracker?.trackTiming @loadDuration, 'Level Load Time', @levelID, @levelID isCourseMode: -> @courseID and @courseInstanceID showAds: -> return false # No ads for now. if application.isProduction() && !me.isPremium() && !me.isTeacher() && !window.serverConfig.picoCTF && !@isCourseMode() return me.getCampaignAdsGroup() is 'leaderboard-ads' false # CocoView overridden methods ############################################### getRenderData: -> c = super() c.world = @world c afterRender: -> super() window.onPlayLevelViewLoaded? @ # still a hack @insertSubView @loadingView = new LevelLoadingView autoUnveil: @options.autoUnveil or @observing, level: @levelLoader?.level ? @level, session: @levelLoader?.session ? @session # May not have @level loaded yet @$el.find('#level-done-button').hide() $('body').addClass('is-playing') $('body').bind('touchmove', false) if @isIPadApp() afterInsert: -> super() # Partially Loaded Setup #################################################### onWorldNecessitiesLoaded: -> # Called when we have enough to build the world, but not everything is loaded @grabLevelLoaderData() team = @getQueryVariable('team') ? @session.get('team') ? @world?.teamForPlayer(0) ? 'humans' @loadOpponentTeam(team) @setupGod() @setTeam team @initGoalManager() @insertSubviews() @initVolume() @register() @controlBar.setBus(@bus) @initScriptManager() onWorldNecessityLoadFailed: (resource) -> @loadingView.onLoadError(resource) grabLevelLoaderData: -> @session = @levelLoader.session @level = @levelLoader.level if @level.isType('web-dev') @$el.addClass 'web-dev' # Hide some of the elements we won't be using return @world = @levelLoader.world if @level.isType('game-dev') @$el.addClass 'game-dev' @howToPlayText = utils.i18n(@level.attributes, 'studentPlayInstructions') @howToPlayText ?= $.i18n.t('play_game_dev_level.default_student_instructions') @howToPlayText = marked(@howToPlayText, { sanitize: true }) @renderSelectors('#how-to-play-game-dev-panel') @$el.addClass 'hero' if @level.isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev') # TODO: figure out what this does and comment it @$el.addClass 'flags' if _.any(@world.thangs, (t) -> (t.programmableProperties and 'findFlags' in t.programmableProperties) or t.inventory?.flag) or @level.get('slug') is 'sky-span' # TODO: Update terminology to always be opponentSession or otherSession # TODO: E.g. if it's always opponent right now, then variable names should be opponentSession until we have coop play @otherSession = @levelLoader.opponentSession unless @level.isType('game-dev') @worldLoadFakeResources = [] # first element (0) is 1%, last (99) is 100% for percent in [1 .. 100] @worldLoadFakeResources.push @supermodel.addSomethingResource 1 @renderSelectors '#stop-real-time-playback-button' onWorldLoadProgressChanged: (e) -> return unless e.god is @god return unless @worldLoadFakeResources @lastWorldLoadPercent ?= 0 worldLoadPercent = Math.floor 100 * e.progress for percent in [@lastWorldLoadPercent + 1 .. worldLoadPercent] by 1 @worldLoadFakeResources[percent - 1].markLoaded() @lastWorldLoadPercent = worldLoadPercent @worldFakeLoadResources = null if worldLoadPercent is 100 # Done, don't need to watch progress any more. loadOpponentTeam: (myTeam) -> opponentSpells = [] for spellTeam, spells of @session.get('teamSpells') ? @otherSession?.get('teamSpells') ? {} continue if spellTeam is myTeam or not myTeam opponentSpells = opponentSpells.concat spells if (not @session.get('teamSpells')) and @otherSession?.get('teamSpells') @session.set('teamSpells', @otherSession.get('teamSpells')) opponentCode = @otherSession?.get('code') or {} myCode = @session.get('code') or {} for spell in opponentSpells [thang, spell] = spell.split '/' c = opponentCode[thang]?[spell] myCode[thang] ?= {} if c then myCode[thang][spell] = c else delete myCode[thang][spell] @session.set('code', myCode) setupGod: -> return if @level.isType('web-dev') return @waitingToSetUpGod = true unless @god @waitingToSetUpGod = undefined @god.setLevel @level.serialize {@supermodel, @session, @otherSession, headless: false, sessionless: false} @god.setLevelSessionIDs if @otherSession then [@session.id, @otherSession.id] else [@session.id] @god.setWorldClassMap @world.classMap setTeam: (team) -> team = team?.team unless _.isString team team ?= 'humans' me.team = team @session.set 'team', team Backbone.Mediator.publish 'level:team-set', team: team # Needed for scripts @team = team initGoalManager: -> @goalManager = new GoalManager(@world, @level.get('goals'), @team) @god?.setGoalManager @goalManager insertSubviews: -> @hintsState = new HintsState({ hidden: true }, { @session, @level }) @insertSubView @tome = new TomeView { @levelID, @session, @otherSession, thangs: @world?.thangs ? [], @supermodel, @level, @observing, @courseID, @courseInstanceID, @god, @hintsState } @insertSubView new LevelPlaybackView session: @session, level: @level unless @level.isType('web-dev') @insertSubView new GoalsView {level: @level} @insertSubView new LevelFlagsView levelID: @levelID, world: @world if @$el.hasClass 'flags' @insertSubView new GoldView {} unless @level.get('slug') in ['wakka-maul'] unless @level.isType('web-dev') @insertSubView new HUDView {level: @level} unless @level.isType('web-dev') @insertSubView new LevelDialogueView {level: @level, sessionID: @session.id} @insertSubView new ChatView levelID: @levelID, sessionID: @session.id, session: @session @insertSubView new ProblemAlertView session: @session, level: @level, supermodel: @supermodel @insertSubView new DuelStatsView level: @level, session: @session, otherSession: @otherSession, supermodel: @supermodel, thangs: @world.thangs if @level.isType('hero-ladder', 'course-ladder') @insertSubView @controlBar = new ControlBarView {worldName: utils.i18n(@level.attributes, 'name'), session: @session, level: @level, supermodel: @supermodel, courseID: @courseID, courseInstanceID: @courseInstanceID} @insertSubView @hintsView = new HintsView({ @session, @level, @hintsState }), @$('.hints-view') @insertSubView @webSurface = new WebSurfaceView {level: @level, @goalManager} if @level.isType('web-dev') #_.delay (=> Backbone.Mediator.publish('level:set-debug', debug: true)), 5000 if @isIPadApp() # if me.displayName() is 'Nick' initVolume: -> volume = me.get('volume') volume = 1.0 unless volume? Backbone.Mediator.publish 'level:set-volume', volume: volume initScriptManager: -> return if @level.isType('web-dev') @scriptManager = new ScriptManager({scripts: @world.scripts or [], view: @, session: @session, levelID: @level.get('slug')}) @scriptManager.loadFromSession() register: -> @bus = LevelBus.get(@levelID, @session.id) @bus.setSession(@session) @bus.setSpells @tome.spells #@bus.connect() if @session.get('multiplayer') # TODO: session's multiplayer flag removed; connect bus another way if we care about it # Load Completed Setup ###################################################### onSessionLoaded: (e) -> return if @session Backbone.Mediator.publish "ipad:language-chosen", language: e.session.get('codeLanguage') ? "python" # Just the level and session have been loaded by the level loader if e.level.get('slug') is 'zero-sum' sorcerer = '52fd1524c7e6cf99160e7bc9' if e.session.get('creator') is '532dbc73a622924444b68ed9' # Wizard Dude gets his own avatar sorcerer = '53e126a4e06b897606d38bef' e.session.set 'heroConfig', {"thangType":sorcerer,"inventory":{"misc-0":"53e2396a53457600003e3f0f","programming-book":"546e266e9df4a17d0d449be5","minion":"54eb5dbc49fa2d5c905ddf56","feet":"53e214f153457600003e3eab","right-hand":"54eab7f52b7506e891ca7202","left-hand":"5463758f3839c6e02811d30f","wrists":"54693797a2b1f53ce79443e9","gloves":"5469425ca2b1f53ce7944421","torso":"546d4a549df4a17d0d449a97","neck":"54693274a2b1f53ce79443c9","eyes":"546941fda2b1f53ce794441d","head":"546d4ca19df4a17d0d449abf"}} else if e.level.get('slug') in ['ace-of-coders', 'elemental-wars'] goliath = '55e1a6e876cb0948c96af9f8' e.session.set 'heroConfig', {"thangType":goliath,"inventory":{"eyes":"53eb99f41a100989a40ce46e","neck":"54693274a2b1f53ce79443c9","wrists":"54693797a2b1f53ce79443e9","feet":"546d4d8e9df4a17d0d449acd","minion":"54eb5bf649fa2d5c905ddf4a","programming-book":"557871261ff17fef5abee3ee"}} else if e.level.get('slug') is 'assembly-speed' raider = '55527eb0b8abf4ba1fe9a107' e.session.set 'heroConfig', {"thangType":raider,"inventory":{}} else if e.level.isType('hero', 'hero-ladder', 'hero-coop') and not _.size e.session.get('heroConfig')?.inventory ? {} @setupManager?.destroy() @setupManager = new LevelSetupManager({supermodel: @supermodel, level: e.level, levelID: @levelID, parent: @, session: e.session, courseID: @courseID, courseInstanceID: @courseInstanceID}) @setupManager.open() onLoaded: -> _.defer => @onLevelLoaderLoaded() onLevelLoaderLoaded: -> # Everything is now loaded return unless @levelLoader.progress() is 1 # double check, since closing the guide may trigger this early # Save latest level played. if not @observing and not (@levelLoader.level.isType('ladder', 'ladder-tutorial')) me.set('lastLevel', @levelID) me.save() application.tracker?.identify() @saveRecentMatch() if @otherSession @levelLoader.destroy() @levelLoader = null if @level.isType('web-dev') Backbone.Mediator.publish 'level:started', {} else @initSurface() saveRecentMatch: -> allRecentlyPlayedMatches = storage.load('recently-played-matches') ? {} recentlyPlayedMatches = allRecentlyPlayedMatches[@levelID] ? [] allRecentlyPlayedMatches[@levelID] = recentlyPlayedMatches recentlyPlayedMatches.unshift yourTeam: me.team, otherSessionID: @otherSession.id, opponentName: @otherSession.get('creatorName') unless _.find recentlyPlayedMatches, otherSessionID: @otherSession.id recentlyPlayedMatches.splice(8) storage.save 'recently-played-matches', allRecentlyPlayedMatches initSurface: -> webGLSurface = $('canvas#webgl-surface', @$el) normalSurface = $('canvas#normal-surface', @$el) surfaceOptions = { thangTypes: @supermodel.getModels(ThangType) @observing playerNames: @findPlayerNames() levelType: @level.get('type', true) stayVisible: @showAds() @gameUIState @level # TODO: change from levelType to level } @surface = new Surface(@world, normalSurface, webGLSurface, surfaceOptions) worldBounds = @world.getBounds() bounds = [{x: worldBounds.left, y: worldBounds.top}, {x: worldBounds.right, y: worldBounds.bottom}] @surface.camera.setBounds(bounds) @surface.camera.zoomTo({x: 0, y: 0}, 0.1, 0) findPlayerNames: -> return {} unless @level.isType('ladder', 'hero-ladder', 'course-ladder') playerNames = {} for session in [@session, @otherSession] when session?.get('team') playerNames[session.get('team')] = session.get('creatorName') or 'Anonymous' playerNames # Once Surface is Loaded #################################################### onLevelStarted: -> return unless @surface? or @webSurface? @loadingView.showReady() @trackLevelLoadEnd() if window.currentModal and not window.currentModal.destroyed and window.currentModal.constructor isnt VictoryModal return Backbone.Mediator.subscribeOnce 'modal:closed', @onLevelStarted, @ @surface?.showLevel() Backbone.Mediator.publish 'level:set-time', time: 0 if (@isEditorPreview or @observing) and not @getQueryVariable('intro') @loadingView.startUnveiling() @loadingView.unveil true else @scriptManager?.initializeCamera() onLoadingViewUnveiling: (e) -> @selectHero() onLoadingViewUnveiled: (e) -> if @level.isType('course-ladder', 'hero-ladder') or @observing # We used to autoplay by default, but now we only do it if the level says to in the introduction script. Backbone.Mediator.publish 'level:set-playing', playing: true @loadingView.$el.remove() @removeSubView @loadingView @loadingView = null @playAmbientSound() # TODO: Is it possible to create a Mongoose ObjectId for 'ls', instead of the string returned from get()? application.tracker?.trackEvent 'Started Level', category:'Play Level', levelID: @levelID, ls: @session?.get('_id') unless @observing $(window).trigger 'resize' _.delay (=> @perhapsStartSimulating?()), 10 * 1000 onSetVolume: (e) -> createjs.Sound.setVolume(if e.volume is 1 then 0.6 else e.volume) # Quieter for now until individual sound FX controls work again. if e.volume and not @ambientSound @playAmbientSound() playAmbientSound: -> return if @destroyed return if @ambientSound return unless me.get 'volume' return unless file = {Dungeon: 'ambient-dungeon', Grass: 'ambient-grass'}[@level.get('terrain')] src = "/file/interface/#{file}#{AudioPlayer.ext}" unless AudioPlayer.getStatus(src)?.loaded AudioPlayer.preloadSound src Backbone.Mediator.subscribeOnce 'audio-player:loaded', @playAmbientSound, @ return @ambientSound = createjs.Sound.play src, loop: -1, volume: 0.1 createjs.Tween.get(@ambientSound).to({volume: 1.0}, 10000) selectHero: -> Backbone.Mediator.publish 'level:suppress-selection-sounds', suppress: true Backbone.Mediator.publish 'tome:select-primary-sprite', {} Backbone.Mediator.publish 'level:suppress-selection-sounds', suppress: false @surface?.focusOnHero() perhapsStartSimulating: -> return unless @shouldSimulate() return console.error "Should not auto-simulate until we fix how these languages are loaded" # TODO: how can we not require these as part of /play bundle? ##require "vendor/aether-#{codeLanguage}" for codeLanguage in ['javascript', 'python', 'coffeescript', 'lua', 'java'] #require 'vendor/aether-javascript' #require 'vendor/aether-python' #require 'vendor/aether-coffeescript' #require 'vendor/aether-lua' #require 'vendor/aether-java' @simulateNextGame() simulateNextGame: -> return @simulator.fetchAndSimulateOneGame() if @simulator simulatorOptions = background: true, leagueID: @courseInstanceID simulatorOptions.levelID = @level.get('slug') if @level.isType('course-ladder', 'hero-ladder') @simulator = new Simulator simulatorOptions # Crude method of mitigating Simulator memory leak issues fetchAndSimulateOneGameOriginal = @simulator.fetchAndSimulateOneGame @simulator.fetchAndSimulateOneGame = => if @simulator.simulatedByYou >= 10 console.log '------------------- Destroying Simulator and making a new one -----------------' @simulator.destroy() @simulator = null @simulateNextGame() else fetchAndSimulateOneGameOriginal.apply @simulator @simulator.fetchAndSimulateOneGame() shouldSimulate: -> return true if @getQueryVariable('simulate') is true return false if @getQueryVariable('simulate') is false stillBuggy = true # Keep this true while we still haven't fixed the zombie worker problem when simulating the more difficult levels on Chrome defaultCores = 2 cores = window.navigator.hardwareConcurrency or defaultCores # Available on Chrome/Opera, soon Safari defaultHeapLimit = 793000000 heapLimit = window.performance?.memory?.jsHeapSizeLimit or defaultHeapLimit # Only available on Chrome, basically just says 32- vs. 64-bit gamesSimulated = me.get('simulatedBy') console.debug "Should we start simulating? Cores:", window.navigator.hardwareConcurrency, "Heap limit:", window.performance?.memory?.jsHeapSizeLimit, "Load duration:", @loadDuration return false unless $.browser?.desktop return false if $.browser?.msie or $.browser?.msedge return false if $.browser.linux return false if me.level() < 8 if @level.isType('course', 'game-dev', 'web-dev') return false else if @level.isType('hero') and gamesSimulated return false if stillBuggy return false if cores < 8 return false if heapLimit < defaultHeapLimit return false if @loadDuration > 10000 else if @level.isType('hero-ladder') and gamesSimulated return false if stillBuggy return false if cores < 4 return false if heapLimit < defaultHeapLimit return false if @loadDuration > 15000 else if @level.isType('hero-ladder') and not gamesSimulated return false if stillBuggy return false if cores < 8 return false if heapLimit <= defaultHeapLimit return false if @loadDuration > 20000 else if @level.isType('course-ladder') return false if cores <= defaultCores return false if heapLimit < defaultHeapLimit return false if @loadDuration > 18000 else console.warn "Unwritten level type simulation heuristics; fill these in for new level type #{@level.get('type')}?" return false if stillBuggy return false if cores < 8 return false if heapLimit < defaultHeapLimit return false if @loadDuration > 10000 console.debug "We should have the power. Begin background ladder simulation." true # callbacks onCtrlS: (e) -> e.preventDefault() onEscapePressed: (e) -> return unless @$el.hasClass 'real-time' Backbone.Mediator.publish 'playback:stop-real-time-playback', {} onLevelReloadFromData: (e) -> isReload = Boolean @world @setLevel e.level, e.supermodel if isReload @scriptManager.setScripts(e.level.get('scripts')) Backbone.Mediator.publish 'tome:cast-spell', {} # a bit hacky onLevelReloadThangType: (e) -> tt = e.thangType for url, model of @supermodel.models if model.id is tt.id for key, val of tt.attributes model.attributes[key] = val break Backbone.Mediator.publish 'tome:cast-spell', {} onWindowResize: (e) => @endHighlight() onDisableControls: (e) -> return if e.controls and not ('level' in e.controls) @shortcutsEnabled = false @wasFocusedOn = document.activeElement $('body').focus() onEnableControls: (e) -> return if e.controls? and not ('level' in e.controls) @shortcutsEnabled = true $(@wasFocusedOn).focus() if @wasFocusedOn @wasFocusedOn = null onDonePressed: -> @showVictory() onShowVictory: (e={}) -> $('#level-done-button').show() unless @level.isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev', 'web-dev') @showVictory(_.pick(e, 'manual')) if e.showModal return if @victorySeen @victorySeen = true victoryTime = (new Date()) - @loadEndTime if not @observing and victoryTime > 10 * 1000 # Don't track it if we're reloading an already-beaten level application.tracker?.trackEvent 'Saw Victory', category: 'Play Level' level: @level.get('name') label: @level.get('name') levelID: @levelID ls: @session?.get('_id') application.tracker?.trackTiming victoryTime, 'Level Victory Time', @levelID, @levelID showVictory: (options={}) -> return if @level.hasLocalChanges() # Don't award achievements when beating level changed in level editor return if @level.isType('game-dev') and @level.get('shareable') and not options.manual @endHighlight() options = {level: @level, supermodel: @supermodel, session: @session, hasReceivedMemoryWarning: @hasReceivedMemoryWarning, courseID: @courseID, courseInstanceID: @courseInstanceID, world: @world} ModalClass = if @level.isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev', 'web-dev') then HeroVictoryModal else VictoryModal ModalClass = CourseVictoryModal if @isCourseMode() or me.isSessionless() if @level.isType('course-ladder') ModalClass = CourseVictoryModal options.courseInstanceID = @getQueryVariable 'course-instance' or @getQueryVariable 'league' ModalClass = PicoCTFVictoryModal if window.serverConfig.picoCTF victoryModal = new ModalClass(options) @openModalView(victoryModal) if me.get('anonymous') window.nextURL = '/play/' + (@level.get('campaign') ? '') # Signup will go here on completion instead of reloading. onRestartLevel: -> @tome.reloadAllCode() Backbone.Mediator.publish 'level:restarted', {} $('#level-done-button', @$el).hide() application.tracker?.trackEvent 'Confirmed Restart', category: 'Play Level', level: @level.get('name'), label: @level.get('name') unless @observing onInfiniteLoop: (e) -> return unless e.firstWorld and e.god is @god @openModalView new InfiniteLoopModal nonUserCodeProblem: e.nonUserCodeProblem application.tracker?.trackEvent 'Saw Initial Infinite Loop', category: 'Play Level', level: @level.get('name'), label: @level.get('name') unless @observing onHighlightDOM: (e) -> @highlightElement e.selector, delay: e.delay, sides: e.sides, offset: e.offset, rotation: e.rotation onEndHighlight: -> @endHighlight() onFocusDom: (e) -> $(e.selector).focus() onContactClicked: (e) -> Backbone.Mediator.publish 'level:contact-button-pressed', {} @openModalView contactModal = new ContactModal levelID: @level.get('slug') or @level.id, courseID: @courseID, courseInstanceID: @courseInstanceID screenshot = @surface.screenshot(1, 'image/png', 1.0, 1) body = b64png: screenshot.replace 'data:image/png;base64,', '' filename: "screenshot-#{@levelID}-#{_.string.slugify((new Date()).toString())}.png" path: "db/user/#{me.id}" mimetype: 'image/png' contactModal.screenshotURL = "http://codecombat.com/file/#{body.path}/#{body.filename}" window.screenshot = screenshot window.screenshotURL = contactModal.screenshotURL $.ajax '/file', type: 'POST', data: body, success: (e) -> contactModal.updateScreenshot?() # Dynamic sound loading onNewWorld: (e) -> return if @headless scripts = @world.scripts # Since these worlds don't have scripts, preserve them. @world = e.world @world.scripts = scripts thangTypes = @supermodel.getModels(ThangType) startFrame = @lastWorldFramesLoaded ? 0 finishedLoading = @world.frames.length is @world.totalFrames if finishedLoading @lastWorldFramesLoaded = 0 if @waitingForSubmissionComplete _.defer @onSubmissionComplete # Give it a frame to make sure we have the latest goals @waitingForSubmissionComplete = false else @lastWorldFramesLoaded = @world.frames.length for [spriteName, message] in @world.thangDialogueSounds startFrame continue unless thangType = _.find thangTypes, (m) -> m.get('name') is spriteName continue unless sound = AudioPlayer.soundForDialogue message, thangType.get('soundTriggers') AudioPlayer.preloadSoundReference sound # Real-time playback onRealTimePlaybackStarted: (e) -> @$el.addClass('real-time').focus() if @level.isType('game-dev') panel = @$('#how-to-play-game-dev-panel') panel.removeClass('hide') # TODO: Remove this once these levels have studentPlayInstructions set. if not @level.get('studentPlayInstructions') lines = switch @level.get('slug') when 'over-the-garden-wall' then ['Watch to see if the peasants are properly protected.'] when 'click-gait' then ['Move to each red "X".', 'Click on the screen to move the Knight there.'] when 'heros-journey' then ['Move to each red "X".', 'Click on the screen to move the Knight there.'] when 'a-maze-ing' then ['Move to the chest of gems.', 'Click on the screen to move the Duelist there.'] when 'gemtacular' then ['Move to each of the gems.', 'Click on the screen to move the Captain there.'] when 'vorpal-mouse' then ['Slay the ogres.', 'Click on the screen to move the Guardian there.', 'Click on the munchkins to attack them!'] when 'crushing-it' then ['Slay the ogres.', 'Click on the screen to move the Goliath there.', 'Click on the munchkins to attack them!'] when 'tabula-rasa' then ['Slay any ogres.', 'Collect any coins.', 'Click on the screen to move the Raider there.', 'Click on any munchkins to attack them!'] else null if lines html = _.map(lines, (line) -> "
#{line}
").join('') panel.find('.panel-body').html(html) @onWindowResize() onRealTimePlaybackEnded: (e) -> return unless @$el.hasClass 'real-time' @$('#how-to-play-game-dev-panel').addClass('hide') if @level.isType('game-dev') @$el.removeClass 'real-time' @onWindowResize() if @world.frames.length is @world.totalFrames and not @surface.countdownScreen?.showing _.delay @onSubmissionComplete, 750 # Wait for transition to end. else @waitingForSubmissionComplete = true onSubmissionComplete: => return if @destroyed Backbone.Mediator.publish 'level:set-time', ratio: 1 return if @level.hasLocalChanges() # Don't award achievements when beating level changed in level editor if @goalManager.checkOverallStatus() is 'success' showModalFn = -> Backbone.Mediator.publish 'level:show-victory', showModal: true @session.recordScores @world.scores, @level if @level.get 'replayable' @session.increaseDifficulty showModalFn else showModalFn() destroy: -> @levelLoader?.destroy() @surface?.destroy() @god?.destroy() @goalManager?.destroy() @scriptManager?.destroy() @setupManager?.destroy() @simulator?.destroy() if ambientSound = @ambientSound # Doesn't seem to work; stops immediately. createjs.Tween.get(ambientSound).to({volume: 0.0}, 1500).call -> ambientSound.stop() $(window).off 'resize', @onWindowResize delete window.world # not sure where this is set, but this is one way to clean it up @bus?.destroy() #@instance.save() unless @instance.loading delete window.nextURL console.profileEnd?() if PROFILE_ME application.tracker?.disableInspectletJS() super() onIPadMemoryWarning: (e) -> @hasReceivedMemoryWarning = true onItemPurchased: (e) -> heroConfig = @session.get('heroConfig') ? {} inventory = heroConfig.inventory ? {} slot = e.item.getAllowedSlots()[0] if slot and not inventory[slot] # Open up the inventory modal so they can equip the new item @setupManager?.destroy() @setupManager = new LevelSetupManager({supermodel: @supermodel, level: @level, levelID: @levelID, parent: @, session: @session, hadEverChosenHero: true}) @setupManager.open()