codecombat/app/lib/world/world.coffee

522 lines
22 KiB
CoffeeScript
Raw Normal View History

2014-01-03 13:32:13 -05:00
Vector = require './vector'
Rectangle = require './rectangle'
2014-07-16 18:57:53 -04:00
Ellipse = require './ellipse'
LineSegment = require './line_segment'
2014-01-03 13:32:13 -05:00
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'
2014-08-21 19:27:52 -04:00
PROGRESS_UPDATE_INTERVAL = 100
2014-01-03 13:32:13 -05:00
DESERIALIZATION_INTERVAL = 20
ITEM_ORIGINAL = '53e12043b82921000051cdf9'
2014-01-03 13:32:13 -05:00
module.exports = class World
2014-06-30 22:16:26 -04:00
@className: 'World'
2014-01-31 19:16:59 -05:00
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
2014-08-21 19:27:52 -04:00
framesSerializedSoFar: 0
2014-01-31 19:16:59 -05:00
apiProperties: ['age', 'dt']
constructor: (@userCodeMap, classMap) ->
2014-01-03 13:32:13 -05:00
# classMap is needed for deserializing Worlds, Thangs, and other classes
2014-07-16 18:57:53 -04:00
@classMap = classMap ? {Vector: Vector, Rectangle: Rectangle, Thang: Thang, Ellipse: Ellipse, LineSegment: LineSegment}
2014-01-03 13:32:13 -05:00
Thang.resetThangIDs()
@userCodeMap ?= {}
@thangs = []
@thangMap = {}
@systems = []
@systemMap = {}
@scriptNotes = []
@rand = new Rand 0 # Existence System may change this seed
2014-01-03 13:32:13 -05:00
@frames = [new WorldFrame(@, 0)]
2014-05-22 22:05:05 -04:00
destroy: ->
@goalManager?.destroy()
thang.destroy() for thang in @thangs
@[key] = undefined for key of @
@destroyed = true
@destroy = ->
2014-01-03 13:32:13 -05:00
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) ->
for old, i in @thangs
if old.id is thang.id
@thangs[i] = thang
@thangMap[thang.id] = thang
thangDialogueSounds: ->
[sounds, seen] = [[], {}]
for frame in @frames
for thangID, state of frame.thangStateMap
2014-06-30 22:16:26 -04:00
continue unless state.thang.say and sayMessage = state.getStateForProp 'sayMessage'
soundKey = state.thang.spriteName + ':' + sayMessage
2014-01-03 13:32:13 -05:00
unless seen[soundKey]
sounds.push [state.thang.spriteName, sayMessage]
seen[soundKey] = true
sounds
setGoalManager: (@goalManager) ->
addError: (error) ->
(@runtimeErrors ?= []).push error
(@unhandledRuntimeErrors ?= []).push error
2014-05-09 17:48:43 -04:00
loadFrames: (loadedCallback, errorCallback, loadProgressCallback, skipDeferredLoading, loadUntilFrame) ->
2014-01-03 13:32:13 -05:00
return if @aborted
unless @thangs.length
2014-06-30 22:16:26 -04:00
console.log 'Warning: loadFrames called on empty World (no thangs).'
2014-01-03 13:32:13 -05:00
t1 = now()
@t0 ?= t1
2014-05-09 18:05:50 -04:00
if loadUntilFrame
2014-05-09 17:48:43 -04:00
frameToLoadUntil = loadUntilFrame + 1
else
frameToLoadUntil = @totalFrames
2014-01-03 13:32:13 -05:00
i = @frames.length
2014-05-09 17:48:43 -04:00
while i < frameToLoadUntil
if @debugging
for thang in @thangs when thang.isProgrammable
userCode = @userCodeMap[thang.id] ? {}
for methodName, aether of userCode
2014-06-19 20:27:41 -04:00
framesToLoadFlowBefore = if methodName is 'plan' or methodName is 'makeBid' then 200 else 1 # Adjust if plan() is taking even longer
aether._shouldSkipFlow = i < loadUntilFrame - framesToLoadFlowBefore
2014-01-03 13:32:13 -05:00
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
# Not an Aether.errors.UserCodeError; maybe we can't recover
@addError error
unless @preloading or @debugging
for error in (@unhandledRuntimeErrors ? [])
return unless errorCallback error # errorCallback tells us whether the error is recoverable
@unhandledRuntimeErrors = []
2014-01-03 13:32:13 -05:00
t2 = now()
if t2 - t1 > PROGRESS_UPDATE_INTERVAL
loadProgressCallback? i / @totalFrames unless @preloading
2014-01-03 13:32:13 -05:00
t1 = t2
if t2 - @t0 > 1000
2014-06-30 22:16:26 -04:00
console.log ' Loaded', i, 'of', @totalFrames, '(+' + (t2 - @t0).toFixed(0) + 'ms)'
2014-01-03 13:32:13 -05:00
@t0 = t2
continueFn = =>
2014-05-22 22:05:05 -04:00
return if @destroyed
2014-05-09 17:48:43 -04:00
if loadUntilFrame
@loadFrames(loadedCallback,errorCallback,loadProgressCallback, skipDeferredLoading, loadUntilFrame)
else
@loadFrames(loadedCallback, errorCallback, loadProgressCallback, skipDeferredLoading)
if skipDeferredLoading
continueFn()
else
setTimeout(continueFn, 0)
2014-01-03 13:32:13 -05:00
return
unless @debugging
2014-05-09 17:48:43 -04:00
@ended = true
system.finish @thangs for system in @systems
unless @preloading
loadProgressCallback? 1
loadedCallback()
finalizePreload: (loadedCallback) ->
@preloading = false
loadedCallback() if @ended
2014-01-03 13:32:13 -05:00
abort: ->
@aborted = true
loadFromLevel: (level, willSimulate=true) ->
@levelComponents = level.levelComponents
@thangTypes = level.thangTypes
2014-01-03 13:32:13 -05:00
@loadSystemsFromLevel level
@loadThangsFromLevel level, willSimulate
@loadScriptsFromLevel level
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
2014-06-30 22:16:26 -04:00
systemClass = @loadClassFromCode systemModel.js, systemModel.name, 'system'
2014-01-03 13:32:13 -05:00
#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
2014-01-03 13:32:13 -05:00
null
loadThangFromLevel: (thangConfig, levelComponents, thangTypes, equipBy=null) ->
components = []
for component 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 equipBy and component.original is ITEM_ORIGINAL
component.config.ownerID = equipBy
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
2014-01-03 13:32:13 -05:00
loadScriptsFromLevel: (level) ->
@scriptNotes = []
@scripts = []
@addScripts level.scripts...
2014-06-30 22:16:26 -04:00
loadClassFromCode: (js, name, kind='component') ->
2014-01-03 13:32:13 -05:00
# Cache them based on source code so we don't have to worry about extra compilations
@componentCodeClassMap ?= {}
@systemCodeClassMap ?= {}
2014-06-30 22:16:26 -04:00
map = if kind is 'component' then @componentCodeClassMap else @systemCodeClassMap
2014-01-03 13:32:13 -05:00
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] = {}
2014-01-03 13:32:13 -05:00
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
2014-01-03 13:32:13 -05:00
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
2014-01-03 13:32:13 -05:00
continue if scriptNote.invalid
@scriptNotes.push scriptNote
return unless @goalManager
@goalManager.submitWorldGenerationEvent(channel, event, @frames.length)
2014-05-13 13:43:39 -04:00
getGoalState: (goalID) ->
@goalManager.getGoalState(goalID)
2014-03-28 21:54:16 -04:00
setGoalState: (goalID, status) ->
@goalManager.setGoalState(goalID, status)
2014-01-03 13:32:13 -05:00
endWorld: (victory=false, delay=3, tentative=false) ->
2014-05-12 11:35:46 -04:00
@totalFrames = Math.min(@totalFrames, @frames.length + Math.floor(delay / @dt)) # end a few seconds later
2014-01-03 13:32:13 -05:00
@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
2014-01-13 19:58:17 -05:00
addTrackedProperties: (props...) ->
@trackedProperties = (@trackedProperties ? []).concat props
2014-08-22 00:23:45 -04:00
serialize: ->
2014-01-03 13:32:13 -05:00
# Code hotspot; optimize it
2014-08-22 00:23:45 -04:00
startFrame = @framesSerializedSoFar
endFrame = @frames.length
2014-08-21 19:27:52 -04:00
console.log "... world serializing frames from", startFrame, "to", endFrame
2014-01-03 13:32:13 -05:00
[transferableObjects, nontransferableObjects] = [0, 0]
o = {totalFrames: @totalFrames, maxTotalFrames: @maxTotalFrames, frameRate: @frameRate, dt: @dt, victory: @victory, userCodeMap: {}, trackedProperties: {}}
2014-01-14 17:03:55 -05:00
o.trackedProperties[prop] = @[prop] for prop in @trackedProperties or []
2014-01-13 19:58:17 -05:00
2014-01-03 13:32:13 -05:00
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
2014-01-03 13:32:13 -05:00
t0 = now()
o.trackedPropertiesThangIDs = []
o.trackedPropertiesPerThangIndices = []
o.trackedPropertiesPerThangKeys = []
o.trackedPropertiesPerThangTypes = []
trackedPropertiesPerThangValues = [] # We won't send these, just the offsets and the storage buffer
2014-08-21 19:27:52 -04:00
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.
2014-01-03 13:32:13 -05:00
transferableStorageBytesNeeded = 0
2014-08-21 19:27:52 -04:00
nFrames = endFrame - startFrame
streaming = nFrames < @totalFrames
2014-01-03 13:32:13 -05:00
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 don't 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).
2014-08-22 00:23:45 -04:00
continue if thang.stateless and not _.some(thang.trackedPropertiesUsed, Boolean) and not streaming
2014-01-03 13:32:13 -05:00
o.trackedPropertiesThangIDs.push thang.id
trackedPropertiesIndices = []
trackedPropertiesKeys = []
trackedPropertiesTypes = []
for used, propIndex in thang.trackedPropertiesUsed
2014-08-21 20:30:46 -04:00
continue unless used# or streaming
2014-01-03 13:32:13 -05:00
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 = []
2014-08-21 19:27:52 -04:00
for frameIndex in [startFrame ... endFrame]
2014-08-21 20:30:46 -04:00
o.frameHashes.push @frames[frameIndex].serialize(frameIndex - startFrame, o.trackedPropertiesThangIDs, o.trackedPropertiesPerThangIndices, o.trackedPropertiesPerThangTypes, trackedPropertiesPerThangValues, o.specialValuesToKeys, o.specialKeysToValues)
2014-01-03 13:32:13 -05:00
t2 = now()
unless typedArraySupport
flattened = []
for storage in o.storageBuffer
for value in storage
flattened.push value
o.storageBuffer = flattened
2014-08-21 19:27:52 -04:00
#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'
2014-06-30 22:16:26 -04:00
#console.log 'Got', transferableObjects, 'transferable objects and', nontransferableObjects, 'nontransferable; stored', transferableStorageBytesNeeded, 'bytes transferably'
2014-01-03 13:32:13 -05:00
o.thangs = (t.serialize() for t in @thangs.concat(@extraneousThangs ? []))
o.scriptNotes = (sn.serialize() for sn in @scriptNotes)
if o.scriptNotes.length > 200
2014-06-30 22:16:26 -04:00
console.log 'Whoa, serializing a lot of WorldScriptNotes here:', o.scriptNotes.length
2014-08-21 19:27:52 -04:00
{serializedWorld: o, transferableObjects: [o.storageBuffer], startFrame: startFrame, endFrame: endFrame}
2014-01-03 13:32:13 -05:00
2014-08-21 19:27:52 -04:00
@deserialize: (o, classMap, oldSerializedWorldFrames, finishedWorldCallback, startFrame, endFrame, streamingWorld) ->
2014-01-03 13:32:13 -05:00
# Code hotspot; optimize it
2014-06-30 22:16:26 -04:00
#console.log 'Deserializing', o, 'length', JSON.stringify(o).length
2014-01-03 13:32:13 -05:00
#console.log JSON.stringify(o)
2014-06-30 22:16:26 -04:00
#console.log 'Got special keys and values:', o.specialValuesToKeys, o.specialKeysToValues
2014-01-03 13:32:13 -05:00
perf = {}
perf.t0 = now()
2014-08-21 19:27:52 -04:00
nFrames = endFrame - startFrame
w = streamingWorld ? new World o.userCodeMap, classMap
2014-01-03 13:32:13 -05:00
[w.totalFrames, w.maxTotalFrames, w.frameRate, w.dt, w.scriptNotes, w.victory] = [o.totalFrames, o.maxTotalFrames, o.frameRate, o.dt, o.scriptNotes ? [], o.victory]
2014-01-14 17:03:55 -05:00
w[prop] = val for prop, val of o.trackedProperties
2014-01-03 13:32:13 -05:00
perf.t1 = now()
2014-08-21 20:30:46 -04:00
if w.thangs.length
2014-08-22 00:23:45 -04:00
for thangConfig in o.thangs when not w.thangMap[thangConfig.id]
w.thangs.push thang = Thang.deserialize(thangConfig, w, classMap)
2014-08-21 20:30:46 -04:00
w.setThang thang
else
w.thangs = (Thang.deserialize(thang, w, classMap) for thang in o.thangs)
w.setThang thang for thang in w.thangs
2014-01-03 13:32:13 -05:00
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
2014-08-21 19:27:52 -04:00
storage = ThangState.createArrayForType(type, nFrames, o.storageBuffer, trackedPropertiesValuesOffsets[propIndex])[0]
2014-01-03 13:32:13 -05:00
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
2014-08-21 19:27:52 -04:00
w.frames = [] unless streamingWorld
clearTimeout @deserializationTimeout if @deserializationTimeout
@deserializationTimeout = _.delay @deserializeSomeFrames, 1, o, w, finishedWorldCallback, perf, startFrame, endFrame
2014-01-03 13:32:13 -05:00
# Spread deserialization out across multiple calls so the interface stays responsive
2014-08-21 19:27:52 -04:00
@deserializeSomeFrames: (o, w, finishedWorldCallback, perf, startFrame, endFrame) =>
2014-01-03 13:32:13 -05:00
++perf.batches
startTime = now()
2014-08-21 19:27:52 -04:00
for frameIndex in [w.frames.length ... endFrame]
2014-08-21 20:30:46 -04:00
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)
2014-01-03 13:32:13 -05:00
if (now() - startTime) > DESERIALIZATION_INTERVAL
2014-08-21 19:27:52 -04:00
console.log " Deserialization not finished, let's do it again soon. Have:", w.frames.length, ", wanted from", startFrame, "to", endFrame
@deserializationTimeout = _.delay @deserializeSomeFrames, 1, o, w, finishedWorldCallback, perf, startFrame, endFrame
2014-01-03 13:32:13 -05:00
return
2014-08-21 19:27:52 -04:00
@deserializationTimeout = null
@finishDeserializing w, finishedWorldCallback, perf, startFrame, endFrame
2014-01-03 13:32:13 -05:00
2014-08-21 19:27:52 -04:00
@finishDeserializing: (w, finishedWorldCallback, perf, startFrame, endFrame) ->
2014-01-03 13:32:13 -05:00
perf.t4 = now()
2014-08-21 19:27:52 -04:00
nFrames = endFrame - startFrame
2014-01-03 13:32:13 -05:00
w.ended = true
2014-08-21 19:27:52 -04:00
w.getFrame(endFrame - 1).restoreState()
2014-01-03 13:32:13 -05:00
perf.t5 = now()
2014-08-21 19:27:52 -04:00
console.log 'Deserialization:', (perf.t5 - perf.t0).toFixed(0) + 'ms (' + ((perf.t5 - perf.t0) / nFrames).toFixed(3) + 'ms per frame).', perf.batches, 'batches.'
2014-01-03 13:32:13 -05:00
if false
2014-06-30 22:16:26 -04:00
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'
console.log ' Deserializing--restoring last WorldFrame:', (perf.t5 - perf.t4).toFixed(2) + 'ms'
2014-01-03 13:32:13 -05:00
finishedWorldCallback w
findFirstChangedFrame: (oldWorld) ->
2014-08-22 00:23:45 -04:00
return 0 unless oldWorld
2014-01-03 13:32:13 -05:00
for newFrame, i in @frames
oldFrame = oldWorld.frames[i]
2014-08-22 00:23:45 -04:00
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
2014-01-03 13:32:13 -05:00
if @frames[i]
2014-08-22 00:23:45 -04:00
console.log 'First changed frame is', firstChangedFrame, 'with hash', @frames[i].hash, 'compared to', oldWorld.frames[i]?.hash
2014-01-03 13:32:13 -05:00
else
2014-06-30 22:16:26 -04:00
console.log 'No frames were changed out of all', @frames.length
2014-08-22 00:23:45 -04:00
firstChangedFrame
2014-01-03 13:32:13 -05:00
pointsForThang: (thangID, frameStart=0, frameEnd=null, camera=null, resolution=4) ->
# 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]
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
2014-01-03 13:32:13 -05:00
allPoints.reverse()
@pointsForThangCache[cacheKey] = allPoints
points = []
[lastX, lastY] = [null, null]
for frameIndex in [Math.floor(frameStart / resolution) ... Math.ceil(frameEnd / resolution)]
x = allPoints[frameIndex * 2 * resolution]
y = allPoints[frameIndex * 2 * resolution + 1]
continue if x is lastX and y is lastY
lastX = x
lastY = y
points.push x, y
points
actionsForThang: (thangID, keepIdle=false) ->
# Optimized
@actionsForThangCache ?= {}
2014-06-30 22:16:26 -04:00
cacheKey = thangID + '_' + Boolean(keepIdle)
2014-01-03 13:32:13 -05:00
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
2014-01-14 17:03:55 -05:00
colorConfigs
teamForPlayer: (n) ->
playableTeams = @playableTeams ? ['humans']
playableTeams[n % playableTeams.length]