mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-11 00:02:19 -05:00
4bb3ac1f0a
Closes #3686
312 lines
12 KiB
CoffeeScript
312 lines
12 KiB
CoffeeScript
RootView = require 'views/core/RootView'
|
|
template = require 'templates/play/spectate'
|
|
{me} = require 'core/auth'
|
|
ThangType = require 'models/ThangType'
|
|
utils = require 'core/utils'
|
|
|
|
World = require 'lib/world/world'
|
|
|
|
# tools
|
|
Surface = require 'lib/surface/Surface'
|
|
God = require 'lib/God' # 'lib/Buddha'
|
|
GoalManager = require 'lib/world/GoalManager'
|
|
ScriptManager = require 'lib/scripts/ScriptManager'
|
|
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'
|
|
|
|
# subviews
|
|
LoadingView = require './level/LevelLoadingView'
|
|
TomeView = require './level/tome/TomeView'
|
|
ChatView = require './level/LevelChatView'
|
|
HUDView = require './level/LevelHUDView'
|
|
ControlBarView = require './level/ControlBarView'
|
|
PlaybackView = require './level/LevelPlaybackView'
|
|
GoalsView = require './level/LevelGoalsView'
|
|
GoldView = require './level/LevelGoldView'
|
|
DuelStatsView = require './level/DuelStatsView'
|
|
VictoryModal = require './level/modal/VictoryModal'
|
|
InfiniteLoopModal = require './level/modal/InfiniteLoopModal'
|
|
|
|
PROFILE_ME = false
|
|
|
|
module.exports = class SpectateLevelView extends RootView
|
|
id: 'spectate-level-view'
|
|
template: template
|
|
cache: false
|
|
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.
|
|
'god:new-world-created': 'onNewWorld'
|
|
'god:streaming-world-updated': 'onNewWorld'
|
|
'god:infinite-loop': 'onInfiniteLoop'
|
|
'level:next-game-pressed': 'onNextGamePressed'
|
|
'level:started': 'onLevelStarted'
|
|
'level:loading-view-unveiled': 'onLoadingViewUnveiled'
|
|
|
|
constructor: (options, @levelID) ->
|
|
console.profile?() if PROFILE_ME
|
|
super options
|
|
|
|
@sessionOne = @getQueryVariable 'session-one'
|
|
@sessionTwo = @getQueryVariable 'session-two'
|
|
if options.spectateSessions
|
|
@sessionOne = options.spectateSessions.sessionOne
|
|
@sessionTwo = options.spectateSessions.sessionTwo
|
|
|
|
if not @sessionOne or not @sessionTwo
|
|
@fetchRandomSessionPair (err, data) =>
|
|
if err? then return console.log "There was an error fetching the random session pair: #{data}"
|
|
@sessionOne = data[0]._id
|
|
@sessionTwo = data[1]._id
|
|
@load()
|
|
else
|
|
@load()
|
|
|
|
setLevel: (@level, @supermodel) ->
|
|
serializedLevel = @level.serialize @supermodel, @session, @otherSession
|
|
@god?.setLevel serializedLevel
|
|
if @world
|
|
@world.loadFromLevel serializedLevel, false
|
|
else
|
|
@load()
|
|
|
|
load: ->
|
|
@levelLoader = new LevelLoader
|
|
supermodel: @supermodel
|
|
levelID: @levelID
|
|
sessionID: @sessionOne
|
|
opponentSessionID: @sessionTwo
|
|
spectateMode: true
|
|
team: @getQueryVariable('team')
|
|
@god = new God maxAngels: 1, spectate: true
|
|
|
|
getRenderData: ->
|
|
c = super()
|
|
c.world = @world
|
|
c
|
|
|
|
afterRender: ->
|
|
window.onPlayLevelViewLoaded? @ # still a hack
|
|
@insertSubView @loadingView = new LoadingView autoUnveil: true, level: @levelLoader?.level ? @level
|
|
@$el.find('#level-done-button').hide()
|
|
super()
|
|
$('body').addClass('is-playing')
|
|
|
|
onLoaded: ->
|
|
_.defer => @onLevelLoaderLoaded()
|
|
|
|
onLevelLoaderLoaded: ->
|
|
@grabLevelLoaderData()
|
|
#at this point, all requisite data is loaded, and sessions are not denormalized
|
|
team = @world.teamForPlayer(0)
|
|
@loadOpponentTeam(team)
|
|
@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
|
|
@initSurface()
|
|
@initGoalManager()
|
|
@initScriptManager()
|
|
@insertSubviews()
|
|
@initVolume()
|
|
|
|
@originalSessionState = $.extend(true, {}, @session.get('state'))
|
|
@register()
|
|
@controlBar.setBus(@bus)
|
|
@surface.showLevel()
|
|
|
|
grabLevelLoaderData: ->
|
|
@session = @levelLoader.session
|
|
@world = @levelLoader.world
|
|
@level = @levelLoader.level
|
|
@otherSession = @levelLoader.opponentSession
|
|
@levelLoader.destroy()
|
|
@levelLoader = null
|
|
|
|
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
|
|
|
|
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)
|
|
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
|
|
|
|
onLevelStarted: (e) ->
|
|
go = =>
|
|
@loadingView?.startUnveiling()
|
|
@loadingView?.unveil true
|
|
_.delay go, 1000
|
|
|
|
onLoadingViewUnveiled: (e) ->
|
|
# Don't remove it; we want its decoration around on large screens.
|
|
#@removeSubView @loadingView
|
|
#@loadingView = null
|
|
Backbone.Mediator.publish 'level:set-playing', playing: false
|
|
Backbone.Mediator.publish 'level:set-time', time: 1 # Helps to have perhaps built a few Thangs and gotten a good list of spritesheets we need to render for our initial paused frame
|
|
|
|
onSupermodelLoadedOne: =>
|
|
@modelsLoaded ?= 0
|
|
@modelsLoaded += 1
|
|
@updateInitString()
|
|
|
|
updateInitString: ->
|
|
return if @surface
|
|
@modelsLoaded ?= 0
|
|
canvas = @$el.find('#surface')[0]
|
|
ctx = canvas.getContext('2d')
|
|
ctx.font='20px Georgia'
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
|
ctx.fillText("Loaded #{@modelsLoaded} thingies",50,50)
|
|
|
|
insertSubviews: ->
|
|
@insertSubView @tome = new TomeView levelID: @levelID, session: @session, otherSession: @otherSession, thangs: @world.thangs, supermodel: @supermodel, spectateView: true, spectateOpponentCodeLanguage: @otherSession?.get('submittedCodeLanguage'), level: @level, god: @god
|
|
@insertSubView new PlaybackView session: @session, level: @level
|
|
|
|
@insertSubView new GoldView {}
|
|
@insertSubView new HUDView {level: @level}
|
|
@insertSubView new DuelStatsView level: @level, session: @session, otherSession: @otherSession, supermodel: @supermodel, thangs: @world.thangs if @level.get('type') in ['hero-ladder', 'course-ladder']
|
|
@insertSubView @controlBar = new ControlBarView {worldName: utils.i18n(@level.attributes, 'name'), session: @session, level: @level, supermodel: @supermodel, spectateGame: true}
|
|
|
|
# callbacks
|
|
|
|
onInfiniteLoop: (e) ->
|
|
return unless e.firstWorld and e.god is @god
|
|
@openModalView new InfiniteLoopModal()
|
|
window.tracker?.trackEvent 'Saw Initial Infinite Loop', level: @world.name, label: @world.name
|
|
|
|
# initialization
|
|
|
|
initSurface: ->
|
|
webGLSurface = $('canvas#webgl-surface', @$el)
|
|
normalSurface = $('canvas#normal-surface', @$el)
|
|
@surface = new Surface @world, normalSurface, webGLSurface, thangTypes: @supermodel.getModels(ThangType), spectateGame: true, playerNames: @findPlayerNames(), levelType: @level.get('type', true)
|
|
worldBounds = @world.getBounds()
|
|
bounds = [{x:worldBounds.left, y:worldBounds.top}, {x:worldBounds.right, y:worldBounds.bottom}]
|
|
@surface.camera.setBounds(bounds)
|
|
zoom = =>
|
|
@surface.camera.zoomTo({x: (worldBounds.right - worldBounds.left) / 2, y: (worldBounds.top - worldBounds.bottom) / 2}, 0.1, 0)
|
|
_.delay zoom, 4000 # call it later for some reason (TODO: figure this out)
|
|
|
|
findPlayerNames: ->
|
|
playerNames = {}
|
|
for session in [@session, @otherSession] when session?.get('team')
|
|
playerNames[session.get('team')] = session.get('creatorName') or 'Anonymous'
|
|
playerNames
|
|
|
|
initGoalManager: ->
|
|
@goalManager = new GoalManager(@world, @level.get('goals'))
|
|
@god.setGoalManager @goalManager
|
|
|
|
initScriptManager: ->
|
|
if @world.scripts
|
|
nonVictoryPlaybackScripts = _.reject @world.scripts, (script) ->
|
|
script.id.indexOf('Set Camera Boundaries') is -1
|
|
else
|
|
console.log 'World scripts don\'t exist!'
|
|
nonVictoryPlaybackScripts = []
|
|
@scriptManager = new ScriptManager({scripts: nonVictoryPlaybackScripts, view:@, session: @session})
|
|
@scriptManager.loadFromSession()
|
|
|
|
initVolume: ->
|
|
volume = me.get('volume')
|
|
volume = 1.0 unless volume?
|
|
Backbone.Mediator.publish 'level:set-volume', volume: volume
|
|
|
|
register: -> return
|
|
|
|
onSessionWillSave: (e) ->
|
|
# Something interesting has happened, so (at a lower frequency), we'll save a screenshot.
|
|
console.log 'Session is saving but shouldn\'t save!!!!!!!'
|
|
|
|
# Throttled
|
|
saveScreenshot: (session) =>
|
|
return unless screenshot = @surface?.screenshot()
|
|
session.save {screenshot: screenshot}, {patch: true, type: 'PUT'}
|
|
|
|
setTeam: (team) ->
|
|
team = team?.team unless _.isString team
|
|
team ?= 'humans'
|
|
me.team = team
|
|
Backbone.Mediator.publish 'level:team-set', team: team
|
|
|
|
# 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
|
|
if @world.frames.length is @world.totalFrames # Finished loading
|
|
@lastWorldFramesLoaded = 0
|
|
unless @getQueryVariable('autoplay') is false
|
|
Backbone.Mediator.publish 'level:set-playing', playing: true # Since we paused at first, now we autostart playback.
|
|
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
|
|
|
|
onNextGamePressed: (e) ->
|
|
@fetchRandomSessionPair (err, data) =>
|
|
return if @destroyed
|
|
if err? then return console.log "There was an error fetching the random session pair: #{data}"
|
|
@sessionOne = data[0]._id
|
|
@sessionTwo = data[1]._id
|
|
url = "/play/spectate/#{@levelID}?session-one=#{@sessionOne}&session-two=#{@sessionTwo}"
|
|
if leagueID = @getQueryVariable 'league'
|
|
url += "&league=" + leagueID
|
|
Backbone.Mediator.publish 'router:navigate', {
|
|
route: url,
|
|
viewClass: SpectateLevelView,
|
|
viewArgs: [
|
|
{
|
|
spectateSessions: {sessionOne: @sessionOne, sessionTwo: @sessionTwo}
|
|
supermodel: @supermodel
|
|
}
|
|
@levelID
|
|
]
|
|
}
|
|
history?.pushState? {}, '', url # Backbone won't update the URL if just query parameters change
|
|
|
|
fetchRandomSessionPair: (cb) ->
|
|
console.log 'Fetching random session pair!'
|
|
randomSessionPairURL = "/db/level/#{@levelID}/random_session_pair"
|
|
$.ajax
|
|
url: randomSessionPairURL
|
|
type: 'GET'
|
|
cache: false
|
|
complete: (jqxhr, textStatus) ->
|
|
if textStatus isnt 'success'
|
|
cb('error', jqxhr.statusText)
|
|
else
|
|
cb(null, $.parseJSON(jqxhr.responseText))
|
|
|
|
destroy: ()->
|
|
@levelLoader?.destroy()
|
|
@surface?.destroy()
|
|
@god?.destroy()
|
|
@goalManager?.destroy()
|
|
@scriptManager?.destroy()
|
|
delete window.world # not sure where this is set, but this is one way to clean it up
|
|
console.profileEnd?() if PROFILE_ME
|
|
super()
|