RootView = require 'views/kinds/RootView' template = require 'templates/play/level' {me} = require 'lib/auth' ThangType = require 'models/ThangType' utils = require 'lib/utils' storage = require 'lib/storage' # 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' RealTimeModel = require 'models/RealTimeModel' RealTimeCollection = require 'collections/RealTimeCollection' # subviews LevelLoadingView = require './LevelLoadingView' TomeView = require './tome/TomeView' ChatView = require './LevelChatView' HUDView = require './LevelHUDView' ControlBarView = require './ControlBarView' LevelPlaybackView = require './LevelPlaybackView' GoalsView = require './LevelGoalsView' LevelFlagsView = require './LevelFlagsView' GoldView = require './LevelGoldView' VictoryModal = require './modal/VictoryModal' InfiniteLoopModal = require './modal/InfiniteLoopModal' GameMenuModal = require 'views/game-menu/GameMenuModal' PROFILE_ME = false module.exports = class PlayLevelView extends RootView id: 'level-view' template: template cache: false shortcutsEnabled: true isEditorPreview: false subscriptions: 'level:set-volume': (e) -> createjs.Sound.setVolume(e.volume) '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:play-next-level': 'onPlayNextLevel' 'level:edit-wizard-settings': 'showWizardSettingsModal' 'level:session-will-save': 'onSessionWillSave' 'level:started': 'onLevelStarted' 'level:loading-view-unveiling': 'onLoadingViewUnveiling' 'level:loading-view-unveiled': 'onLoadingViewUnveiled' 'level:loaded': 'onLevelLoaded' 'level:session-loaded': 'onSessionLoaded' 'playback:real-time-playback-waiting': 'onRealTimePlaybackWaiting' 'playback:real-time-playback-started': 'onRealTimePlaybackStarted' 'playback:real-time-playback-ended': 'onRealTimePlaybackEnded' 'real-time-multiplayer:joined-game': 'onJoinedRealTimeMultiplayerGame' 'real-time-multiplayer:left-game': 'onLeftRealTimeMultiplayerGame' 'real-time-multiplayer:manual-cast': 'onRealTimeMultiplayerCast' 'level:hero-config-changed': 'onHeroConfigChanged' 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', {} shortcuts: 'ctrl+s': 'onCtrlS' # Initial Setup ############################################################# constructor: (options, @levelID) -> console.profile?() if PROFILE_ME super options if not me.get('hourOfCode') and @getQueryVariable 'hour_of_code' @setUpHourOfCode() @isEditorPreview = @getQueryVariable 'dev' @sessionID = @getQueryVariable 'session' $(window).on 'resize', @onWindowResize @saveScreenshot = _.throttle @saveScreenshot, 30000 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', level: @levelID, label: @levelID, ['Google Analytics'] setUpHourOfCode: -> me.set 'hourOfCode', true me.patch() $('body').append($('')) application.tracker?.trackEvent 'Hour of Code Begin', {} setLevel: (@level, givenSupermodel) -> @supermodel.models = givenSupermodel.models @supermodel.collections = givenSupermodel.collections @supermodel.shouldSaveBackups = givenSupermodel.shouldSaveBackups serializedLevel = @level.serialize @supermodel, @session @god?.setLevel serializedLevel if @world @world.loadFromLevel serializedLevel, false else @load() load: -> @loadStartTime = new Date() @god = new God debugWorker: true @levelLoader = new LevelLoader supermodel: @supermodel, levelID: @levelID, sessionID: @sessionID, opponentSessionID: @getQueryVariable('opponent'), team: @getQueryVariable('team') @listenToOnce @levelLoader, 'world-necessities-loaded', @onWorldNecessitiesLoaded # CocoView overridden methods ############################################### updateProgress: (progress) -> super(progress) return if @seenDocs return if @isIPadApp() return unless @levelLoader.session.loaded and @levelLoader.level.loaded return unless showFrequency = @levelLoader.level.get('showsGuide') session = @levelLoader.session diff = new Date().getTime() - new Date(session.get('created')).getTime() return if showFrequency is 'first-time' and diff > (5 * 60 * 1000) articles = @levelLoader.supermodel.getModels Article for article in articles return unless article.loaded @showGuide() showGuide: -> @seenDocs = true LevelGuideModal = require './modal/LevelGuideModal' options = docs: @levelLoader.level.get('documentation') supermodel: @supermodel firstOnly: true @openModalView(new LevelGuideModal(options), true) onGuideOpened = (e) -> @guideOpenTime = new Date() onGuideClosed = (e) -> application.tracker?.trackTiming new Date() - @guideOpenTime, 'Intro Guide Time', @levelID, @levelID, 100 @onLevelStarted() Backbone.Mediator.subscribeOnce 'modal:opened', onGuideOpened, @ Backbone.Mediator.subscribeOnce 'modal:closed', onGuideClosed, @ return true getRenderData: -> c = super() c.world = @world if me.get('hourOfCode') and me.get('preferredLanguage', true) is 'en-US' # Show the Hour of Code footer explanation until it's been more than a day elapsed = (new Date() - new Date(me.get('dateCreated'))) c.explainHourOfCode = elapsed < 86400 * 1000 c afterRender: -> super() window.onPlayLevelViewLoaded? @ # still a hack @insertSubView @loadingView = new LevelLoadingView autoUnveil: @options.autoUnveil, level: @level # 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') ? @world.teamForPlayer(0) @loadOpponentTeam(team) @setupGod() @setTeam team @initGoalManager() @insertSubviews() @initVolume() @listenTo(@session, 'change:multiplayer', @onMultiplayerChanged) @originalSessionState = $.extend(true, {}, @session.get('state')) @register() @controlBar.setBus(@bus) @initScriptManager() grabLevelLoaderData: -> @session = @levelLoader.session @world = @levelLoader.world @level = @levelLoader.level @otherSession = @levelLoader.opponentSession @worldLoadFakeResources = [] # first element (0) is 1%, last (100) is 100% for percent in [1 .. 100] @worldLoadFakeResources.push @supermodel.addSomethingResource "world_simulation_#{percent}%", 1 onWorldLoadProgressChanged: (e) -> 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('transpiledCode') 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) if @session.get('multiplayer') and @otherSession? # For now, ladderGame will disallow multiplayer, because session code combining doesn't play nice yet. @session.set 'multiplayer', false setupGod: -> @god.setLevel @level.serialize @supermodel, @session @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 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: -> @insertSubView @tome = new TomeView levelID: @levelID, session: @session, otherSession: @otherSession, thangs: @world.thangs, supermodel: @supermodel, level: @level @insertSubView new LevelPlaybackView session: @session @insertSubView new GoalsView {} @insertSubView new LevelFlagsView world: @world @insertSubView new GoldView {} @insertSubView new HUDView {} @insertSubView new ChatView levelID: @levelID, sessionID: @session.id, session: @session worldName = utils.i18n @level.attributes, 'name' @controlBar = @insertSubView new ControlBarView {worldName: worldName, session: @session, level: @level, supermodel: @supermodel} Backbone.Mediator.publish('level:set-debug', debug: true) 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: -> @scriptManager = new ScriptManager({scripts: @world.scripts or [], view: @, session: @session}) @scriptManager.loadFromSession() register: -> @bus = LevelBus.get(@levelID, @session.id) @bus.setSession(@session) @bus.setSpells @tome.spells @bus.connect() if @session.get('multiplayer') # Load Completed Setup ###################################################### onLevelLoaded: (e) -> # Just the level has been loaded by the level loader @showWizardSettingsModal() if not me.get('name') and not @isIPadApp() and e.level.get('type', true) isnt 'hero' onSessionLoaded: (e) -> # Just the level and session have been loaded by the level loader if e.level.get('type', true) is 'hero' and not _.size e.session.get('heroConfig')?.inventory ? {} @openModalView new GameMenuModal level: e.level, session: e.session 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 (@levelLoader.level.get('type') in ['ladder', 'ladder-tutorial']) me.set('lastLevel', @levelID) me.save() @saveRecentMatch() if @otherSession @levelLoader.destroy() @levelLoader = null @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: -> surfaceCanvas = $('canvas#surface', @$el) @surface = new Surface(@world, surfaceCanvas, thangTypes: @supermodel.getModels(ThangType), playJingle: not @isEditorPreview, wizards: @level.get('type', true) isnt 'hero') 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) # Once Surface is Loaded #################################################### onLevelStarted: -> return unless @surface? @loadingView.showReady() if window.currentModal and not window.currentModal.destroyed and window.currentModal.constructor isnt VictoryModal return Backbone.Mediator.subscribeOnce 'modal:closed', @onLevelStarted, @ @surface.showLevel() if @otherSession and @level.get('type', true) isnt 'hero' # TODO: colorize name and cloud by team, colorize wizard by user's color config @surface.createOpponentWizard id: @otherSession.get('creator'), name: @otherSession.get('creatorName'), team: @otherSession.get('team'), levelSlug: @level.get('slug'), codeLanguage: @otherSession.get('submittedCodeLanguage') if @isEditorPreview @loadingView.startUnveiling() @loadingView.unveil() onLoadingViewUnveiling: (e) -> @restoreSessionState() onLoadingViewUnveiled: (e) -> @loadingView.$el.remove() @removeSubView @loadingView @loadingView = null unless @isEditorPreview @loadEndTime = new Date() loadDuration = @loadEndTime - @loadStartTime console.debug "Level unveiled after #{(loadDuration / 1000).toFixed(2)}s" application.tracker?.trackEvent 'Finished Level Load', level: @levelID, label: @levelID, loadDuration: loadDuration, ['Google Analytics'] application.tracker?.trackTiming loadDuration, 'Level Load Time', @levelID, @levelID @playAmbientSound() playAmbientSound: -> return if @ambientSound 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) restoreSessionState: -> return if @alreadyLoadedState @alreadyLoadedState = true state = @originalSessionState if @level.get('type', true) is 'hero' Backbone.Mediator.publish 'tome:select-primary-sprite', {} @surface.focusOnHero() Backbone.Mediator.publish 'level:set-time', time: 0 Backbone.Mediator.publish 'level:set-playing', playing: true else if state.frame and @level.get('type', true) isnt 'ladder' # https://github.com/codecombat/codecombat/issues/714 Backbone.Mediator.publish 'level:set-time', time: 0, frameOffset: state.frame if state.selected # TODO: Should also restore selected spell here by saving spellName Backbone.Mediator.publish 'level:select-sprite', thangID: state.selected, spellName: null if state.playing? Backbone.Mediator.publish 'level:set-playing', playing: state.playing # callbacks onCtrlS: (e) -> e.preventDefault() 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', {} onHeroConfigChanged: (e) -> # Doesn't work because the new inventory ThangTypes may not be loaded. #@setLevel @level, @supermodel #Backbone.Mediator.publish 'tome:cast-spell', {} # We'll just make a new PlayLevelView instead console.log 'Hero config changed; reload the level.' Backbone.Mediator.publish 'router:navigate', { route: window.location.pathname, viewClass: PlayLevelView, viewArgs: [{supermodel: @supermodel, autoUnveil: true}, @levelID] } onWindowResize: (s...) -> $('#pointer').css('opacity', 0.0) clearInterval(@pointerInterval) 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.get('type', true) is 'hero' @showVictory() if e.showModal setTimeout(@preloadNextLevel, 3000) return if @victorySeen @victorySeen = true victoryTime = (new Date()) - @loadEndTime if victoryTime > 10 * 1000 # Don't track it if we're reloading an already-beaten level application.tracker?.trackEvent 'Saw Victory', level: @level.get('name'), label: @level.get('name') application.tracker?.trackTiming victoryTime, 'Level Victory Time', @levelID, @levelID, 100 showVictory: -> options = {level: @level, supermodel: @supermodel, session: @session} docs = new VictoryModal(options) @openModalView(docs) if me.get('anonymous') window.nextLevelURL = @getNextLevelURL() # 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', level: @level.get('name'), label: @level.get('name') onInfiniteLoop: (e) -> return unless e.firstWorld @openModalView new InfiniteLoopModal() application.tracker?.trackEvent 'Saw Initial Infinite Loop', level: @level.get('name'), label: @level.get('name') onPlayNextLevel: -> nextLevelID = @getNextLevelID() nextLevelURL = @getNextLevelURL() Backbone.Mediator.publish 'router:navigate', { route: nextLevelURL, viewClass: PlayLevelView, viewArgs: [{supermodel: @supermodel}, nextLevelID]} getNextLevel: -> return null unless nextLevelOriginal = @level.get('nextLevel')?.original levels = @supermodel.getModels(Level) return l for l in levels when l.get('original') is nextLevelOriginal getNextLevelID: -> return null unless nextLevel = @getNextLevel() nextLevelID = nextLevel.get('slug') or nextLevel.id getNextLevelURL: -> return null unless @getNextLevelID() "/play/level/#{@getNextLevelID()}" onHighlightDom: (e) -> if e.delay delay = e.delay delete e.delay @pointerInterval = _.delay((=> @onHighlightDom e), delay) return @addPointer() selector = e.selector + ':visible' dom = $(selector) return if parseFloat(dom.css('opacity')) is 0.0 offset = dom.offset() return if not offset target_left = offset.left + dom.outerWidth() * 0.5 target_top = offset.top + dom.outerHeight() * 0.5 if e.sides if 'left' in e.sides then target_left = offset.left if 'right' in e.sides then target_left = offset.left + dom.outerWidth() if 'top' in e.sides then target_top = offset.top if 'bottom' in e.sides then target_top = offset.top + dom.outerHeight() else # aim to hit the side if the target is entirely on one side of the screen if offset.left > @$el.outerWidth()*0.5 target_left = offset.left else if offset.left + dom.outerWidth() < @$el.outerWidth()*0.5 target_left = offset.left + dom.outerWidth() # aim to hit the bottom or top if the target is entirely on the top or bottom of the screen if offset.top > @$el.outerWidth()*0.5 target_top = offset.top else if offset.top + dom.outerHeight() < @$el.outerHeight()*0.5 target_top = offset.top + dom.outerHeight() if e.offset target_left += e.offset.x target_top += e.offset.y @pointerRadialDistance = -47 # - Math.sqrt(Math.pow(dom.outerHeight()*0.5, 2), Math.pow(dom.outerWidth()*0.5)) @pointerRotation = e.rotation ? Math.atan2(@$el.outerWidth()*0.5 - target_left, target_top - @$el.outerHeight()*0.5) pointer = $('#pointer') pointer .css('opacity', 1.0) .css('transition', 'none') .css('transform', "rotate(#{@pointerRotation}rad) translate(-3px, #{@pointerRadialDistance}px)") .css('top', target_top - 50) .css('left', target_left - 50) setTimeout(()=> return if @destroyed @animatePointer() clearInterval(@pointerInterval) @pointerInterval = setInterval(@animatePointer, 1200) , 1) animatePointer: => pointer = $('#pointer') pointer.css('transition', 'all 0.6s ease-out') pointer.css('transform', "rotate(#{@pointerRotation}rad) translate(-3px, #{@pointerRadialDistance-50}px)") #Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'dom_highlight', volume: 0.75 # Never mind, this is currently so annoying setTimeout((=> pointer.css('transform', "rotate(#{@pointerRotation}rad) translate(-3px, #{@pointerRadialDistance}px)").css('transition', 'all 0.4s ease-in')), 800) onFocusDom: (e) -> $(e.selector).focus() onEndHighlight: -> $('#pointer').css('opacity', 0.0) clearInterval(@pointerInterval) onMultiplayerChanged: (e) -> if @session.get('multiplayer') @bus.connect() else @bus.removeFirebaseData => @bus.disconnect() addPointer: -> p = $('#pointer') return if p.length @$el.append($('')) preloadNextLevel: => # TODO: Loading models in the middle of gameplay causes stuttering. Most of the improvement in loading time is simply from passing the supermodel from this level to the next, but if we can find a way to populate the level early without it being noticeable, that would be even better. # return if @destroyed # return if @preloaded # nextLevel = @getNextLevel() # @supermodel.populateModel nextLevel # @preloaded = true onSessionWillSave: (e) -> # Something interesting has happened, so (at a lower frequency), we'll save a screenshot. #@saveScreenshot e.session # Throttled saveScreenshot: (session) => return unless screenshot = @surface?.screenshot() session.save {screenshot: screenshot}, {patch: true} # 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 @onSubmissionComplete() @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 onRealTimePlaybackWaiting: (e) -> @$el.addClass('real-time').focus() @onWindowResize() onRealTimePlaybackStarted: (e) -> @$el.addClass('real-time').focus() @onWindowResize() onRealTimePlaybackEnded: (e) -> return unless @$el.hasClass 'real-time' @$el.removeClass 'real-time' @onWindowResize() if @world.frames.length is @world.totalFrames _.delay @onSubmissionComplete, 750 # Wait for transition to end. else @waitingForSubmissionComplete = true @onRealTimeMultiplayerPlaybackEnded() onSubmissionComplete: => return if @destroyed Backbone.Mediator.publish 'level:show-victory', showModal: true if @goalManager.checkOverallStatus() is 'success' destroy: -> @levelLoader?.destroy() @surface?.destroy() @god?.destroy() @goalManager?.destroy() @scriptManager?.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 clearInterval(@pointerInterval) @bus?.destroy() #@instance.save() unless @instance.loading delete window.nextLevelURL console.profileEnd?() if PROFILE_ME super() # Real-time Multiplayer ###################################################### onRealTimeMultiplayerPlaybackEnded: -> if @multiplayerSession @multiplayerSession.set 'state', 'coding' players = new RealTimeCollection('multiplayer_level_sessions/' + @multiplayerSession.id + '/players') players.each (player) -> player.set 'state', 'coding' if player.id is me.id onJoinedRealTimeMultiplayerGame: (e) -> @multiplayerSession = new RealTimeModel('multiplayer_level_sessions/' + e.session.id) onLeftRealTimeMultiplayerGame: (e) -> if @multiplayerSession @multiplayerSession.off() @multiplayerSession = null onRealTimeMultiplayerCast: (e) -> unless @multiplayerSession console.error 'onRealTimeMultiplayerCast without a multiplayerSession' return players = new RealTimeCollection('multiplayer_level_sessions/' + @multiplayerSession.id + '/players') myPlayer = opponentPlayer = null players.each (player) -> if player.id is me.id myPlayer = player else opponentPlayer = player if myPlayer console.info 'Submitting my code' myPlayer.set 'code', @session.get('code') myPlayer.set 'codeLanguage', @session.get('codeLanguage') myPlayer.set 'state', 'submitted' myPlayer.set 'team', me.team else console.error 'Did not find my player in onRealTimeMultiplayerCast' if opponentPlayer # TODO: Shouldn't need nested opponentPlayer change listeners here state = opponentPlayer.get('state') console.info 'Other player is', state if state in ['submitted', 'ready'] @onOpponentSubmitted(opponentPlayer, myPlayer) else # Wait for opponent to submit their code opponentPlayer.on 'change', (e) => state = opponentPlayer.get('state') if state in ['submitted', 'ready'] @onOpponentSubmitted(opponentPlayer, myPlayer) onOpponentSubmitted: (opponentPlayer, myPlayer) => # Save opponent's code Backbone.Mediator.publish 'real-time-multiplayer:new-opponent-code', {codeLanguage: opponentPlayer.get('codeLanguage'), code: opponentPlayer.get('code'), team: opponentPlayer.get('team')} # I'm ready to rumble myPlayer.set 'state', 'ready' if opponentPlayer.get('state') is 'ready' console.info 'All real-time multiplayer players are ready!' @multiplayerSession.set 'state', 'running' else # Wait for opponent to be ready opponentPlayer.on 'change', (e) => if opponentPlayer.get('state') is 'ready' opponentPlayer.off() console.info 'All real-time multiplayer players are ready!' @multiplayerSession.set 'state', 'running'