codecombat/app/lib/world/world.coffee
Nick Winter d77625bc77 Game dev levels (#3810)
* Tweak API doc behavior and styling

* Instead of moving to the left during active dialogues, just move to the top
* Allow pointer events
* Adjust close button
* Re-enable pinning API docs for game-dev and web-dev levels

* Make sidebar in PlayGameDevLevelView stretch, better layout columns

* Set up content of PlayGameDevLevelView sidebar to scroll

* Add rest of PlayGameDevLevelView sidebar content, rework what loading looks like

* Finish PlayGameDevLevelView

* Add share area below
* Cover the brown background, paint it gray

* Tweak PlayGameDevLevelView

* Have progress bar show everything
* Fix Surface resize handling

* Fix PlayGameDevLevelView resizing incorrectly when playing

* Add GameDevVictoryModal to PlayGameDevLevelView

* Don't show missing-doctype annotation in Ace

* Hook up GameDevVictoryModal copy button

* Fix onChangeAnnotation runtime error

* Fix onLevelLoaded runtime error

* Have CourseVictoryModal link to /courses when course is done

* Trim, update CourseDetailsView

* Remove last vestiges of teacherMode
* Remove giant navigation buttons at top
* Quick switch to flat style

* Add analytics for game-dev

* Update Analytics events for gamedev

* Prefix event names with context
* Send to Mixpanel
* Include more properties

* Mostly set up indefinite play and autocast for game-dev levels

* Set up cast buttons and shortcut for game-dev

* Add rudimentary instructions when students play game-dev levels

* Couple tweaks

* fix a bit of code that expects frames to always stick around
* have PlayGameDevLevelView render a couple frames on load

* API Docs use 'game' instead of 'hero'

* Move tags to head without combining

* Add HTML comment-start string

Fixes missing entry point arrows

* Fix some whitespace
2016-07-28 13:39:58 -07:00

656 lines
29 KiB
CoffeeScript

Vector = require './vector'
Rectangle = require './rectangle'
Ellipse = require './ellipse'
LineSegment = require './line_segment'
WorldFrame = require './world_frame'
Thang = require './thang'
ThangState = require './thang_state'
Rand = require './rand'
WorldScriptNote = require './world_script_note'
{now, consolidateThangs, typedArraySupport} = require './world_utils'
Component = require 'lib/world/component'
System = require 'lib/world/system'
PROGRESS_UPDATE_INTERVAL = 100
DESERIALIZATION_INTERVAL = 10
REAL_TIME_BUFFER_MIN = 2 * PROGRESS_UPDATE_INTERVAL
REAL_TIME_BUFFER_MAX = 3 * PROGRESS_UPDATE_INTERVAL
REAL_TIME_BUFFERED_WAIT_INTERVAL = 0.5 * PROGRESS_UPDATE_INTERVAL
REAL_TIME_COUNTDOWN_DELAY = 3000 # match CountdownScreen
ITEM_ORIGINAL = '53e12043b82921000051cdf9'
EXISTS_ORIGINAL = '524b4150ff92f1f4f8000024'
COUNTDOWN_LEVELS = ['sky-span']
module.exports = class World
@className: 'World'
age: 0
ended: false
preloading: false # Whether we are just preloading a world in case we soon cast it
debugging: false # Whether we are just rerunning to debug a world we've already cast
headless: false # Whether we are just simulating for goal states instead of all serialized results
framesSerializedSoFar: 0
framesClearedSoFar: 0
apiProperties: ['age', 'dt']
realTimeBufferMax: REAL_TIME_BUFFER_MAX / 1000
constructor: (@userCodeMap, classMap) ->
# classMap is needed for deserializing Worlds, Thangs, and other classes
@classMap = classMap ? {Vector: Vector, Rectangle: Rectangle, Thang: Thang, Ellipse: Ellipse, LineSegment: LineSegment}
Thang.resetThangIDs()
@userCodeMap ?= {}
@thangs = []
@thangMap = {}
@systems = []
@systemMap = {}
@scriptNotes = []
@rand = new Rand 0 # Existence System may change this seed
@frames = [new WorldFrame(@, 0)]
destroy: ->
@goalManager?.destroy()
thang.destroy() for thang in @thangs
@[key] = undefined for key of @
@destroyed = true
@destroy = ->
getFrame: (frameIndex) ->
# Optimize it a bit--assume we have all if @ended and are at the previous frame otherwise
frames = @frames
if @ended
frame = frames[frameIndex]
else if frameIndex
frame = frames[frameIndex - 1].getNextFrame()
frames.push frame
else
frame = frames[0]
@age = frameIndex * @dt
frame
getThangByID: (id) ->
@thangMap[id]
setThang: (thang) ->
thang.stateChanged = true
for old, i in @thangs
if old.id is thang.id
@thangs[i] = thang
break
@thangMap[thang.id] = thang
thangDialogueSounds: (startFrame=0) ->
return [] unless startFrame < @frames.length
[sounds, seen] = [[], {}]
for frameIndex in [startFrame ... @frames.length]
frame = @frames[frameIndex]
for thangID, state of frame.thangStateMap
continue unless state.thang.say and sayMessage = state.getStateForProp 'sayMessage'
soundKey = state.thang.spriteName + ':' + sayMessage
unless seen[soundKey]
sounds.push [state.thang.spriteName, sayMessage]
seen[soundKey] = true
sounds
setGoalManager: (@goalManager) ->
addError: (error) ->
(@runtimeErrors ?= []).push error
(@unhandledRuntimeErrors ?= []).push error
loadFrames: (loadedCallback, errorCallback, loadProgressCallback, preloadedCallback, skipDeferredLoading, loadUntilFrame) ->
return if @aborted
@totalFrames = 2 if @justBegin
console.log 'Warning: loadFrames called on empty World (no thangs).' unless @thangs.length
continueLaterFn = =>
@loadFrames(loadedCallback, errorCallback, loadProgressCallback, preloadedCallback, skipDeferredLoading, loadUntilFrame) unless @destroyed
if @realTime and not @countdownFinished
@realTimeSpeedFactor = 1
unless @showsCountdown
if @levelID in ['woodland-cleaver', 'village-guard', 'shield-rush']
@realTimeSpeedFactor = 2
else if @levelID in ['thornbush-farm', 'back-to-back', 'ogre-encampment', 'peasant-protection', 'munchkin-swarm', 'munchkin-harvest', 'swift-dagger', 'shrapnel', 'arcane-ally', 'touch-of-death', 'bonemender']
@realTimeSpeedFactor = 3
if @showsCountdown
return setTimeout @finishCountdown(continueLaterFn), REAL_TIME_COUNTDOWN_DELAY
else
@finishCountdown continueLaterFn
t1 = now()
@t0 ?= t1
@worldLoadStartTime ?= t1
@lastRealTimeUpdate ?= 0
frameToLoadUntil = if loadUntilFrame then loadUntilFrame + 1 else @totalFrames # Might stop early if debugging.
i = @frames.length
while true
if @indefiniteLength
break if not @realTime # realtime has been stopped
break if @victory? # game won or lost # TODO: give a couple seconds of buffer after victory is set instead of ending instantly
else
break if i >= frameToLoadUntil
break if i >= @totalFrames
return unless @shouldContinueLoading t1, loadProgressCallback, skipDeferredLoading, continueLaterFn
@adjustFlowSettings loadUntilFrame if @debugging
try
@getFrame(i)
++i # Increment this after we have succeeded in getting the frame, otherwise we'll have to do that frame again
catch error
@addError error # Not an Aether.errors.UserCodeError; maybe we can't recover
unless @preloading or @debugging
for error in (@unhandledRuntimeErrors ? [])
return unless errorCallback error # errorCallback tells us whether the error is recoverable
@unhandledRuntimeErrors = []
@finishLoadingFrames loadProgressCallback, loadedCallback, preloadedCallback
finishLoadingFrames: (loadProgressCallback, loadedCallback, preloadedCallback) ->
unless @debugging
@ended = true
system.finish @thangs for system in @systems
if @preloading
preloadedCallback()
else
loadProgressCallback? 1
loadedCallback()
finishCountdown: (continueLaterFn) -> =>
return if @destroyed
@countdownFinished = true
continueLaterFn()
shouldDelayRealTimeSimulation: (t) ->
return false unless @realTime
timeSinceStart = (t - @worldLoadStartTime) * @realTimeSpeedFactor
timeLoaded = @frames.length * @dt * 1000
timeBuffered = timeLoaded - timeSinceStart
timeBuffered > REAL_TIME_BUFFER_MAX * @realTimeSpeedFactor
shouldUpdateRealTimePlayback: (t) ->
return false unless @realTime
return false if @frames.length * @dt is @lastRealTimeUpdate
timeLoaded = @frames.length * @dt * 1000
timeSinceStart = (t - @worldLoadStartTime) * @realTimeSpeedFactor
remainingBuffer = @lastRealTimeUpdate * 1000 - timeSinceStart
remainingBuffer < REAL_TIME_BUFFER_MIN * @realTimeSpeedFactor
shouldContinueLoading: (t1, loadProgressCallback, skipDeferredLoading, continueLaterFn) ->
t2 = now()
chunkSize = @frames.length - @framesSerializedSoFar
simedTime = @frames.length / @frameRate
chunkTime = switch
when simedTime > 15 then 7
when simedTime > 10 then 5
when simedTime > 5 then 3
when simedTime > 2 then 1
else 0.5
bailoutTime = Math.max(2000*chunkTime, 10000)
dt = t2 - t1
if @realTime
shouldUpdateProgress = @shouldUpdateRealTimePlayback t2
shouldDelayRealTimeSimulation = not shouldUpdateProgress and @shouldDelayRealTimeSimulation t2
else
shouldUpdateProgress = (dt > PROGRESS_UPDATE_INTERVAL and (chunkSize / @frameRate >= chunkTime) or dt > bailoutTime)
shouldDelayRealTimeSimulation = false
return true unless shouldUpdateProgress or shouldDelayRealTimeSimulation
# Stop loading frames for now; continue in a moment.
if shouldUpdateProgress
@lastRealTimeUpdate = @frames.length * @dt if @realTime
#console.log 'we think it is now', (t2 - @worldLoadStartTime) / 1000, 'so delivering', @lastRealTimeUpdate
loadProgressCallback? @frames.length / @totalFrames unless @preloading
t1 = t2
if t2 - @t0 > 1000
console.log ' Loaded', @frames.length, 'of', @totalFrames, '(+' + (t2 - @t0).toFixed(0) + 'ms)' unless @realTime
@t0 = t2
if skipDeferredLoading
continueLaterFn()
else
delay = if shouldDelayRealTimeSimulation then REAL_TIME_BUFFERED_WAIT_INTERVAL else 0
setTimeout continueLaterFn, delay
false
adjustFlowSettings: (loadUntilFrame) ->
for thang in @thangs when thang.isProgrammable
userCode = @userCodeMap[thang.id] ? {}
for methodName, aether of userCode
framesToLoadFlowBefore = if methodName is 'plan' or methodName is 'makeBid' then 200 else 1 # Adjust if plan() is taking even longer
aether._shouldSkipFlow = @frames.length < loadUntilFrame - framesToLoadFlowBefore
finalizePreload: (loadedCallback) ->
@preloading = false
loadedCallback() if @ended
abort: ->
@aborted = true
addFlagEvent: (flagEvent) ->
@flagHistory.push flagEvent
addRealTimeInputEvent: (realTimeInputEvent) ->
@realTimeInputEvents.push realTimeInputEvent
loadFromLevel: (level, willSimulate=true) ->
@levelID = level.slug
@levelComponents = level.levelComponents
@thangTypes = level.thangTypes
@loadScriptsFromLevel level
@loadSystemsFromLevel level
@loadThangsFromLevel level, willSimulate
@showsCountdown = @levelID in COUNTDOWN_LEVELS or _.any(@thangs, (t) -> (t.programmableProperties and 'findFlags' in t.programmableProperties) or t.inventory?.flag)
@picoCTFProblem = level.picoCTFProblem if level.picoCTFProblem
if @picoCTFProblem?.instances and not @picoCTFProblem.flag_sha1
@picoCTFProblem = _.merge @picoCTFProblem, @picoCTFProblem.instances[0]
system.start @thangs for system in @systems
loadSystemsFromLevel: (level) ->
# Remove old Systems
@systems = []
@systemMap = {}
# Load new Systems
for levelSystem in level.systems
systemModel = levelSystem.model
config = levelSystem.config
systemClass = @loadClassFromCode systemModel.js, systemModel.name, 'system'
#console.log "using db system class ---\n", systemClass, "\n--- from code ---n", systemModel.js, "\n---"
system = new systemClass @, config
@addSystems system
null
loadThangsFromLevel: (level, willSimulate) ->
# Remove old Thangs
@thangs = []
@thangMap = {}
# Load new Thangs
toAdd = (@loadThangFromLevel thangConfig, level.levelComponents, level.thangTypes for thangConfig in level.thangs ? [])
@extraneousThangs = consolidateThangs toAdd if willSimulate # Combine walls, for example; serialize the leftovers later
@addThang thang for thang in toAdd
null
loadThangFromLevel: (thangConfig, levelComponents, thangTypes, equipBy=null) ->
components = []
for component, componentIndex in thangConfig.components
componentModel = _.find levelComponents, (c) ->
c.original is component.original and c.version.major is (component.majorVersion ? 0)
componentClass = @loadClassFromCode componentModel.js, componentModel.name, 'component'
components.push [componentClass, component.config]
if component.original is ITEM_ORIGINAL
isItem = true
component.config.ownerID = equipBy if equipBy
else if component.original is EXISTS_ORIGINAL
existsConfigIndex = componentIndex
if isItem and existsConfigIndex?
# For memory usage performance, make sure these don't get any tracked properties assigned.
components[existsConfigIndex][1] = {exists: false, stateless: true}
thangTypeOriginal = thangConfig.thangType
thangTypeModel = _.find thangTypes, (t) -> t.original is thangTypeOriginal
return console.error thangConfig.id ? equipBy, 'could not find ThangType for', thangTypeOriginal unless thangTypeModel
thangTypeName = thangTypeModel.name
thang = new Thang @, thangTypeName, thangConfig.id
try
thang.addComponents components...
catch e
console.error 'couldn\'t load components for', thangTypeOriginal, thangConfig.id, 'because', e.toString(), e.stack
thang
addThang: (thang) ->
@thangs.unshift thang # Interactions happen in reverse order of specification/drawing
@setThang thang
@updateThangState thang
thang.updateRegistration()
thang
loadScriptsFromLevel: (level) ->
@scriptNotes = []
@scripts = []
@addScripts level.scripts...
loadClassFromCode: (js, name, kind='component') ->
# Cache them based on source code so we don't have to worry about extra compilations
@componentCodeClassMap ?= {}
@systemCodeClassMap ?= {}
map = if kind is 'component' then @componentCodeClassMap else @systemCodeClassMap
c = map[js]
return c if c
try
c = map[js] = eval js
catch err
console.error "Couldn't compile #{kind} code:", err, "\n", js
c = map[js] = {}
c.className = name
c
updateThangState: (thang) ->
@frames[@frames.length-1].thangStateMap[thang.id] = thang.getState()
size: ->
@calculateBounds() unless @width? and @height?
return [@width, @height] if @width? and @height?
getBounds: ->
@calculateBounds() unless @bounds?
return @bounds
calculateBounds: ->
bounds = {left: 0, top: 0, right: 0, bottom: 0}
hasLand = _.some @thangs, 'isLand'
for thang in @thangs when thang.isLand or (not hasLand and thang.rectangle) # Look at Lands only
rect = thang.rectangle().axisAlignedBoundingBox()
bounds.left = Math.min(bounds.left, rect.x - rect.width / 2)
bounds.right = Math.max(bounds.right, rect.x + rect.width / 2)
bounds.bottom = Math.min(bounds.bottom, rect.y - rect.height / 2)
bounds.top = Math.max(bounds.top, rect.y + rect.height / 2)
@width = bounds.right - bounds.left
@height = bounds.top - bounds.bottom
@bounds = bounds
[@width, @height]
publishNote: (channel, event) ->
event ?= {}
channel = 'world:' + channel
for script in @scripts ? []
continue if script.channel isnt channel
scriptNote = new WorldScriptNote script, event
continue if scriptNote.invalid
@scriptNotes.push scriptNote
return unless @goalManager
@goalManager.submitWorldGenerationEvent(channel, event, @frames.length)
getGoalState: (goalID) ->
@goalManager.getGoalState(goalID)
setGoalState: (goalID, status) ->
@goalManager.setGoalState(goalID, status)
endWorld: (victory=false, delay=3, tentative=false) ->
@totalFrames = Math.min(@totalFrames, @frames.length + Math.floor(delay / @dt)) # end a few seconds later
@victory = victory # TODO: should just make this signify the winning superteam
@victoryIsTentative = tentative
status = if @victory then 'won' else 'lost'
@publishNote status
console.log "The world ended in #{status} on frame #{@totalFrames}"
addSystems: (systems...) ->
@systems = @systems.concat systems
for system in systems
@systemMap[system.constructor.className] = system
getSystem: (systemClassName) ->
@systemMap?[systemClassName]
addScripts: (scripts...) ->
@scripts = (@scripts ? []).concat scripts
addTrackedProperties: (props...) ->
@trackedProperties = (@trackedProperties ? []).concat props
serialize: ->
# Code hotspot; optimize it
@freeMemoryBeforeFinalSerialization() if @ended
startFrame = @framesSerializedSoFar
endFrame = @frames.length
if @indefiniteLength
toClear = Math.max(@framesSerializedSoFar-10, 0)
for i in _.range(@framesClearedSoFar, toClear)
@frames[i] = null
@framesClearedSoFar = @framesSerializedSoFar
#console.log "... world serializing frames from", startFrame, "to", endFrame, "of", @totalFrames
[transferableObjects, nontransferableObjects] = [0, 0]
serializedFlagHistory = (_.omit(_.clone(flag), 'processed') for flag in @flagHistory)
o = {totalFrames: @totalFrames, maxTotalFrames: @maxTotalFrames, frameRate: @frameRate, dt: @dt, victory: @victory, userCodeMap: {}, trackedProperties: {}, flagHistory: serializedFlagHistory, difficulty: @difficulty, scores: @getScores(), randomSeed: @randomSeed, picoCTFFlag: @picoCTFFlag}
o.trackedProperties[prop] = @[prop] for prop in @trackedProperties or []
for thangID, methods of @userCodeMap
serializedMethods = o.userCodeMap[thangID] = {}
for methodName, method of methods
serializedMethods[methodName] = method.serialize?() ? method # serialize the method again if it has been deserialized
t0 = now()
o.trackedPropertiesThangIDs = []
o.trackedPropertiesPerThangIndices = []
o.trackedPropertiesPerThangKeys = []
o.trackedPropertiesPerThangTypes = []
trackedPropertiesPerThangValues = [] # We won't send these, just the offsets and the storage buffer
o.trackedPropertiesPerThangValuesOffsets = [] # Needed to reconstruct ArrayBufferViews on other end, since Firefox has bugs transfering those: https://bugzilla.mozilla.org/show_bug.cgi?id=841904 and https://bugzilla.mozilla.org/show_bug.cgi?id=861925 # Actually, as of January 2014, it should be fixed. So we could try to undo the workaround.
transferableStorageBytesNeeded = 0
nFrames = endFrame - startFrame
for thang in @thangs
# Don't serialize empty trackedProperties for stateless Thangs which haven't changed (like obstacles).
# Check both, since sometimes people mark stateless Thangs but then change them, and those should still be tracked, and the inverse doesn't work on the other end (we'll just think it doesn't exist then).
# If streaming the world, a thang marked stateless that actually change will get messed up. I think.
continue if thang.stateless and not _.some(thang.trackedPropertiesUsed, Boolean)
o.trackedPropertiesThangIDs.push thang.id
trackedPropertiesIndices = []
trackedPropertiesKeys = []
trackedPropertiesTypes = []
for used, propIndex in thang.trackedPropertiesUsed
continue unless used
trackedPropertiesIndices.push propIndex
trackedPropertiesKeys.push thang.trackedPropertiesKeys[propIndex]
trackedPropertiesTypes.push thang.trackedPropertiesTypes[propIndex]
o.trackedPropertiesPerThangIndices.push trackedPropertiesIndices
o.trackedPropertiesPerThangKeys.push trackedPropertiesKeys
o.trackedPropertiesPerThangTypes.push trackedPropertiesTypes
trackedPropertiesPerThangValues.push []
o.trackedPropertiesPerThangValuesOffsets.push []
for type in trackedPropertiesTypes
transferableStorageBytesNeeded += ThangState.transferableBytesNeededForType(type, nFrames)
if typedArraySupport
o.storageBuffer = new ArrayBuffer(transferableStorageBytesNeeded)
else
o.storageBuffer = []
storageBufferOffset = 0
for trackedPropertiesValues, thangIndex in trackedPropertiesPerThangValues
trackedPropertiesValuesOffsets = o.trackedPropertiesPerThangValuesOffsets[thangIndex]
for type, propIndex in o.trackedPropertiesPerThangTypes[thangIndex]
[storage, bytesStored] = ThangState.createArrayForType type, nFrames, o.storageBuffer, storageBufferOffset
trackedPropertiesValues.push storage
trackedPropertiesValuesOffsets.push storageBufferOffset
++transferableObjects if bytesStored
++nontransferableObjects unless bytesStored
if typedArraySupport
storageBufferOffset += bytesStored
else
# Instead of one big array with each storage as a view into it, they're all separate, so let's keep 'em around for flattening.
storageBufferOffset += storage.length
o.storageBuffer.push storage
o.specialKeysToValues = [null, Infinity, NaN]
# Whatever is in specialKeysToValues index 0 will be default for anything missing, so let's make sure it's null.
# Don't think we can include undefined or it'll be treated as a sparse array; haven't tested performance.
o.specialValuesToKeys = {}
for specialValue, i in o.specialKeysToValues
o.specialValuesToKeys[specialValue] = i
t1 = now()
o.frameHashes = []
for frameIndex in [startFrame ... endFrame]
o.frameHashes.push @frames[frameIndex].serialize(frameIndex - startFrame, o.trackedPropertiesThangIDs, o.trackedPropertiesPerThangIndices, o.trackedPropertiesPerThangTypes, trackedPropertiesPerThangValues, o.specialValuesToKeys, o.specialKeysToValues)
t2 = now()
unless typedArraySupport
flattened = []
for storage in o.storageBuffer
for value in storage
flattened.push value
o.storageBuffer = flattened
#console.log 'Allocating memory:', (t1 - t0).toFixed(0), 'ms; assigning values:', (t2 - t1).toFixed(0), 'ms, so', ((t2 - t1) / nFrames).toFixed(3), 'ms per frame for', nFrames, 'frames'
#console.log 'Got', transferableObjects, 'transferable objects and', nontransferableObjects, 'nontransferable; stored', transferableStorageBytesNeeded, 'bytes transferably'
o.thangs = (t.serialize() for t in @thangs.concat(@extraneousThangs ? []))
o.scriptNotes = (sn.serialize() for sn in @scriptNotes)
if o.scriptNotes.length > 200
console.log 'Whoa, serializing a lot of WorldScriptNotes here:', o.scriptNotes.length
@freeMemoryAfterEachSerialization() unless @ended
{serializedWorld: o, transferableObjects: [o.storageBuffer], startFrame: startFrame, endFrame: endFrame}
@deserialize: (o, classMap, oldSerializedWorldFrames, finishedWorldCallback, startFrame, endFrame, level, streamingWorld) ->
# Code hotspot; optimize it
#console.log 'Deserializing', o, 'length', JSON.stringify(o).length
#console.log JSON.stringify(o)
#console.log 'Got special keys and values:', o.specialValuesToKeys, o.specialKeysToValues
perf = {}
perf.t0 = now()
nFrames = endFrame - startFrame
if streamingWorld
w = streamingWorld
# Make sure we get any Aether updates from the new frames into the already-deserialized streaming world Aethers.
for thangID, methods of o.userCodeMap
for methodName, serializedAether of methods
for aetherStateKey in ['flow', 'metrics', 'style', 'problems']
w.userCodeMap[thangID] ?= {}
w.userCodeMap[thangID][methodName] ?= {}
w.userCodeMap[thangID][methodName][aetherStateKey] = serializedAether[aetherStateKey]
else
w = new World o.userCodeMap, classMap
[w.totalFrames, w.maxTotalFrames, w.frameRate, w.dt, w.scriptNotes, w.victory, w.flagHistory, w.difficulty, w.scores, w.randomSeed, w.picoCTFFlag] = [o.totalFrames, o.maxTotalFrames, o.frameRate, o.dt, o.scriptNotes ? [], o.victory, o.flagHistory, o.difficulty, o.scores, o.randomSeed, o.picoCTFFlag]
w[prop] = val for prop, val of o.trackedProperties
perf.t1 = now()
if w.thangs.length
for thangConfig in o.thangs
if thang = w.thangMap[thangConfig.id]
for prop, val of thangConfig.finalState
thang[prop] = val
else
w.thangs.push thang = Thang.deserialize(thangConfig, w, classMap, level.levelComponents)
w.setThang thang
else
w.thangs = (Thang.deserialize(thang, w, classMap, level.levelComponents) for thang in o.thangs)
w.setThang thang for thang in w.thangs
w.scriptNotes = (WorldScriptNote.deserialize(sn, w, classMap) for sn in o.scriptNotes)
perf.t2 = now()
o.trackedPropertiesThangs = (w.getThangByID thangID for thangID in o.trackedPropertiesThangIDs)
o.trackedPropertiesPerThangValues = []
for trackedPropertyTypes, thangIndex in o.trackedPropertiesPerThangTypes
o.trackedPropertiesPerThangValues.push (trackedPropertiesValues = [])
trackedPropertiesValuesOffsets = o.trackedPropertiesPerThangValuesOffsets[thangIndex]
for type, propIndex in trackedPropertyTypes
storage = ThangState.createArrayForType(type, nFrames, o.storageBuffer, trackedPropertiesValuesOffsets[propIndex])[0]
unless typedArraySupport
# This could be more efficient
i = trackedPropertiesValuesOffsets[propIndex]
storage = o.storageBuffer.slice i, i + storage.length
trackedPropertiesValues.push storage
perf.t3 = now()
perf.batches = 0
perf.framesCPUTime = 0
w.frames = [] unless streamingWorld
clearTimeout @deserializationTimeout if @deserializationTimeout
if w.indefiniteLength
clearTo = Math.max(w.frames.length - 100, 0)
if clearTo > w.framesClearedSoFar
for i in _.range(w.framesClearedSoFar, clearTo)
w.frames[i] = null
w.framesClearedSoFar = clearTo
@deserializationTimeout = _.delay @deserializeSomeFrames, 1, o, w, finishedWorldCallback, perf, startFrame, endFrame
w # Return in-progress deserializing world
# Spread deserialization out across multiple calls so the interface stays responsive
@deserializeSomeFrames: (o, w, finishedWorldCallback, perf, startFrame, endFrame) =>
++perf.batches
startTime = now()
for frameIndex in [w.frames.length ... endFrame]
w.frames.push WorldFrame.deserialize(w, frameIndex - startFrame, o.trackedPropertiesThangIDs, o.trackedPropertiesThangs, o.trackedPropertiesPerThangKeys, o.trackedPropertiesPerThangTypes, o.trackedPropertiesPerThangValues, o.specialKeysToValues, o.frameHashes[frameIndex - startFrame], w.dt * frameIndex)
elapsed = now() - startTime
if elapsed > DESERIALIZATION_INTERVAL and frameIndex < endFrame - 1
#console.log " Deserialization not finished, let's do it again soon. Have:", w.frames.length, ", wanted from", startFrame, "to", endFrame
perf.framesCPUTime += elapsed
@deserializationTimeout = _.delay @deserializeSomeFrames, 1, o, w, finishedWorldCallback, perf, startFrame, endFrame
return
@deserializationTimeout = null
perf.framesCPUTime += elapsed
@finishDeserializing w, finishedWorldCallback, perf, startFrame, endFrame
@finishDeserializing: (w, finishedWorldCallback, perf, startFrame, endFrame) ->
perf.t4 = now()
w.ended = true
nFrames = endFrame - startFrame
totalCPUTime = perf.t3 - perf.t0 + perf.framesCPUTime
#console.log 'Deserialization:', totalCPUTime.toFixed(0) + 'ms (' + (totalCPUTime / nFrames).toFixed(3) + 'ms per frame).', perf.batches, 'batches. Did', startFrame, 'to', endFrame, 'in', (perf.t4 - perf.t0).toFixed(0) + 'ms wall clock time.'
if false
console.log ' Deserializing--constructing new World:', (perf.t1 - perf.t0).toFixed(2) + 'ms'
console.log ' Deserializing--Thangs and ScriptNotes:', (perf.t2 - perf.t1).toFixed(2) + 'ms'
console.log ' Deserializing--reallocating memory:', (perf.t3 - perf.t2).toFixed(2) + 'ms'
console.log ' Deserializing--WorldFrames:', (perf.t4 - perf.t3).toFixed(2) + 'ms wall clock time,', (perf.framesCPUTime).toFixed(2) + 'ms CPU time'
finishedWorldCallback w
findFirstChangedFrame: (oldWorld) ->
return 0 unless oldWorld
for newFrame, i in @frames
oldFrame = oldWorld.frames[i]
break unless oldFrame and ((newFrame.hash is oldFrame.hash) or not newFrame.hash? or not oldFrame.hash?) # undefined gets in there when streaming at the last frame of each batch for some reason
firstChangedFrame = i
if @frames.length is @totalFrames
if @frames[i]
console.log 'First changed frame is', firstChangedFrame, 'with hash', @frames[i].hash, 'compared to', oldWorld.frames[i]?.hash
else
console.log 'No frames were changed out of all', @frames.length
firstChangedFrame
freeMemoryBeforeFinalSerialization: ->
@levelComponents = null
@thangTypes = null
freeMemoryAfterEachSerialization: ->
@frames[i] = null for frame, i in @frames when i < @frames.length - 1
pointsForThang: (thangID, camera=null) ->
# Optimized
@pointsForThangCache ?= {}
cacheKey = thangID
allPoints = @pointsForThangCache[cacheKey]
unless allPoints
allPoints = []
lastFrameIndex = @frames.length - 1
lastPos = x: null, y: null
for frameIndex in [lastFrameIndex .. 0] by -1
frame = @frames[frameIndex]
continue unless frame # may have been evicted for game dev levels
if pos = frame.thangStateMap[thangID]?.getStateForProp 'pos'
pos = camera.worldToSurface {x: pos.x, y: pos.y} if camera # without z
if not lastPos.x? or (Math.abs(lastPos.x - pos.x) + Math.abs(lastPos.y - pos.y)) > 1
lastPos = pos
allPoints.push lastPos.y, lastPos.x unless lastPos.y is 0 and lastPos.x is 0
allPoints.reverse()
@pointsForThangCache[cacheKey] = allPoints
return allPoints
actionsForThang: (thangID, keepIdle=false) ->
# Optimized
@actionsForThangCache ?= {}
cacheKey = thangID + '_' + Boolean(keepIdle)
cached = @actionsForThangCache[cacheKey]
return cached if cached
states = (frame.thangStateMap[thangID] for frame in @frames)
actions = []
lastAction = ''
for state, i in states
action = state?.getStateForProp 'action'
continue unless action and (action isnt lastAction or state.actionActivated)
continue unless state.action isnt 'idle' or keepIdle
actions.push {frame: i, pos: state.pos, name: action}
lastAction = action
@actionsForThangCache[cacheKey] = actions
return actions
getTeamColors: ->
teamConfigs = @teamConfigs or {}
colorConfigs = {}
colorConfigs[teamName] = config.color for teamName, config of teamConfigs
colorConfigs
teamForPlayer: (n) ->
playableTeams = @playableTeams ? ['humans']
playableTeams[n % playableTeams.length]
getScores: ->
time: @age
'damage-taken': @getSystem('Combat')?.damageTakenForTeam 'humans'
'damage-dealt': @getSystem('Combat')?.damageDealtForTeam 'humans'
'gold-collected': @getSystem('Inventory')?.teamGold.humans?.collected
'difficulty': @difficulty