codecombat/app/views/play/level/PlayLevelView.coffee

771 lines
34 KiB
CoffeeScript
Raw Normal View History

RootView = require 'views/kinds/RootView'
2014-01-03 13:32:13 -05:00
template = require 'templates/play/level'
2014-06-30 22:16:26 -04:00
{me} = require 'lib/auth'
2014-01-03 13:32:13 -05:00
ThangType = require 'models/ThangType'
utils = require 'lib/utils'
2014-08-30 17:30:53 -04:00
storage = require 'lib/storage'
2014-01-03 13:32:13 -05:00
# tools
Surface = require 'lib/surface/Surface'
God = require 'lib/God'
GoalManager = require 'lib/world/GoalManager'
ScriptManager = require 'lib/scripts/ScriptManager'
LevelBus = require 'lib/LevelBus'
2014-01-03 13:32:13 -05:00
LevelLoader = require 'lib/LevelLoader'
LevelSession = require 'models/LevelSession'
Level = require 'models/Level'
LevelComponent = require 'models/LevelComponent'
Article = require 'models/Article'
2014-01-03 13:32:13 -05:00
Camera = require 'lib/surface/Camera'
AudioPlayer = require 'lib/AudioPlayer'
2014-08-30 00:46:26 -04:00
RealTimeModel = require 'models/RealTimeModel'
RealTimeCollection = require 'collections/RealTimeCollection'
2014-01-03 13:32:13 -05:00
# subviews
LevelLoadingView = require './LevelLoadingView'
ProblemAlertView = require './tome/ProblemAlertView'
TomeView = require './tome/TomeView'
ChatView = require './LevelChatView'
HUDView = require './LevelHUDView'
ControlBarView = require './ControlBarView'
LevelPlaybackView = require './LevelPlaybackView'
GoalsView = require './LevelGoalsView'
2014-08-23 22:00:35 -04:00
LevelFlagsView = require './LevelFlagsView'
GoldView = require './LevelGoldView'
VictoryModal = require './modal/VictoryModal'
HeroVictoryModal = require './modal/HeroVictoryModal'
InfiniteLoopModal = require './modal/InfiniteLoopModal'
MultiplayerStatusView = require './MultiplayerStatusView'
LevelSetupManager = require 'lib/LevelSetupManager'
2014-01-03 13:32:13 -05:00
PROFILE_ME = false
module.exports = class PlayLevelView extends RootView
2014-01-03 13:32:13 -05:00
id: 'level-view'
template: template
cache: false
shortcutsEnabled: true
isEditorPreview: false
subscriptions:
'level:set-volume': (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.
'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'
2014-08-30 12:19:41 -04:00
'god:world-load-progress-changed': 'onWorldLoadProgressChanged'
'god:new-world-created': 'onNewWorld'
2014-08-21 19:27:52 -04:00
'god:streaming-world-updated': 'onNewWorld'
2014-01-03 13:32:13 -05:00
'god:infinite-loop': 'onInfiniteLoop'
'level:reload-from-data': 'onLevelReloadFromData'
'level:reload-thang-type': 'onLevelReloadThangType'
'level:play-next-level': 'onPlayNextLevel'
'level:edit-wizard-settings': 'showWizardSettingsModal'
2014-01-03 13:32:13 -05:00
'level:session-will-save': 'onSessionWillSave'
2014-03-14 20:06:08 -04:00
'level:started': 'onLevelStarted'
2014-09-21 18:52:49 -04:00
'level:loading-view-unveiling': 'onLoadingViewUnveiling'
2014-03-14 20:06:08 -04:00
'level:loading-view-unveiled': 'onLoadingViewUnveiled'
'level:loaded': 'onLevelLoaded'
'level:session-loaded': 'onSessionLoaded'
2014-08-30 00:46:26 -04:00
'playback:real-time-playback-waiting': 'onRealTimePlaybackWaiting'
'playback:real-time-playback-started': 'onRealTimePlaybackStarted'
'playback:real-time-playback-ended': 'onRealTimePlaybackEnded'
'real-time-multiplayer:created-game': 'onRealTimeMultiplayerCreatedGame'
'real-time-multiplayer:joined-game': 'onRealTimeMultiplayerJoinedGame'
'real-time-multiplayer:left-game': 'onRealTimeMultiplayerLeftGame'
'real-time-multiplayer:manual-cast': 'onRealTimeMultiplayerCast'
'level:hero-config-changed': 'onHeroConfigChanged'
2014-01-03 13:32:13 -05:00
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', {}
2014-01-03 13:32:13 -05:00
2014-02-20 18:11:20 -05:00
shortcuts:
'ctrl+s': 'onCtrlS'
# Initial Setup #############################################################
2014-02-20 18:11:20 -05:00
2014-01-03 13:32:13 -05:00
constructor: (options, @levelID) ->
console.profile?() if PROFILE_ME
super options
2014-06-30 22:16:26 -04:00
if not me.get('hourOfCode') and @getQueryVariable 'hour_of_code'
@setUpHourOfCode()
2014-01-03 13:32:13 -05:00
@isEditorPreview = @getQueryVariable 'dev'
@sessionID = @getQueryVariable 'session'
2014-01-03 13:32:13 -05:00
2014-08-29 20:52:47 -04:00
$(window).on 'resize', @onWindowResize
2014-01-03 13:32:13 -05:00
@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()
2014-06-21 12:47:48 -04:00
application.tracker?.trackEvent 'Started Level Load', level: @levelID, label: @levelID, ['Google Analytics']
setUpHourOfCode: ->
me.set 'hourOfCode', true
me.patch()
2014-06-30 22:16:26 -04:00
$('body').append($('<img src="http://code.org/api/hour/begin_codecombat.png" style="visibility: hidden;">'))
application.tracker?.trackEvent 'Hour of Code Begin', {}
2014-02-12 15:41:41 -05:00
setLevel: (@level, givenSupermodel) ->
@supermodel.models = givenSupermodel.models
@supermodel.collections = givenSupermodel.collections
@supermodel.shouldSaveBackups = givenSupermodel.shouldSaveBackups
serializedLevel = @level.serialize @supermodel, @session, @otherSession
@god?.setLevel serializedLevel
2014-01-21 02:14:34 -05:00
if @world
@world.loadFromLevel serializedLevel, false
else
@load()
2014-01-15 21:37:47 -05:00
load: ->
@loadStartTime = new Date()
@god = new God debugWorker: true
2014-06-30 22:16:26 -04:00
@levelLoader = new LevelLoader supermodel: @supermodel, levelID: @levelID, sessionID: @sessionID, opponentSessionID: @getQueryVariable('opponent'), team: @getQueryVariable('team')
@listenToOnce @levelLoader, 'world-necessities-loaded', @onWorldNecessitiesLoaded
trackLevelLoadEnd: ->
return if @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
application.tracker?.trackEvent 'Play Level', Action: 'Loaded', levelID: @levelID
# CocoView overridden methods ###############################################
2014-01-03 13:32:13 -05:00
getRenderData: ->
c = super()
c.world = @world
2014-08-23 18:51:59 -04:00
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
2014-01-03 13:32:13 -05:00
c
afterRender: ->
2014-04-17 19:23:35 -04:00
super()
2014-01-03 13:32:13 -05:00
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')
2014-09-06 22:50:31 -04:00
$('body').bind('touchmove', false) if @isIPadApp()
2014-01-03 13:32:13 -05:00
afterInsert: ->
super()
# Partially Loaded Setup ####################################################
onWorldNecessitiesLoaded: ->
# Called when we have enough to build the world, but not everything is loaded
@grabLevelLoaderData()
2014-06-30 22:16:26 -04:00
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)
2014-01-03 13:32:13 -05:00
@initScriptManager()
grabLevelLoaderData: ->
@session = @levelLoader.session
@world = @levelLoader.world
@level = @levelLoader.level
@$el.addClass 'hero' if @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop']
@$el.addClass 'flags' if (@level.get('slug') in ['sky-span', 'coinucopia']) or (@level.get('type', true) in ['hero-ladder', 'hero-coop']) # TODO: figure out when the player has flags.
@otherSession = @levelLoader.opponentSession
2014-08-30 12:19:41 -04:00
@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')
2014-06-30 22:16:26 -04:00
@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
2014-01-03 13:32:13 -05:00
setupGod: ->
@god.setLevel @level.serialize @supermodel, @session, @otherSession
@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
2014-05-19 23:50:05 -04:00
@team = team
initGoalManager: ->
2014-05-19 23:50:05 -04:00
@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, levelID: @levelID, level: @level
@insertSubView new GoalsView {}
@insertSubView new LevelFlagsView world: @world if (@levelID in ['sky-span', 'coinucopia']) or @level.get('type', true) in ['hero-ladder', 'hero-coop'] # TODO: figure out when flags are available
@insertSubView new GoldView {}
@insertSubView new HUDView {level: @level}
@insertSubView new ChatView levelID: @levelID, sessionID: @session.id, session: @session
if @level.get('type') in ['ladder', 'hero-ladder']
@insertSubView new MultiplayerStatusView levelID: @levelID, session: @session, level: @level
@insertSubView new ProblemAlertView {}
worldName = utils.i18n @level.attributes, 'name'
@controlBar = @insertSubView new ControlBarView {worldName: worldName, session: @session, level: @level, supermodel: @supermodel}
2014-10-02 01:02:52 -04:00
#_.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: ->
@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')
# 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 not (e.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop'])
onSessionLoaded: (e) ->
# Just the level and session have been loaded by the level loader
if e.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop'] and not _.size e.session.get('heroConfig')?.inventory ? {}
setupManager = new LevelSetupManager({supermodel: @supermodel, levelID: @levelID, parent: @})
setupManager.open()
@onRealTimeMultiplayerLevelLoaded e.session if e.level.get('type') in ['ladder', 'hero-ladder']
onLoaded: ->
_.defer => @onLevelLoaderLoaded()
onLevelLoaderLoaded: ->
# Everything is now loaded
return unless @levelLoader.progress() is 1 # double check, since closing the guide may trigger this early
2014-08-30 17:30:53 -04:00
# Save latest level played.
if not (@levelLoader.level.get('type') in ['ladder', 'ladder-tutorial'])
me.set('lastLevel', @levelID)
me.save()
2014-08-30 17:30:53 -04:00
@saveRecentMatch() if @otherSession
@levelLoader.destroy()
@levelLoader = null
@initSurface()
2014-08-30 17:30:53 -04:00
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)
@surface = new Surface(@world, normalSurface, webGLSurface, thangTypes: @supermodel.getModels(ThangType), playJingle: not @isEditorPreview, wizards: not (@level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop']))
worldBounds = @world.getBounds()
2014-06-30 22:16:26 -04:00
bounds = [{x: worldBounds.left, y: worldBounds.top}, {x: worldBounds.right, y: worldBounds.bottom}]
@surface.camera.setBounds(bounds)
2014-06-30 22:16:26 -04:00
@surface.camera.zoomTo({x: 0, y: 0}, 0.1, 0)
# Once Surface is Loaded ####################################################
onLevelStarted: ->
return unless @surface?
@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()
if @otherSession and not (@level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop'])
# 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()
2014-03-14 20:06:08 -04:00
2014-09-21 18:52:49 -04:00
onLoadingViewUnveiling: (e) ->
@restoreSessionState()
2014-03-14 20:06:08 -04:00
onLoadingViewUnveiled: (e) ->
2014-04-28 17:01:33 -04:00
@loadingView.$el.remove()
2014-03-14 20:06:08 -04:00
@removeSubView @loadingView
@loadingView = null
@playAmbientSound()
application.tracker?.trackEvent 'Play Level', Action: 'Start Level', levelID: @levelID
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) in ['hero', 'hero-ladder', 'hero-coop']
Backbone.Mediator.publish 'level:suppress-selection-sounds', suppress: true
2014-09-06 22:50:31 -04:00
Backbone.Mediator.publish 'tome:select-primary-sprite', {}
Backbone.Mediator.publish 'level:suppress-selection-sounds', suppress: false
@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
2014-01-03 13:32:13 -05:00
# callbacks
2014-02-20 18:11:20 -05:00
onCtrlS: (e) ->
e.preventDefault()
2014-01-03 13:32:13 -05:00
onLevelReloadFromData: (e) ->
isReload = Boolean @world
@setLevel e.level, e.supermodel
2014-01-03 13:32:13 -05:00
if isReload
@scriptManager.setScripts(e.level.get('scripts'))
2014-08-31 18:08:52 -04:00
Backbone.Mediator.publish 'tome:cast-spell', {} # a bit hacky
2014-01-03 13:32:13 -05:00
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
2014-08-31 18:08:52 -04:00
Backbone.Mediator.publish 'tome:cast-spell', {}
onHeroConfigChanged: (e) ->
2014-08-31 18:08:52 -04:00
# 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.'
2014-08-31 18:08:52 -04:00
Backbone.Mediator.publish 'router:navigate', {
route: window.location.pathname,
viewClass: PlayLevelView,
viewArgs: [{supermodel: @supermodel, autoUnveil: true}, @levelID]
}
onWindowResize: (e) => @endHighlight()
2014-01-03 13:32:13 -05:00
2014-02-12 15:41:41 -05:00
onDisableControls: (e) ->
2014-01-03 13:32:13 -05:00
return if e.controls and not ('level' in e.controls)
@shortcutsEnabled = false
@wasFocusedOn = document.activeElement
$('body').focus()
2014-02-12 15:41:41 -05:00
onEnableControls: (e) ->
2014-01-03 13:32:13 -05:00
return if e.controls? and not ('level' in e.controls)
@shortcutsEnabled = true
$(@wasFocusedOn).focus() if @wasFocusedOn
@wasFocusedOn = null
2014-02-12 15:41:41 -05:00
onDonePressed: -> @showVictory()
2014-01-03 13:32:13 -05:00
2014-02-12 15:41:41 -05:00
onShowVictory: (e) ->
$('#level-done-button').show() unless @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop']
2014-01-03 13:32:13 -05:00
@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
2014-01-03 13:32:13 -05:00
showVictory: ->
2014-03-31 16:56:13 -04:00
options = {level: @level, supermodel: @supermodel, session: @session}
ModalClass = if @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop'] then HeroVictoryModal else VictoryModal
victoryModal = new ModalClass(options)
@openModalView(victoryModal)
2014-03-31 16:56:13 -04:00
if me.get('anonymous')
window.nextLevelURL = @getNextLevelURL() # Signup will go here on completion instead of reloading.
2014-01-03 13:32:13 -05:00
onRestartLevel: ->
@tome.reloadAllCode()
Backbone.Mediator.publish 'level:restarted', {}
2014-01-03 13:32:13 -05:00
$('#level-done-button', @$el).hide()
application.tracker?.trackEvent 'Confirmed Restart', level: @level.get('name'), label: @level.get('name')
2014-01-03 13:32:13 -05:00
onInfiniteLoop: (e) ->
return unless e.firstWorld
@openModalView new InfiniteLoopModal()
application.tracker?.trackEvent 'Saw Initial Infinite Loop', level: @level.get('name'), label: @level.get('name')
2014-01-03 13:32:13 -05:00
2014-02-12 15:41:41 -05:00
onPlayNextLevel: ->
2014-03-31 16:56:13 -04:00
nextLevelID = @getNextLevelID()
nextLevelURL = @getNextLevelURL()
2014-01-03 13:32:13 -05:00
Backbone.Mediator.publish 'router:navigate', {
2014-03-31 16:56:13 -04:00
route: nextLevelURL,
2014-01-03 13:32:13 -05:00
viewClass: PlayLevelView,
2014-06-30 22:16:26 -04:00
viewArgs: [{supermodel: @supermodel}, nextLevelID]}
2014-01-03 13:32:13 -05:00
getNextLevel: ->
return null unless nextLevelOriginal = @level.get('nextLevel')?.original
2014-01-03 13:32:13 -05:00
levels = @supermodel.getModels(Level)
return l for l in levels when l.get('original') is nextLevelOriginal
2014-03-31 16:56:13 -04:00
getNextLevelID: ->
return null unless nextLevel = @getNextLevel()
2014-03-31 16:56:13 -04:00
nextLevelID = nextLevel.get('slug') or nextLevel.id
getNextLevelURL: ->
return null unless @getNextLevelID()
"/play/level/#{@getNextLevelID()}"
2014-03-31 16:56:13 -04:00
onHighlightDOM: (e) -> @highlightElement e.selector, delay: e.delay, sides: e.sides, offset: e.offset, rotation: e.rotation
2014-01-03 13:32:13 -05:00
onEndHighlight: -> @endHighlight()
2014-01-03 13:32:13 -05:00
onFocusDom: (e) -> $(e.selector).focus()
2014-01-03 13:32:13 -05:00
2014-02-11 18:38:36 -05:00
onMultiplayerChanged: (e) ->
2014-01-03 13:32:13 -05:00
if @session.get('multiplayer')
@bus.connect()
else
@bus.removeFirebaseData =>
@bus.disconnect()
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.
2014-05-12 16:28:46 -04:00
#@saveScreenshot e.session
2014-01-03 13:32:13 -05:00
# Throttled
saveScreenshot: (session) =>
return unless screenshot = @surface?.screenshot()
session.save {screenshot: screenshot}, {patch: true, type: 'PUT'}
2014-01-03 13:32:13 -05:00
# 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
2014-08-30 00:46:26 -04:00
onRealTimePlaybackWaiting: (e) ->
@$el.addClass('real-time').focus()
@onWindowResize()
onRealTimePlaybackStarted: (e) ->
@$el.addClass('real-time').focus()
@onWindowResize()
onRealTimePlaybackEnded: (e) ->
2014-09-23 13:56:52 -04:00
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'
2014-01-03 13:32:13 -05:00
destroy: ->
@levelLoader?.destroy()
2014-01-03 13:32:13 -05:00
@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()
2014-08-29 20:52:47 -04:00
$(window).off 'resize', @onWindowResize
2014-02-11 17:24:06 -05:00
delete window.world # not sure where this is set, but this is one way to clean it up
2014-01-03 13:32:13 -05:00
@bus?.destroy()
#@instance.save() unless @instance.loading
2014-03-31 16:56:13 -04:00
delete window.nextLevelURL
2014-01-03 13:32:13 -05:00
console.profileEnd?() if PROFILE_ME
@onRealTimeMultiplayerLevelUnloaded()
super()
# Real-time Multiplayer ######################################################
#
# This view acts as a hub for the real-time multiplayer session for the current level.
#
# It performs these actions:
# Player heartbeat
# Publishes player status
# Updates real-time multiplayer session state
# Updates real-time multiplayer player state
# Cleans up old sessions (sets state to 'finished')
# Real-time multiplayer cast handshake
#
# It monitors these:
# Real-time multiplayer sessions
# Current real-time multiplayer session
# Internal multiplayer create/joined/left events
#
# Real-time state variables:
# @realTimePlayerStatus - User's real-time multiplayer state for this level
# @realTimePlayerGameStatus - User's state for current real-time multiplayer game session
# @realTimeSession - Current real-time multiplayer game session
# @realTimeOpponent - Current real-time multiplayer opponent
# @realTimePlayers - Real-time players for current real-time multiplayer game session
# @realTimeSessionCollection - Collection of all real-time multiplayer sessions
#
# TODO: Move this code to it's own file, or possibly the LevelBus
# TODO: save settings somewhere reasonable
onRealTimeMultiplayerLevelLoaded: (session) ->
return if me.get('anonymous')
players = new RealTimeCollection('multiplayer_players/' + @levelID)
players.create
id: me.id
name: session.get('creatorName')
state: 'playing'
created: new Date().toISOString()
heartbeat: new Date().toISOString()
@realTimePlayerStatus = new RealTimeModel('multiplayer_players/' + @levelID + '/' + me.id)
@timerMultiplayerHeartbeatID = setInterval @onRealTimeMultiplayerHeartbeat, 60 * 1000
@cleanupRealTimeSessions()
cleanupRealTimeSessions: ->
@realTimeSessionCollection = new RealTimeCollection 'multiplayer_level_sessions'
@realTimeSessionCollection.on 'add', @cleanupRealTimeSession
@realTimeSessionCollection.each @cleanupRealTimeSession
cleanupRealTimeSession: (session) =>
if session.get('state') isnt 'finished'
players = new RealTimeCollection 'multiplayer_level_sessions/' + session.id + '/players'
players.each (player) =>
if player.id is me.id
p = new RealTimeModel 'multiplayer_level_sessions/' + session.id + '/players/' + me.id
console.info 'Cleaning up previous real-time multiplayer session', session.id
p.set 'state', 'left'
session.set 'state', 'finished'
onRealTimeMultiplayerLevelUnloaded: ->
clearInterval @timerMultiplayerHeartbeatID if @timerMultiplayerHeartbeatID?
if @realTimeSessionCollection?
@realTimeSessionCollection.off 'add', @cleanupRealTimeSession
@realTimeSessionCollection = null
onRealTimeMultiplayerHeartbeat: =>
@realTimePlayerStatus.set 'heartbeat', new Date().toISOString() if @realTimePlayerStatus
onRealTimeMultiplayerCreatedGame: (e) ->
# Watch external multiplayer session
@realTimeSession = new RealTimeModel 'multiplayer_level_sessions/' + e.session.id
@realTimeSession.on 'change', @onRealTimeSessionChanged
@realTimePlayers = new RealTimeCollection 'multiplayer_level_sessions/' + e.session.id + '/players'
@realTimePlayers.on 'add', @onRealTimePlayerAdded
@realTimePlayerGameStatus = new RealTimeModel 'multiplayer_level_sessions/' + e.session.id + '/players/' + me.id
@realTimePlayerGameStatus.set 'state', 'coding'
@realTimePlayerStatus.set 'state', 'available'
Backbone.Mediator.publish 'real-time-multiplayer:player-status', status: 'Waiting for opponent..'
onRealTimeSessionChanged: (e) =>
# console.log 'PlayLevelView onRealTimeSessionChanged', e
if e.get('state') is 'finished'
@realTimeGameEnded()
Backbone.Mediator.publish 'real-time-multiplayer:left-game', {}
onRealTimePlayerAdded: (e) =>
# console.log 'PlayLevelView onRealTimePlayerAdded', e
# Assume game is full, game on
if @realTimeSession.get('state') is 'creating'
@realTimeSession.set 'state', 'coding'
@realTimePlayerStatus.set 'state', 'unavailable'
@realTimeOpponent = new RealTimeModel('multiplayer_level_sessions/' + @realTimeSession.id + '/players/' + e.id)
@realTimeOpponent.on 'change', @onRealTimeOpponentChanged
Backbone.Mediator.publish 'real-time-multiplayer:player-status', status: 'Playing against ' + e.get('name')
else
console.error 'PlayLevelView onRealTimePlayerAdded session in unexpected state', @realTimeSession.get('state')
onRealTimeOpponentChanged: (e) =>
# console.log 'PlayLevelView onRealTimeOpponentChanged', e
switch @realTimeOpponent.get('state')
when 'left'
console.info 'Real-time multiplayer opponent left the game'
opponentID = @realTimeOpponent.id
@realTimeGameEnded()
Backbone.Mediator.publish 'real-time-multiplayer:left-game', id: opponentID
when 'submitted'
# TODO: What should this message say?
Backbone.Mediator.publish 'real-time-multiplayer:player-status', status: @realTimeOpponent.get('name') + ' waiting for your code..'
onRealTimeMultiplayerJoinedGame: (e) ->
# console.log 'PlayLevelView onRealTimeMultiplayerJoinedGame', e
if e.id is me.id
@realTimeSession = new RealTimeModel 'multiplayer_level_sessions/' + e.session.id
@realTimeSession.set 'state', 'coding'
@realTimeSession.on 'change', @onRealTimeSessionChanged
@realTimePlayers = new RealTimeCollection 'multiplayer_level_sessions/' + e.session.id + '/players'
@realTimePlayers.on 'add', @onRealTimePlayerAdded
@realTimePlayerGameStatus = new RealTimeModel 'multiplayer_level_sessions/' + e.session.id + '/players/' + me.id
@realTimePlayerGameStatus.set 'state', 'coding'
@realTimePlayerStatus.set 'state', 'unavailable'
for id, player of e.session.get('players')
if id isnt me.id
@realTimeOpponent = new RealTimeModel 'multiplayer_level_sessions/' + e.session.id + '/players/' + id
Backbone.Mediator.publish 'real-time-multiplayer:player-status', status: 'Playing against ' + player.name
onRealTimeMultiplayerLeftGame: (e) ->
# console.log 'PlayLevelView onRealTimeMultiplayerLeftGame', e
if e.id? and e.id is me.id
@realTimePlayerGameStatus.set 'state', 'left'
@realTimeGameEnded()
realTimeGameEnded: ->
if @realTimeOpponent?
@realTimeOpponent.off 'change', @onRealTimeOpponentChanged
@realTimeOpponent = null
if @realTimePlayers?
@realTimePlayers.off 'add', @onRealTimePlayerAdded
@realTimePlayers = null
if @realTimeSession?
@realTimeSession.off 'change', @onRealTimeSessionChanged
@realTimeSession.set 'state', 'finished'
@realTimeSession = null
if @realTimePlayerGameStatus?
@realTimePlayerGameStatus = null
if @realTimePlayerStatus?
@realTimePlayerStatus.set 'state', 'playing'
Backbone.Mediator.publish 'real-time-multiplayer:player-status', status: ''
onRealTimeMultiplayerCast: (e) ->
unless @realTimeSession
console.error 'onRealTimeMultiplayerCast without a multiplayerSession'
return
players = new RealTimeCollection('multiplayer_level_sessions/' + @realTimeSession.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']
@realTimeOpponentSubmittedCode opponentPlayer, myPlayer
else
# Wait for opponent to submit their code
Backbone.Mediator.publish 'real-time-multiplayer:player-status', status: 'Waiting for code from ' + @realTimeOpponent.get('name')
opponentPlayer.on 'change', (e) =>
state = opponentPlayer.get('state')
if state in ['submitted', 'ready']
@realTimeOpponentSubmittedCode opponentPlayer, myPlayer
onRealTimeMultiplayerPlaybackEnded: ->
if @realTimeSession?
@realTimeSession.set 'state', 'coding'
@realTimePlayers.each (player) -> player.set 'state', 'coding' if player.id is me.id
if @realTimeOpponent?
Backbone.Mediator.publish 'real-time-multiplayer:player-status', status: 'Playing against ' + @realTimeOpponent.get('name')
realTimeOpponentSubmittedCode: (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!'
@realTimeSession.set 'state', 'running'
Backbone.Mediator.publish 'real-time-multiplayer:player-status', status: 'Battling ' + @realTimeOpponent.get('name')
else
# Wait for opponent to be ready
opponentPlayer.on 'change', (e) =>
if opponentPlayer.get('state') is 'ready'
opponentPlayer.off 'change'
console.info 'All real-time multiplayer players are ready!'
@realTimeSession.set 'state', 'running'
Backbone.Mediator.publish 'real-time-multiplayer:player-status', status: 'Battling ' + @realTimeOpponent.get('name')