codecombat/app/lib/scripts/ScriptManager.coffee

351 lines
12 KiB
CoffeeScript

CocoClass = require 'lib/CocoClass'
CocoView = require 'views/kinds/CocoView'
{scriptMatchesEventPrereqs} = require './../world/script_event_prereqs'
allScriptModules = []
allScriptModules.push(require './SpriteScriptModule')
allScriptModules.push(require './DOMScriptModule')
allScriptModules.push(require './SurfaceScriptModule')
allScriptModules.push(require './PlaybackScriptModule')
allScriptModules.push(require './SoundScriptModule')
DEFAULT_BOT_MOVE_DURATION = 500
DEFAULT_SCRUB_DURATION = 1000
module.exports = ScriptManager = class ScriptManager extends CocoClass
scriptInProgress: false
currentNoteGroup: null
currentTimeouts: []
worldLoading: true
ignoreEvents: false
quiet: false
triggered: []
ended: []
noteGroupQueue: []
originalScripts: [] # use these later when you want to revert to an original state
subscriptions:
'script:end-current-script': 'onEndNoteGroup'
'level:loading-view-unveiling': -> @setWorldLoading(false)
'level:restarted': 'onLevelRestarted'
'level:shift-space-pressed': 'onEndNoteGroup'
'level:escape-pressed': 'onEndAll'
shortcuts:
'⇧+space, space, enter': -> Backbone.Mediator.publish 'level:shift-space-pressed', {}
'escape': -> Backbone.Mediator.publish 'level:escape-pressed', {}
# SETUP / TEARDOWN
constructor: (options) ->
super(options)
@originalScripts = options.scripts
@session = options.session
@debugScripts = CocoView.getQueryVariable 'dev'
@initProperties()
@addScriptSubscriptions()
@beginTicking()
setScripts: (@originalScripts) ->
@quiet = true
@initProperties()
@loadFromSession()
@quiet = false
@addScriptSubscriptions()
@run()
initProperties: ->
@endAll({force:true}) if @scriptInProgress
@triggered = []
@ended = []
@noteGroupQueue = []
@scripts = $.extend(true, [], @originalScripts)
addScriptSubscriptions: ->
idNum = 0
makeCallback = (channel) => (event) => @onNote(channel, event)
for script in @scripts
script.id = (idNum++).toString() unless script.id
callback = makeCallback(script.channel) # curry in the channel argument
@addNewSubscription(script.channel, callback)
beginTicking: ->
@tickInterval = setInterval @tick, 5000
tick: =>
scriptStates = {}
now = new Date()
for script in @scripts
scriptStates[script.id] =
timeSinceLastEnded: (if script.lastEnded then now - script.lastEnded else 0) / 1000
timeSinceLastTriggered: (if script.lastTriggered then now - script.lastTriggered else 0) / 1000
stateEvent =
scriptRunning: @currentNoteGroup?.scriptID or ''
noteGroupRunning: @currentNoteGroup?.name or ''
scriptStates: scriptStates
timeSinceLastScriptEnded: (if @lastScriptEnded then now - @lastScriptEnded else 0) / 1000
Backbone.Mediator.publish 'script:tick', stateEvent # Used to trigger level scripts.
loadFromSession: ->
# load the queue with note groups to skip through
@addEndedScriptsFromSession()
@addPartiallyEndedScriptFromSession()
for noteGroup in @noteGroupQueue
@processNoteGroup(noteGroup)
addPartiallyEndedScriptFromSession: ->
scripts = @session.get('state').scripts
return unless scripts?.currentScript
script = _.find @scripts, {id: scripts.currentScript}
return unless script
@triggered.push(script.id)
noteChain = @processScript(script)
return unless noteChain
if scripts.currentScriptOffset
noteGroup.skipMe = true for noteGroup in noteChain[..scripts.currentScriptOffset-1]
@addNoteChain(noteChain, false)
addEndedScriptsFromSession: ->
scripts = @session.get('state').scripts
return unless scripts
endedObj = scripts['ended'] or {}
sortedPairs = _.sortBy(_.pairs(endedObj), (pair) -> pair[1])
scriptsToSkip = (p[0] for p in sortedPairs)
for scriptID in scriptsToSkip
script = _.find @scripts, {id: scriptID}
unless script
console.warn 'Couldn\'t find script for', scriptID, 'from scripts', @scripts, 'when restoring session scripts.'
continue
continue if script.repeats # repeating scripts are not 'rerun'
@triggered.push(scriptID)
@ended.push(scriptID)
noteChain = @processScript(script)
return unless noteChain
noteGroup.skipMe = true for noteGroup in noteChain
@addNoteChain(noteChain, false)
setWorldLoading: (@worldLoading) ->
@run() unless @worldLoading
destroy: ->
@onEndAll()
clearInterval @tickInterval
super()
# TRIGGERERING NOTES
onNote: (channel, event) ->
return if @ignoreEvents
for script in @scripts
alreadyTriggered = script.id in @triggered
continue unless script.channel is channel
continue if alreadyTriggered and not script.repeats
continue if script.lastTriggered? and script.repeats is 'session'
continue if script.lastTriggered? and new Date().getTime() - script.lastTriggered < 1
continue if script.neverRun
if script.notAfter
for scriptID in script.notAfter
if scriptID in @triggered
script.neverRun = true
break
continue if script.neverRun
continue unless @scriptPrereqsSatisfied(script)
continue unless scriptMatchesEventPrereqs(script, event)
# everything passed!
console.debug "SCRIPT: Running script '#{script.id}'" if @debugScripts
script.lastTriggered = new Date().getTime()
@triggered.push(script.id) unless alreadyTriggered
noteChain = @processScript(script)
if not noteChain then return @trackScriptCompletions (script.id)
@addNoteChain(noteChain)
@run()
scriptPrereqsSatisfied: (script) ->
_.every(script.scriptPrereqs or [], (prereq) => prereq in @triggered)
processScript: (script) ->
noteChain = script.noteChain
return null unless noteChain?.length
noteGroup.scriptID = script.id for noteGroup in noteChain
lastNoteGroup = noteChain[noteChain.length - 1]
lastNoteGroup.isLast = true
return noteChain
addNoteChain: (noteChain, clearYields=true) ->
@processNoteGroup(noteGroup) for noteGroup in noteChain
noteGroup.index = i for noteGroup, i in noteChain
if clearYields
noteGroup.skipMe = true for noteGroup in @noteGroupQueue when noteGroup.script.yields
@noteGroupQueue.push noteGroup for noteGroup in noteChain
@endYieldingNote()
processNoteGroup: (noteGroup) ->
return if noteGroup.modules?
if noteGroup.playback?.scrub?
noteGroup.playback.scrub.duration ?= DEFAULT_SCRUB_DURATION
noteGroup.sprites ?= []
for sprite in noteGroup.sprites
if sprite.move?
sprite.move.duration ?= DEFAULT_BOT_MOVE_DURATION
sprite.id ?= 'Captain Anya'
noteGroup.script ?= {}
noteGroup.script.yields ?= true
noteGroup.script.skippable ?= true
noteGroup.modules = (new Module(noteGroup) for Module in allScriptModules when Module.neededFor(noteGroup))
endYieldingNote: ->
if @scriptInProgress and @currentNoteGroup?.script.yields
@endNoteGroup()
return true
# STARTING NOTES
run: ->
# catch all for analyzing the current state and doing whatever needs to happen next
return if @scriptInProgress
@skipAhead()
return unless @noteGroupQueue.length
nextNoteGroup = @noteGroupQueue[0]
return if @worldLoading and nextNoteGroup.skipMe
return if @worldLoading and not nextNoteGroup.script?.beforeLoad
@noteGroupQueue = @noteGroupQueue[1..]
@currentNoteGroup = nextNoteGroup
@notifyScriptStateChanged()
@scriptInProgress = true
@currentTimeouts = []
console.debug "SCRIPT: Starting note group '#{nextNoteGroup.name}'" if @debugScripts
for module in nextNoteGroup.modules
@processNote(note, nextNoteGroup) for note in module.startNotes()
if nextNoteGroup.script.duration
f = => @onNoteGroupTimeout? nextNoteGroup
setTimeout(f, nextNoteGroup.script.duration)
Backbone.Mediator.publish 'script:note-group-started', {}
skipAhead: ->
return if @worldLoading
return unless @noteGroupQueue[0]?.skipMe
@ignoreEvents = true
for noteGroup, i in @noteGroupQueue
break unless noteGroup.skipMe
console.debug "SCRIPT: Skipping note group '#{noteGroup.name}'" if @debugScripts
@processNoteGroup(noteGroup)
for module in noteGroup.modules
notes = module.skipNotes()
@processNote(note, noteGroup) for note in notes
@trackScriptCompletionsFromNoteGroup(noteGroup)
@noteGroupQueue = @noteGroupQueue[i..]
@ignoreEvents = false
processNote: (note, noteGroup) ->
note.event ?= {}
if note.delay
f = => @sendDelayedNote noteGroup, note
@currentTimeouts.push setTimeout(f, note.delay)
else
@publishNote(note)
sendDelayedNote: (noteGroup, note) ->
# some events should only happen after the bot has moved into position
return unless noteGroup is @currentNoteGroup
@publishNote(note)
publishNote: (note) ->
Backbone.Mediator.publish 'playback:real-time-playback-ended', {} unless @session.get('heroConfig') # Only old levels need this, to stop interfering with old victory coolcams.
Backbone.Mediator.publish note.channel, note.event ? {}
# ENDING NOTES
onLevelRestarted: ->
@quiet = true
@endAll({force:true})
@initProperties()
@resetThings()
Backbone.Mediator.publish 'script:reset', {}
@quiet = false
@run()
onEndNoteGroup: (e) ->
# press enter
return unless @currentNoteGroup?.script.skippable
@endNoteGroup()
@run()
endNoteGroup: ->
return if @ending # kill infinite loops right here
@ending = true
return unless @currentNoteGroup?
console.debug "SCRIPT: Ending note group '#{@currentNoteGroup.name}'" if @debugScripts
clearTimeout(timeout) for timeout in @currentTimeouts
for module in @currentNoteGroup.modules
@processNote(note, @currentNoteGroup) for note in module.endNotes()
Backbone.Mediator.publish 'script:note-group-ended', {} unless @quiet
@scriptInProgress = false
@trackScriptCompletionsFromNoteGroup(@currentNoteGroup)
@currentNoteGroup = null
unless @noteGroupQueue.length
@notifyScriptStateChanged()
@resetThings()
@ending = false
onEndAll: (e) ->
# Escape was pressed.
@endAll()
endAll: (options) ->
options ?= {}
if @scriptInProgress
return if (not @currentNoteGroup.script.skippable) and (not options.force)
@endNoteGroup()
for noteGroup, i in @noteGroupQueue
if ((noteGroup.script?.skippable) is false) and not options.force
@noteGroupQueue = @noteGroupQueue[i..]
@run()
@notifyScriptStateChanged()
return
@processNoteGroup(noteGroup)
for module in noteGroup.modules
notes = module.skipNotes()
@processNote(note, noteGroup) for note in notes unless @quiet
@trackScriptCompletionsFromNoteGroup(noteGroup) unless @quiet
@noteGroupQueue = []
@resetThings()
@notifyScriptStateChanged()
onNoteGroupTimeout: (noteGroup) ->
return unless noteGroup is @currentNoteGroup
@endNoteGroup()
@run()
resetThings: ->
Backbone.Mediator.publish 'level:enable-controls', {}
Backbone.Mediator.publish 'level:set-letterbox', { on: false }
trackScriptCompletionsFromNoteGroup: (noteGroup) ->
return unless noteGroup.isLast
@trackScriptCompletions(noteGroup.scriptID)
trackScriptCompletions: (scriptID) ->
return if @quiet
@ended.push(scriptID) unless scriptID in @ended
for script in @scripts
if script.id is scriptID
script.lastEnded = new Date()
@lastScriptEnded = new Date()
Backbone.Mediator.publish 'script:ended', {scriptID: scriptID}
notifyScriptStateChanged: ->
return if @quiet
event =
currentScript: @currentNoteGroup?.scriptID or null
currentScriptOffset: @currentNoteGroup?.index or 0
Backbone.Mediator.publish 'script:state-changed', event