mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-23 23:58:02 -05:00
A ton of misc bug fixes and performance improvements, getting ready to merge world streaming and flags back in.
This commit is contained in:
parent
1a7e4554f0
commit
718d586f07
21 changed files with 110 additions and 79 deletions
|
@ -27,8 +27,7 @@ init = ->
|
|||
# Set up Backbone.Mediator schemas
|
||||
setUpDefinitions()
|
||||
setUpChannels()
|
||||
#Backbone.Mediator.setValidationEnabled document.location.href.search(/codecombat.com/) is -1
|
||||
Backbone.Mediator.setValidationEnabled false # STREAM: Should change back
|
||||
Backbone.Mediator.setValidationEnabled document.location.href.search(/codecombat.com/) is -1
|
||||
app.initialize()
|
||||
Backbone.history.start({ pushState: true })
|
||||
handleNormalUrls()
|
||||
|
|
|
@ -91,7 +91,7 @@ module.exports = class God extends CocoClass
|
|||
userCodeMap = {}
|
||||
for spellKey, spell of spells
|
||||
for thangID, spellThang of spell.thangs
|
||||
continue if spellThang.thang.programmableMethods[spell.name].cloneOf
|
||||
continue if spellThang.thang?.programmableMethods[spell.name].cloneOf
|
||||
(userCodeMap[thangID] ?= {})[spell.name] = spellThang.aether.serialize()
|
||||
userCodeMap
|
||||
|
||||
|
|
|
@ -184,7 +184,7 @@ module.exports = class Simulator extends CocoClass
|
|||
try
|
||||
@commenceSimulationAndSetupCallback()
|
||||
catch err
|
||||
console.error 'There was an error in simulation:', err, "-- trying again in #{@retryDelayInSeconds} seconds"
|
||||
console.error 'There was an error in simulation:', err, err.stack, "-- trying again in #{@retryDelayInSeconds} seconds"
|
||||
@simulateAnotherTaskAfterDelay()
|
||||
|
||||
assignWorldAndLevelFromLevelLoaderAndDestroyIt: ->
|
||||
|
|
|
@ -59,7 +59,8 @@ module.exports = class WizardSprite extends IndieSprite
|
|||
|
||||
toggle: (to) ->
|
||||
@imageObject?.visible = to
|
||||
@labels.name?[if to then 'show' else 'hide']()
|
||||
label[if to then 'show' else 'hide']() for name, label of @labels
|
||||
mark.mark?.visible = to for name, mark of @marks
|
||||
|
||||
onPlayerStatesChanged: (e) ->
|
||||
for playerID, state of e.states
|
||||
|
|
|
@ -47,7 +47,6 @@ module.exports = class ThangState
|
|||
value = @specialKeysToValues[specialKey]
|
||||
else if type is 'Thang'
|
||||
specialKey = storage[@frameIndex]
|
||||
console.error "couldn't find world from thang", @thang, "for", @specialKeysToValues[specialKey] unless @thang.world
|
||||
value = @thang.world.getThangByID @specialKeysToValues[specialKey]
|
||||
else if type is 'array'
|
||||
specialKey = storage[@frameIndex]
|
||||
|
@ -174,7 +173,6 @@ module.exports = class ThangState
|
|||
# Optimize like no tomorrow--most performance-sensitive part of the whole app, called once per WorldFrame per Thang per trackedProperty, blocking the UI
|
||||
ts = new ThangState
|
||||
ts.thang = thang
|
||||
console.error "couldn't find thang!", @ unless thang
|
||||
ts.frameIndex = frameIndex
|
||||
ts.trackedPropertyKeys = trackedPropertyKeys
|
||||
ts.trackedPropertyTypes = trackedPropertyTypes
|
||||
|
|
|
@ -71,9 +71,11 @@ module.exports = class World
|
|||
@thangs[i] = thang
|
||||
@thangMap[thang.id] = thang
|
||||
|
||||
thangDialogueSounds: ->
|
||||
thangDialogueSounds: (startFrame=0) ->
|
||||
return [] unless startFrame < @frames.length
|
||||
[sounds, seen] = [[], {}]
|
||||
for frame in @frames
|
||||
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
|
||||
|
@ -90,64 +92,30 @@ module.exports = class World
|
|||
|
||||
loadFrames: (loadedCallback, errorCallback, loadProgressCallback, skipDeferredLoading, loadUntilFrame) ->
|
||||
return if @aborted
|
||||
unless @thangs.length
|
||||
console.log 'Warning: loadFrames called on empty World (no thangs).'
|
||||
console.log 'Warning: loadFrames called on empty World (no thangs).' unless @thangs.length
|
||||
t1 = now()
|
||||
@t0 ?= t1
|
||||
@worldLoadStartTime ?= t1
|
||||
@lastRealTimeUpdate ?= 0
|
||||
if loadUntilFrame
|
||||
frameToLoadUntil = loadUntilFrame + 1
|
||||
else
|
||||
frameToLoadUntil = @totalFrames
|
||||
continueLaterFn = =>
|
||||
@loadFrames(loadedCallback, errorCallback, loadProgressCallback, skipDeferredLoading, loadUntilFrame) unless @destroyed
|
||||
frameToLoadUntil = if loadUntilFrame then loadUntilFrame + 1 else @totalFrames # Might stop early if debugging.
|
||||
i = @frames.length
|
||||
while i < frameToLoadUntil and i < @totalFrames
|
||||
t2 = now()
|
||||
if @realTime
|
||||
shouldUpdateProgress = @shouldUpdateRealTimePlayback t2
|
||||
shouldDelayRealTimeSimulation = not shouldUpdateProgress and @shouldDelayRealTimeSimulation t2
|
||||
else
|
||||
shouldUpdateProgress = t2 - t1 > PROGRESS_UPDATE_INTERVAL
|
||||
shouldDelayRealTimeSimulation = false
|
||||
if shouldUpdateProgress or shouldDelayRealTimeSimulation
|
||||
if shouldUpdateProgress
|
||||
@lastRealTimeUpdate = i * @dt if @realTime
|
||||
#console.log 'we think it is now', (t2 - @worldLoadStartTime) / 1000, 'so delivering', @lastRealTimeUpdate
|
||||
loadProgressCallback? i / @totalFrames unless @preloading
|
||||
t1 = t2
|
||||
if t2 - @t0 > 1000
|
||||
console.log ' Loaded', i, 'of', @totalFrames, '(+' + (t2 - @t0).toFixed(0) + 'ms)' unless @realTime
|
||||
@t0 = t2
|
||||
continueFn = =>
|
||||
return if @destroyed
|
||||
if loadUntilFrame
|
||||
@loadFrames(loadedCallback,errorCallback,loadProgressCallback, skipDeferredLoading, loadUntilFrame)
|
||||
else
|
||||
@loadFrames(loadedCallback, errorCallback, loadProgressCallback, skipDeferredLoading)
|
||||
if skipDeferredLoading
|
||||
continueFn()
|
||||
else
|
||||
delay = if shouldDelayRealTimeSimulation then REAL_TIME_BUFFERED_WAIT_INTERVAL else 0
|
||||
setTimeout(continueFn, delay)
|
||||
return
|
||||
|
||||
if @debugging
|
||||
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 = i < loadUntilFrame - framesToLoadFlowBefore
|
||||
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
|
||||
++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
|
||||
@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
|
||||
|
||||
finishLoadingFrames: (loadProgressCallback, loadedCallback) ->
|
||||
unless @debugging
|
||||
@ended = true
|
||||
system.finish @thangs for system in @systems
|
||||
|
@ -170,6 +138,38 @@ module.exports = class World
|
|||
remainingBuffer = @lastRealTimeUpdate * 1000 - timeSinceStart
|
||||
remainingBuffer < REAL_TIME_BUFFER_MIN
|
||||
|
||||
shouldContinueLoading: (t1, loadProgressCallback, skipDeferredLoading, continueLaterFn) ->
|
||||
t2 = now()
|
||||
if @realTime
|
||||
shouldUpdateProgress = @shouldUpdateRealTimePlayback t2
|
||||
shouldDelayRealTimeSimulation = not shouldUpdateProgress and @shouldDelayRealTimeSimulation t2
|
||||
else
|
||||
shouldUpdateProgress = t2 - t1 > PROGRESS_UPDATE_INTERVAL
|
||||
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
|
||||
|
|
|
@ -498,7 +498,9 @@
|
|||
space: "Space"
|
||||
enter: "Enter"
|
||||
escape: "Escape"
|
||||
shift: "Shift"
|
||||
cast_spell: "Cast current spell."
|
||||
run_real_time: "Run in real time."
|
||||
continue_script: "Continue past current script."
|
||||
skip_scripts: "Skip past all skippable scripts."
|
||||
toggle_playback: "Toggle play/pause."
|
||||
|
|
|
@ -63,6 +63,7 @@ module.exports = class ThangType extends CocoModel
|
|||
options = _.clone options
|
||||
options.resolutionFactor ?= SPRITE_RESOLUTION_FACTOR
|
||||
options.async ?= false
|
||||
options.thang = null # Don't hold onto any bad Thang references.
|
||||
options
|
||||
|
||||
buildSpriteSheet: (options) ->
|
||||
|
@ -188,7 +189,7 @@ module.exports = class ThangType extends CocoModel
|
|||
portrait = if portrait then '(Portrait)' else ''
|
||||
name = _.string.rpad @get('name'), 20
|
||||
time = _.string.lpad '' + new Date().getTime() - startTime, 6
|
||||
#console.debug "Built sheet: #{name} #{time}ms #{kind} #{portrait}" # STREAM: uncomment
|
||||
console.debug "Built sheet: #{name} #{time}ms #{kind} #{portrait}"
|
||||
|
||||
spriteSheetKey: (options) ->
|
||||
colorConfigs = []
|
||||
|
|
|
@ -62,9 +62,10 @@ module.exports =
|
|||
additionalProperties: false
|
||||
properties:
|
||||
color:
|
||||
type: 'string'
|
||||
enum: ['green', 'black', 'violet']
|
||||
description: 'The flag color to place next, or omitted/null if deselected.'
|
||||
oneOf: [
|
||||
{type: 'null'}
|
||||
{type: 'string', enum: ['green', 'black', 'violet'], description: 'The flag color to place next, or omitted/null if deselected.'}
|
||||
]
|
||||
pos:
|
||||
type: 'object'
|
||||
additionalProperties: false
|
||||
|
|
|
@ -11,10 +11,11 @@ module.exports =
|
|||
type: "object"
|
||||
preload:
|
||||
type: "boolean"
|
||||
realTime:
|
||||
type: "boolean"
|
||||
required: []
|
||||
additionalProperties: false
|
||||
|
||||
# TODO do we really need both 'cast-spell' and 'cast-spells'?
|
||||
"tome:cast-spells":
|
||||
title: "Cast Spells"
|
||||
$schema: "http://json-schema.org/draft-04/schema#"
|
||||
|
@ -25,6 +26,8 @@ module.exports =
|
|||
type: "object"
|
||||
preload:
|
||||
type: "boolean"
|
||||
realTime:
|
||||
type: "boolean"
|
||||
required: []
|
||||
additionalProperties: false
|
||||
|
||||
|
@ -33,7 +36,9 @@ module.exports =
|
|||
$schema: "http://json-schema.org/draft-04/schema#"
|
||||
description: "Published when you wish to manually recast all spells"
|
||||
type: "object"
|
||||
properties: {}
|
||||
properties:
|
||||
realTime:
|
||||
type: "boolean"
|
||||
required: []
|
||||
additionalProperties: false
|
||||
|
||||
|
|
|
@ -8,6 +8,10 @@ block modal-body-content
|
|||
dt(title="Shift+" + enter)
|
||||
code ⇧+#{enter}
|
||||
dd(data-i18n="keyboard_shortcuts.cast_spell") Cast current spell.
|
||||
dl.dl-horizontal
|
||||
dt(title=ctrlName + "+Shift+" + enter)
|
||||
code #{ctrl}+⇧+#{enter}
|
||||
dd(data-i18n="keyboard_shortcuts.run_real_time") Run in real time.
|
||||
dl.dl-horizontal
|
||||
dt(title="Shift+" + space)
|
||||
code ⇧+#{space}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
div.btn-group.btn-group-lg.cast-button-group
|
||||
.button-progress-overlay
|
||||
button.btn.btn-inverse.banner.cast-button(title=castShortcutVerbose + ": Cast current spell", data-i18n="play_level.tome_cast_button_cast") Spell Cast
|
||||
button.btn.btn-inverse.banner.cast-real-time-button(title=castRealTimeShortcutVerbose + ": Cast in real time")
|
||||
button.btn.btn-inverse.banner.cast-button(title=castVerbose, data-i18n="play_level.tome_cast_button_cast") Spell Cast
|
||||
button.btn.btn-inverse.banner.cast-real-time-button(title=castRealTimeVerbose)
|
||||
i.glyphicon.glyphicon-play
|
|
@ -197,8 +197,9 @@ module.exports = class LadderTabView extends CocoView
|
|||
|
||||
formatCount = d3.format(',.0')
|
||||
|
||||
x = d3.scale.linear().domain([-3000, 6000]).range([0, width])
|
||||
|
||||
minX = Math.floor(Math.min(histogramData...) / 1000) * 1000
|
||||
maxX = Math.ceil(Math.max(histogramData...) / 1000) * 1000
|
||||
x = d3.scale.linear().domain([minX, maxX]).range([0, width])
|
||||
data = d3.layout.histogram().bins(x.ticks(20))(histogramData)
|
||||
y = d3.scale.linear().domain([0, d3.max(data, (d) -> d.y)]).range([height, 10])
|
||||
|
||||
|
|
|
@ -37,12 +37,13 @@ module.exports = class LevelFlagsView extends CocoView
|
|||
@flagHistory = []
|
||||
|
||||
onRealTimePlaybackEnded: (e) ->
|
||||
@realTime = false
|
||||
@onFlagSelected color: null
|
||||
@realTime = false
|
||||
@$el.hide()
|
||||
|
||||
onFlagSelected: (e) ->
|
||||
return if @flagColor is e.color
|
||||
return unless @realTime
|
||||
return if @flagColor is e.color and e.source is 'shortcut'
|
||||
color = if e.source is 'button' and e.color is @flagColor then null else e.color
|
||||
@flagColor = color
|
||||
Backbone.Mediator.publish 'level:flag-color-selected', color: color
|
||||
|
@ -72,4 +73,4 @@ module.exports = class LevelFlagsView extends CocoView
|
|||
|
||||
onNewWorld: (event) ->
|
||||
return unless event.world.name is @world.name
|
||||
@world = event.world
|
||||
@world = @options.world = event.world
|
||||
|
|
|
@ -22,7 +22,7 @@ module.exports = class LevelPlaybackView extends CocoView
|
|||
'level-toggle-grid': 'onToggleGrid'
|
||||
'surface:frame-changed': 'onFrameChanged'
|
||||
'god:new-world-created': 'onNewWorld'
|
||||
'god:streaming-world-updated': 'onNewWorld' # Maybe?
|
||||
'god:streaming-world-updated': 'onNewWorld'
|
||||
'level-set-letterbox': 'onSetLetterbox'
|
||||
'tome:cast-spells': 'onTomeCast'
|
||||
'playback:real-time-playback-ended': 'onRealTimePlaybackEnded'
|
||||
|
|
|
@ -512,7 +512,12 @@ module.exports = class PlayLevelView extends RootView
|
|||
@world = e.world
|
||||
@world.scripts = scripts
|
||||
thangTypes = @supermodel.getModels(ThangType)
|
||||
for [spriteName, message] in @world.thangDialogueSounds()
|
||||
startFrame = @lastWorldFramesLoaded ? 0
|
||||
if @world.frames.length is @world.totalFrames # Finished loading
|
||||
@lastWorldFramesLoaded = 0
|
||||
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
|
||||
|
|
|
@ -21,13 +21,15 @@ module.exports = class CastButtonView extends CocoView
|
|||
@spells = options.spells
|
||||
@levelID = options.levelID
|
||||
@castShortcut = '⇧↵'
|
||||
@castShortcutVerbose = 'Shift+Enter'
|
||||
@castRealTimeShortcutVerbose = 'Ctrl+Shift+Enter'
|
||||
|
||||
getRenderData: (context={}) ->
|
||||
context = super context
|
||||
context.castShortcutVerbose = @castShortcutVerbose
|
||||
context.castRealTimeShortcutVerbose = @castRealTimeShortcutVerbose
|
||||
shift = $.i18n.t 'keyboard_shortcuts.shift'
|
||||
enter = $.i18n.t 'keyboard_shortcuts.enter'
|
||||
castShortcutVerbose = "#{shift}+#{enter}"
|
||||
castRealTimeShortcutVerbose = (if @isMac() then 'Cmd' else 'Ctrl') + '+' + castShortcutVerbose
|
||||
context.castVerbose = castShortcutVerbose + ': ' + $.i18n.t('keyboard_shortcuts.cast_spell')
|
||||
context.castRealTimeVerbose = castRealTimeShortcutVerbose + ': ' + $.i18n.t('keyboard_shortcuts.run_real_time')
|
||||
context
|
||||
|
||||
afterRender: ->
|
||||
|
@ -101,6 +103,6 @@ module.exports = class CastButtonView extends CocoView
|
|||
@autocastDelay = delay = parseInt delay
|
||||
me.set('autocastDelay', delay)
|
||||
me.patch()
|
||||
spell.view.setAutocastDelay delay for spellKey, spell of @spells
|
||||
spell.view?.setAutocastDelay delay for spellKey, spell of @spells
|
||||
@castOptions.find('a').each ->
|
||||
$(@).toggleClass('selected', parseInt($(@).attr('data-delay')) is delay)
|
||||
|
|
|
@ -40,10 +40,13 @@ module.exports = class Spell
|
|||
if @permissions.readwrite.length and sessionSource = @session.getSourceFor(@spellKey)
|
||||
@source = sessionSource
|
||||
@thangs = {}
|
||||
if true # @canRead() # Surely we could avoid creating these if we'll never use them? TODO
|
||||
@view = new SpellView {spell: @, session: @session, worker: @worker}
|
||||
@view.render() # Get it ready and code loaded in advance
|
||||
@tabView = new SpellListTabEntryView spell: @, supermodel: @supermodel, language: @language
|
||||
@tabView.render()
|
||||
#else
|
||||
# @spell.loaded = true
|
||||
@team = @permissions.readwrite[0] ? 'common'
|
||||
Backbone.Mediator.publish 'tome:spell-created', spell: @
|
||||
|
||||
|
|
|
@ -517,7 +517,7 @@ module.exports = class SpellView extends CocoView
|
|||
spellThang.castAether = aether
|
||||
spellThang.aether = @spell.createAether thang
|
||||
#console.log thangID, @spell.spellKey, 'ran', aether.metrics.callsExecuted, 'times over', aether.metrics.statementsExecuted, 'statements, with max recursion depth', aether.metrics.maxDepth, 'and full flow/metrics', aether.metrics, aether.flow
|
||||
@spell.transpile()
|
||||
@spell.transpile() # TODO: is there any way we can avoid doing this if it hasn't changed? Causes a slight hang.
|
||||
@updateAether false, false
|
||||
|
||||
# --------------------------------------------------------------------------------------------------
|
||||
|
|
|
@ -29,6 +29,8 @@ module.exports = class ThangListView extends CocoView
|
|||
false
|
||||
), @sortScoreForThang
|
||||
@muggleThangs = _.sortBy _.without(@thangs, @readwriteThangs..., @readThangs...), @sortScoreForThang
|
||||
if @muggleThangs.length > 15
|
||||
@muggleThangs = [] # Don't render a zillion of these. Slow, too long, maybe not useful.
|
||||
|
||||
sortScoreForThang: (t) =>
|
||||
# Sort by my team, then most spells and fewest shared Thangs per spell,
|
||||
|
@ -73,6 +75,12 @@ module.exports = class ThangListView extends CocoView
|
|||
null
|
||||
|
||||
adjustThangs: (spells, thangs) ->
|
||||
# TODO: it would be nice to not have to do this any more, like if we migrate to the hero levels.
|
||||
# Recreating all the ThangListEntryViews and their ThangAvatarViews is pretty slow.
|
||||
# So they aren't even kept up-to-date during world streaming.
|
||||
# Updating the existing subviews? Would be kind of complicated to get all the new thangs and spells propagated.
|
||||
# I would do it, if I didn't think we were perhaps soon to not do the ThangList any more.
|
||||
# Will temporary reduce the number of muggle thangs we're willing to draw.
|
||||
@spells = @options.spells = spells
|
||||
for entry in @entries
|
||||
entry.$el.remove()
|
||||
|
|
Loading…
Reference in a new issue