mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-27 17:45:40 -05:00
255 lines
8.6 KiB
CoffeeScript
255 lines
8.6 KiB
CoffeeScript
Bus = require './Bus'
|
|
{me} = require 'core/auth'
|
|
LevelSession = require 'models/LevelSession'
|
|
utils = require 'core/utils'
|
|
|
|
module.exports = class LevelBus extends Bus
|
|
|
|
@get: (levelID, sessionID) ->
|
|
docName = "play/level/#{levelID}/#{sessionID}"
|
|
return Bus.getFromCache(docName) or new LevelBus docName
|
|
|
|
subscriptions:
|
|
'tome:editing-began': 'onEditingBegan'
|
|
'tome:editing-ended': 'onEditingEnded'
|
|
'script:state-changed': 'onScriptStateChanged'
|
|
'script:ended': 'onScriptEnded'
|
|
'script:reset': 'onScriptReset'
|
|
'surface:sprite-selected': 'onSpriteSelected'
|
|
'level:show-victory': 'onVictory'
|
|
'tome:spell-changed': 'onSpellChanged'
|
|
'tome:spell-created': 'onSpellCreated'
|
|
'tome:cast-spells': 'onCastSpells'
|
|
'tome:winnability-updated': 'onWinnabilityUpdated'
|
|
'application:idle-changed': 'onIdleChanged'
|
|
'goal-manager:new-goal-states': 'onNewGoalStates'
|
|
'god:new-world-created': 'onNewWorldCreated'
|
|
|
|
constructor: ->
|
|
super(arguments...)
|
|
@changedSessionProperties = {}
|
|
saveDelay = window.serverConfig?.sessionSaveDelay
|
|
[wait, maxWait] = switch
|
|
when not application.isProduction or not saveDelay then [1, 5] # Save quickly in development.
|
|
when me.isAnonymous() then [saveDelay.anonymous.min, saveDelay.anonymous.max]
|
|
else [saveDelay.registered.min, saveDelay.registered.max]
|
|
@saveSession = _.debounce @reallySaveSession, wait * 1000, {maxWait: maxWait * 1000}
|
|
@playerIsIdle = false
|
|
|
|
init: ->
|
|
super()
|
|
@fireScriptsRef = @fireRef?.child('scripts')
|
|
|
|
setSession: (@session) ->
|
|
@timerIntervalID = setInterval(@incrementSessionPlaytime, 1000)
|
|
|
|
onIdleChanged: (e) ->
|
|
@playerIsIdle = e.idle
|
|
|
|
incrementSessionPlaytime: =>
|
|
if @playerIsIdle then return
|
|
@changedSessionProperties.playtime = true
|
|
@session.set('playtime', (@session.get('playtime') ? 0) + 1)
|
|
|
|
onPoint: ->
|
|
return true
|
|
|
|
onMeSynced: =>
|
|
super()
|
|
|
|
join: ->
|
|
super()
|
|
|
|
disconnect: ->
|
|
@fireScriptsRef?.off()
|
|
@fireScriptsRef = null
|
|
super()
|
|
|
|
removeFirebaseData: (callback) ->
|
|
return callback?() unless @myConnection
|
|
@myConnection.child('connected')
|
|
@fireRef.remove()
|
|
@onDisconnect.cancel(-> callback?())
|
|
|
|
# UPDATING FIREBASE AND SESSION
|
|
|
|
onEditingBegan: -> #@wizardRef?.child('editing').set(true) # no more wizards
|
|
onEditingEnded: -> #@wizardRef?.child('editing').set(false) # no more wizards
|
|
|
|
# HACK: Backbone does not work with nested documents, but we want to
|
|
# patch only those props that have changed. Look into plugins to
|
|
# give Backbone support for nested docs and update the code here.
|
|
|
|
# TODO: The LevelBus doesn't need to be in charge of updating the
|
|
# LevelSession object. Either break this off into a separate class
|
|
# or have the LevelSession object listen for all these events itself.
|
|
|
|
setSpells: (spells) ->
|
|
@onSpellCreated spell: spell for spellKey, spell of spells
|
|
|
|
onSpellChanged: (e) ->
|
|
return unless @onPoint()
|
|
code = @session.get('code')
|
|
code ?= {}
|
|
parts = e.spell.spellKey.split('/')
|
|
|
|
code[parts[0]] ?= {}
|
|
code[parts[0]][parts[1]] = e.spell.getSource()
|
|
@changedSessionProperties.code = true
|
|
@session.set({'code': code})
|
|
@saveSession()
|
|
|
|
onSpellCreated: (e) ->
|
|
return unless @onPoint()
|
|
spellTeam = e.spell.team
|
|
@teamSpellMap ?= {}
|
|
@teamSpellMap[spellTeam] ?= []
|
|
|
|
unless e.spell.spellKey in @teamSpellMap[spellTeam]
|
|
@teamSpellMap[spellTeam].push e.spell.spellKey
|
|
@changedSessionProperties.teamSpells = true
|
|
@session.set({'teamSpells': @teamSpellMap})
|
|
@saveSession()
|
|
if spellTeam is me.team or (e.spell.otherSession and spellTeam isnt e.spell.otherSession.get('team'))
|
|
# https://github.com/codecombat/codecombat/issues/81
|
|
@onSpellChanged e # Save the new spell to the session, too.
|
|
|
|
onCastSpells: (e) ->
|
|
return unless @onPoint() and e.realTime
|
|
# We have incremented state.submissionCount and reset state.flagHistory.
|
|
@changedSessionProperties.state = true
|
|
@saveSession()
|
|
|
|
onWinnabilityUpdated: (e) ->
|
|
return unless @onPoint() and e.winnable
|
|
return unless e.level.get('slug') in ['ace-of-coders', 'elemental-wars'] # Mirror matches don't otherwise show victory, so we win here.
|
|
return if @session.get('state')?.complete
|
|
@onVictory()
|
|
|
|
onNewWorldCreated: (e) ->
|
|
return unless @onPoint()
|
|
# Record the flag history.
|
|
state = @session.get('state')
|
|
flagHistory = (flag for flag in e.world.flagHistory when flag.source isnt 'code')
|
|
return if _.isEqual state.flagHistory, flagHistory
|
|
state.flagHistory = flagHistory
|
|
@changedSessionProperties.state = true
|
|
@session.set('state', state)
|
|
@saveSession()
|
|
|
|
onScriptStateChanged: (e) ->
|
|
return unless @onPoint()
|
|
@fireScriptsRef?.update(e)
|
|
state = @session.get('state')
|
|
scripts = state.scripts ? {}
|
|
scripts.currentScript = e.currentScript
|
|
scripts.currentScriptOffset = e.currentScriptOffset
|
|
@changedSessionProperties.state = true
|
|
@session.set('state', state)
|
|
@saveSession()
|
|
|
|
onScriptEnded: (e) ->
|
|
return unless @onPoint()
|
|
state = @session.get('state')
|
|
scripts = state.scripts
|
|
scripts.ended ?= {}
|
|
return if scripts.ended[e.scriptID]?
|
|
index = _.keys(scripts.ended).length + 1
|
|
@fireScriptsRef?.child('ended').child(e.scriptID).set(index)
|
|
scripts.ended[e.scriptID] = index
|
|
@session.set('state', state)
|
|
@changedSessionProperties.state = true
|
|
@saveSession()
|
|
|
|
onScriptReset: ->
|
|
return unless @onPoint()
|
|
@fireScriptsRef?.set({})
|
|
state = @session.get('state')
|
|
state.scripts = {}
|
|
#state.complete = false # Keep it complete once ever completed.
|
|
@session.set('state', state)
|
|
@changedSessionProperties.state = true
|
|
@saveSession()
|
|
|
|
onSpriteSelected: (e) ->
|
|
return unless @onPoint()
|
|
state = @session.get('state')
|
|
state.selected = e.thang?.id or null
|
|
@session.set('state', state)
|
|
@changedSessionProperties.state = true
|
|
@saveSession()
|
|
|
|
onVictory: ->
|
|
return unless @onPoint()
|
|
state = @session.get('state')
|
|
state.complete = true
|
|
@session.set('state', state)
|
|
@changedSessionProperties.state = true
|
|
@reallySaveSession() # Make sure it saves right away; don't debounce it.
|
|
|
|
onNewGoalStates: (e) ->
|
|
# TODO: this log doesn't capture when null-status goals are being set during world streaming. Where can they be coming from?
|
|
goalStates = e.goalStates
|
|
return console.error("Somehow trying to save null goal states!", newGoalStates) if _.find(newGoalStates, (gs) -> not gs.status)
|
|
|
|
return unless e.overallStatus is 'success'
|
|
newGoalStates = goalStates
|
|
state = @session.get('state')
|
|
oldGoalStates = state.goalStates or {}
|
|
|
|
changed = false
|
|
for goalKey, goalState of newGoalStates
|
|
continue if oldGoalStates[goalKey]?.status is 'success' and goalState.status isnt 'success' # don't undo success, this property is for keying off achievements
|
|
continue if utils.kindaEqual state.goalStates?[goalKey], goalState # Only save when goals really change
|
|
changed = true
|
|
oldGoalStates[goalKey] = _.cloneDeep newGoalStates[goalKey]
|
|
|
|
if changed
|
|
state.goalStates = oldGoalStates
|
|
@session.set 'state', state
|
|
@changedSessionProperties.state = true
|
|
@saveSession()
|
|
|
|
onPlayerJoined: (snapshot) =>
|
|
super(arguments...)
|
|
return unless @onPoint()
|
|
players = @session.get('players')
|
|
players ?= {}
|
|
player = snapshot.val()
|
|
return if players[player.id]?
|
|
players[player.id] = {}
|
|
@session.set('players', players)
|
|
@changedSessionProperties.players = true
|
|
@saveSession()
|
|
|
|
onChatAdded: (snapshot) =>
|
|
super(arguments...)
|
|
chat = @session.get('chat')
|
|
chat ?= []
|
|
message = snapshot.val()
|
|
return if message.system
|
|
chat.push(message)
|
|
chat = chat[chat.length-50...] if chat.length > 50
|
|
@session.set('chat', chat)
|
|
@changedSessionProperties.chat = true
|
|
@saveSession()
|
|
|
|
# Debounced as saveSession
|
|
reallySaveSession: ->
|
|
return if _.isEmpty @changedSessionProperties
|
|
# don't let peeking admins mess with the session accidentally
|
|
return unless @session.get('creator') is me.id
|
|
return if @session.fake
|
|
Backbone.Mediator.publish 'level:session-will-save', session: @session
|
|
patch = {}
|
|
patch[prop] = @session.get(prop) for prop of @changedSessionProperties
|
|
@changedSessionProperties = {}
|
|
|
|
# since updates are coming fast and loose for session objects
|
|
# don't let what the server returns overwrite changes since the save began
|
|
tempSession = new LevelSession _id: @session.id
|
|
tempSession.save(patch, {patch: true, type: 'PUT'})
|
|
|
|
destroy: ->
|
|
clearInterval(@timerIntervalID)
|
|
super()
|