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

700 lines
32 KiB
CoffeeScript

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()
@$('#how-to-play-game-dev-panel').removeClass('hide') if @level.isType('game-dev')
@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()