mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-26 00:58:00 -05:00
Merge branch 'master' into production
This commit is contained in:
commit
14de27c923
56 changed files with 941 additions and 449 deletions
|
@ -2,7 +2,7 @@ var window = self;
|
|||
var Global = self;
|
||||
|
||||
importScripts("/javascripts/lodash.js", "/javascripts/aether.js");
|
||||
console.log("Aether Tome worker has finished importing scripts.");
|
||||
//console.log("Aether Tome worker has finished importing scripts.");
|
||||
var aethers = {};
|
||||
|
||||
var createAether = function (spellKey, options)
|
||||
|
|
|
@ -300,7 +300,7 @@ self.setupDebugWorldToRunUntilFrame = function (args) {
|
|||
}
|
||||
Math.random = self.debugWorld.rand.randf; // so user code is predictable
|
||||
Aether.replaceBuiltin("Math", Math);
|
||||
replacedLoDash = _.runInContext(self);
|
||||
var replacedLoDash = _.runInContext(self);
|
||||
for(var key in replacedLoDash)
|
||||
_[key] = replacedLoDash[key];
|
||||
}
|
||||
|
@ -346,6 +346,7 @@ self.runWorld = function runWorld(args) {
|
|||
self.world.loadFromLevel(args.level, true);
|
||||
self.world.preloading = args.preload;
|
||||
self.world.headless = args.headless;
|
||||
self.world.realTime = args.realTime;
|
||||
self.goalManager = new GoalManager(self.world);
|
||||
self.goalManager.setGoals(args.goals);
|
||||
self.goalManager.setCode(args.userCodeMap);
|
||||
|
@ -358,16 +359,26 @@ self.runWorld = function runWorld(args) {
|
|||
}
|
||||
Math.random = self.world.rand.randf; // so user code is predictable
|
||||
Aether.replaceBuiltin("Math", Math);
|
||||
replacedLoDash = _.runInContext(self);
|
||||
var replacedLoDash = _.runInContext(self);
|
||||
for(var key in replacedLoDash)
|
||||
_[key] = replacedLoDash[key];
|
||||
self.postMessage({type: 'start-load-frames'});
|
||||
self.world.loadFrames(self.onWorldLoaded, self.onWorldError, self.onWorldLoadProgress);
|
||||
};
|
||||
|
||||
self.serializeFramesSoFar = function serializeFramesSoFar() {
|
||||
if(!self.world) return; // We probably got this message late, after delivering the world.
|
||||
if(self.world.framesSerializedSoFar == self.world.frames.length) return;
|
||||
self.onWorldLoaded();
|
||||
self.world.framesSerializedSoFar = self.world.frames.length;
|
||||
};
|
||||
|
||||
self.onWorldLoaded = function onWorldLoaded() {
|
||||
if(self.world.framesSerializedSoFar == self.world.frames.length) return;
|
||||
if(self.world.ended)
|
||||
self.goalManager.worldGenerationEnded();
|
||||
var goalStates = self.goalManager.getGoalStates();
|
||||
if(self.world.ended)
|
||||
self.postMessage({type: 'end-load-frames', goalStates: goalStates});
|
||||
var t1 = new Date();
|
||||
var diff = t1 - self.t0;
|
||||
|
@ -381,10 +392,12 @@ self.onWorldLoaded = function onWorldLoaded() {
|
|||
catch(error) {
|
||||
console.log("World serialization error:", error.toString() + "\n" + error.stack || error.stackTrace);
|
||||
}
|
||||
|
||||
var t2 = new Date();
|
||||
//console.log("About to transfer", serialized.serializedWorld.trackedPropertiesPerThangValues, serialized.transferableObjects);
|
||||
var messageType = self.world.ended ? 'new-world' : 'some-frames-serialized';
|
||||
try {
|
||||
var message = {type: 'new-world', serialized: serialized.serializedWorld, goalStates: goalStates};
|
||||
var message = {type: messageType, serialized: serialized.serializedWorld, goalStates: goalStates, startFrame: serialized.startFrame, endFrame: serialized.endFrame};
|
||||
if(transferableSupported)
|
||||
self.postMessage(message, serialized.transferableObjects);
|
||||
else
|
||||
|
@ -393,11 +406,14 @@ self.onWorldLoaded = function onWorldLoaded() {
|
|||
catch(error) {
|
||||
console.log("World delivery error:", error.toString() + "\n" + error.stack || error.stackTrace);
|
||||
}
|
||||
|
||||
if(self.world.ended) {
|
||||
var t3 = new Date();
|
||||
console.log("And it was so: (" + (diff / self.world.totalFrames).toFixed(3) + "ms per frame,", self.world.totalFrames, "frames)\nSimulation :", diff + "ms \nSerialization:", (t2 - t1) + "ms\nDelivery :", (t3 - t2) + "ms");
|
||||
self.world.goalManager.destroy();
|
||||
self.world.destroy();
|
||||
self.world = null;
|
||||
}
|
||||
};
|
||||
|
||||
self.onWorldError = function onWorldError(error) {
|
||||
|
@ -443,6 +459,11 @@ self.finalizePreload = function finalizePreload() {
|
|||
self.world.finalizePreload(self.onWorldLoaded);
|
||||
};
|
||||
|
||||
self.addFlagEvent = function addFlagEvent(flagEvent) {
|
||||
if(!self.world) return;
|
||||
self.world.addFlagEvent(flagEvent);
|
||||
};
|
||||
|
||||
self.addEventListener('message', function(event) {
|
||||
self[event.data.func](event.data.args);
|
||||
});
|
||||
|
|
|
@ -12,6 +12,9 @@ module.exports = class Angel extends CocoClass
|
|||
infiniteLoopTimeoutDuration: 7500 # wait this long for a response when checking
|
||||
abortTimeoutDuration: 500 # give in-process or dying workers this long to give up
|
||||
|
||||
subscriptions:
|
||||
'level:flag-updated': 'onFlagEvent'
|
||||
|
||||
constructor: (@shared) ->
|
||||
super()
|
||||
@say 'Got my wings.'
|
||||
|
@ -64,14 +67,6 @@ module.exports = class Angel extends CocoClass
|
|||
clearTimeout @condemnTimeout
|
||||
@beholdGoalStates event.data.goalStates # Work ends here if we're headless.
|
||||
|
||||
# We pay attention to certain progress indicators as the world loads.
|
||||
when 'world-load-progress-changed'
|
||||
Backbone.Mediator.publish 'god:world-load-progress-changed', event.data
|
||||
when 'console-log'
|
||||
@log event.data.args...
|
||||
when 'user-code-problem'
|
||||
Backbone.Mediator.publish 'god:user-code-problem', problem: event.data.problem
|
||||
|
||||
# We have to abort like an infinite loop if we see one of these; they're not really recoverable
|
||||
when 'non-user-code-problem'
|
||||
Backbone.Mediator.publish 'god:non-user-code-problem', problem: event.data.problem
|
||||
|
@ -80,9 +75,7 @@ module.exports = class Angel extends CocoClass
|
|||
else
|
||||
@fireWorker()
|
||||
|
||||
# Either the world finished simulating successfully, or we abort the worker.
|
||||
when 'new-world'
|
||||
@beholdWorld event.data.serialized, event.data.goalStates
|
||||
# If it didn't finish simulating successfully, or we abort the worker.
|
||||
when 'abort'
|
||||
@say 'Aborted.', event.data
|
||||
clearTimeout @abortTimeout
|
||||
|
@ -91,6 +84,23 @@ module.exports = class Angel extends CocoClass
|
|||
_.remove @shared.busyAngels, @
|
||||
@doWork()
|
||||
|
||||
# We pay attention to certain progress indicators as the world loads.
|
||||
when 'console-log'
|
||||
@log event.data.args...
|
||||
when 'user-code-problem'
|
||||
Backbone.Mediator.publish 'god:user-code-problem', problem: event.data.problem
|
||||
when 'world-load-progress-changed'
|
||||
Backbone.Mediator.publish 'god:world-load-progress-changed', event.data
|
||||
unless event.data.progress is 1 or @work.preload or @work.headless or @work.synchronous or @deserializationQueue.length or @shared.firstWorld
|
||||
@worker.postMessage func: 'serializeFramesSoFar' # Stream it!
|
||||
|
||||
# We have some or all of the frames serialized, so let's send the (partially?) simulated world to the Surface.
|
||||
when 'some-frames-serialized', 'new-world'
|
||||
deserializationArgs = [event.data.serialized, event.data.goalStates, event.data.startFrame, event.data.endFrame, @streamingWorld]
|
||||
@deserializationQueue.push deserializationArgs
|
||||
if @deserializationQueue.length is 1
|
||||
@beholdWorld deserializationArgs...
|
||||
|
||||
else
|
||||
@log 'Received unsupported message:', event.data
|
||||
|
||||
|
@ -99,27 +109,37 @@ module.exports = class Angel extends CocoClass
|
|||
Backbone.Mediator.publish 'god:goals-calculated', goalStates: goalStates
|
||||
@finishWork() if @shared.headless
|
||||
|
||||
beholdWorld: (serialized, goalStates) ->
|
||||
beholdWorld: (serialized, goalStates, startFrame, endFrame, streamingWorld) ->
|
||||
return if @aborting
|
||||
# Toggle BOX2D_ENABLED during deserialization so that if we have box2d in the namespace, the Collides Components still don't try to create bodies for deserialized Thangs upon attachment.
|
||||
window.BOX2D_ENABLED = false
|
||||
World.deserialize serialized, @shared.worldClassMap, @shared.lastSerializedWorldFrames, @finishBeholdingWorld(goalStates)
|
||||
World.deserialize serialized, @shared.worldClassMap, @shared.lastSerializedWorldFrames, @finishBeholdingWorld(goalStates), startFrame, endFrame, streamingWorld
|
||||
window.BOX2D_ENABLED = true
|
||||
@shared.lastSerializedWorldFrames = serialized.frames
|
||||
|
||||
finishBeholdingWorld: (goalStates) -> (world) =>
|
||||
return if @aborting
|
||||
world.findFirstChangedFrame @shared.world
|
||||
@streamingWorld = world
|
||||
finished = world.frames.length is world.totalFrames
|
||||
firstChangedFrame = world.findFirstChangedFrame @shared.world
|
||||
eventType = if finished then 'god:new-world-created' else 'god:streaming-world-updated'
|
||||
if finished
|
||||
@shared.world = world
|
||||
errorCount = (t for t in @shared.world.thangs when t.errorsOut).length
|
||||
Backbone.Mediator.publish 'god:new-world-created', world: world, firstWorld: @shared.firstWorld, errorCount: errorCount, goalStates: goalStates, team: me.team
|
||||
Backbone.Mediator.publish eventType, world: world, firstWorld: @shared.firstWorld, goalStates: goalStates, team: me.team, firstChangedFrame: firstChangedFrame
|
||||
if finished
|
||||
for scriptNote in @shared.world.scriptNotes
|
||||
Backbone.Mediator.publish scriptNote.channel, scriptNote.event
|
||||
@shared.goalManager?.world = world
|
||||
@finishWork()
|
||||
else
|
||||
@deserializationQueue.shift() # Finished with this deserialization.
|
||||
if deserializationArgs = @deserializationQueue[0] # Start another?
|
||||
@beholdWorld deserializationArgs...
|
||||
|
||||
finishWork: ->
|
||||
@streamingWorld = null
|
||||
@shared.firstWorld = false
|
||||
@deserializationQueue = []
|
||||
@running = false
|
||||
_.remove @shared.busyAngels, @
|
||||
@doWork()
|
||||
|
@ -127,6 +147,7 @@ module.exports = class Angel extends CocoClass
|
|||
finalizePreload: ->
|
||||
@say 'Finalize preload.'
|
||||
@worker.postMessage func: 'finalizePreload'
|
||||
@work.preload = false
|
||||
|
||||
infinitelyLooped: =>
|
||||
@say 'On infinitely looped! Aborting?', @aborting
|
||||
|
@ -145,6 +166,7 @@ module.exports = class Angel extends CocoClass
|
|||
@say 'Running world...'
|
||||
@running = true
|
||||
@shared.busyAngels.push @
|
||||
@deserializationQueue = []
|
||||
@worker.postMessage func: 'runWorld', args: @work
|
||||
clearTimeout @purgatoryTimer
|
||||
@say 'Infinite loop timer started at interval of', @infiniteLoopIntervalDuration
|
||||
|
@ -158,6 +180,8 @@ module.exports = class Angel extends CocoClass
|
|||
@say 'Aborting...'
|
||||
@running = false
|
||||
@work = null
|
||||
@streamingWorld = null
|
||||
@deserializationQueue = null
|
||||
_.remove @shared.busyAngels, @
|
||||
@abortTimeout = _.delay @fireWorker, @abortTimeoutDuration
|
||||
@aborting = true
|
||||
|
@ -184,6 +208,10 @@ module.exports = class Angel extends CocoClass
|
|||
@worker.addEventListener 'message', @onWorkerMessage
|
||||
@worker.creationTime = new Date()
|
||||
|
||||
onFlagEvent: (e) ->
|
||||
return unless @running and @work.realTime
|
||||
@worker.postMessage func: 'addFlagEvent', args: e
|
||||
|
||||
|
||||
#### Synchronous code for running worlds on main thread (profiling / IE9) ####
|
||||
simulateSync: (work) =>
|
||||
|
|
|
@ -53,9 +53,9 @@ module.exports = class God extends CocoClass
|
|||
setWorldClassMap: (worldClassMap) -> @angelsShare.worldClassMap = worldClassMap
|
||||
|
||||
onTomeCast: (e) ->
|
||||
@createWorld e.spells, e.preload
|
||||
@createWorld e.spells, e.preload, e.realTime
|
||||
|
||||
createWorld: (spells, preload=false) ->
|
||||
createWorld: (spells, preload=false, realTime=false) ->
|
||||
console.log "#{@nick}: Let there be light upon #{@level.name}! (preload: #{preload})"
|
||||
userCodeMap = @getUserCodeMap spells
|
||||
|
||||
|
@ -84,12 +84,14 @@ module.exports = class God extends CocoClass
|
|||
headless: @angelsShare.headless
|
||||
preload: preload
|
||||
synchronous: not Worker? # Profiling world simulation is easier on main thread, or we are IE9.
|
||||
realTime: realTime
|
||||
angel.workIfIdle() for angel in @angelsShare.angels
|
||||
|
||||
getUserCodeMap: (spells) ->
|
||||
userCodeMap = {}
|
||||
for spellKey, spell of spells
|
||||
for thangID, spellThang of spell.thangs
|
||||
continue if spellThang.thang?.programmableMethods[spell.name].cloneOf
|
||||
(userCodeMap[thangID] ?= {})[spell.name] = spellThang.aether.serialize()
|
||||
userCodeMap
|
||||
|
||||
|
|
|
@ -197,6 +197,8 @@ module.exports = class LevelBus extends Bus
|
|||
onNewGoalStates: ({goalStates})->
|
||||
state = @session.get 'state'
|
||||
unless utils.kindaEqual state.goalStates, goalStates # Only save when goals really change
|
||||
# TODO: this log doesn't capture when null-status goals are being set during world streaming. Where can they be coming from?
|
||||
return console.error("Somehow trying to save null goal states!", goalStates) if _.find(goalStates, (gs) -> not gs.status)
|
||||
state.goalStates = goalStates
|
||||
@session.set 'state', state
|
||||
@changedSessionProperties.state = true
|
||||
|
|
|
@ -78,6 +78,10 @@ module.exports = class LevelLoader extends CocoClass
|
|||
@listenToOnce @opponentSession, 'sync', @loadDependenciesForSession
|
||||
|
||||
loadDependenciesForSession: (session) ->
|
||||
return if @levelID is 'sky-span' # TODO
|
||||
# TODO: I think this runs afoul of https://github.com/codecombat/codecombat/issues/1108
|
||||
# TODO: this shouldn't happen when it's not a hero level, but we don't have level loaded yet,
|
||||
# and the sessions are being created with default hero config regardless of whether it's a hero level.
|
||||
if heroConfig = session.get('heroConfig')
|
||||
url = "/db/thang.type/#{heroConfig.thangType}/version?project=name,components,original"
|
||||
@worldNecessities.push @maybeLoadURL(url, ThangType, 'thang')
|
||||
|
@ -107,7 +111,8 @@ module.exports = class LevelLoader extends CocoClass
|
|||
systemVersions = []
|
||||
articleVersions = []
|
||||
|
||||
for thang in @level.get('thangs') or []
|
||||
flagThang = thangType: '53fa25f25bc220000052c2be', id: 'Placeholder Flag', components: []
|
||||
for thang in (@level.get('thangs') or []).concat [flagThang]
|
||||
thangIDs.push thang.thangType
|
||||
@loadItemThangsEquippedByLevelThang(thang)
|
||||
for comp in thang.components or []
|
||||
|
|
|
@ -14,7 +14,7 @@ module.exports = LinkedInHandler = class LinkedInHandler extends CocoClass
|
|||
onLinkedInLoaded: (e) ->
|
||||
IN.Event.on IN, 'auth', @onLinkedInAuth
|
||||
|
||||
onLinkedInAuth: (e) => console.log 'Authorized with LinkedIn'
|
||||
onLinkedInAuth: (e) => #console.log 'Authorized with LinkedIn'
|
||||
|
||||
constructEmployerAgreementObject: (cb) =>
|
||||
IN.API.Profile('me')
|
||||
|
|
|
@ -269,6 +269,7 @@ module.exports = ScriptManager = class ScriptManager extends CocoClass
|
|||
@publishNote(note)
|
||||
|
||||
publishNote: (note) ->
|
||||
Backbone.Mediator.publish 'playback:real-time-playback-ended', {}
|
||||
Backbone.Mediator.publish(note.channel, note.event)
|
||||
|
||||
# ENDING NOTES
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
module.exports = initializeLinkedIn = ->
|
||||
window.linkedInAsyncInit = ->
|
||||
console.log 'Linkedin async init success!'
|
||||
#console.log 'Linkedin async init success!'
|
||||
Backbone.Mediator.publish 'linkedin-loaded'
|
||||
|
||||
linkedInSnippet =
|
||||
|
|
|
@ -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: ->
|
||||
|
|
|
@ -1,81 +0,0 @@
|
|||
CocoClass = require 'lib/CocoClass'
|
||||
|
||||
module.exports = class CastingScreen extends CocoClass
|
||||
subscriptions:
|
||||
'tome:cast-spells': 'onCastingBegins'
|
||||
'god:new-world-created': 'onCastingEnds'
|
||||
'god:world-load-progress-changed': 'onWorldLoadProgressChanged'
|
||||
|
||||
constructor: (options) ->
|
||||
super()
|
||||
options ?= {}
|
||||
@camera = options.camera
|
||||
@layer = options.layer
|
||||
console.error @toString(), 'needs a camera.' unless @camera
|
||||
console.error @toString(), 'needs a layer.' unless @layer
|
||||
@build()
|
||||
|
||||
onCastingBegins: (e) -> @show() unless e.preload
|
||||
onCastingEnds: (e) -> @hide()
|
||||
|
||||
toString: -> '<CastingScreen>'
|
||||
|
||||
build: ->
|
||||
@dimLayer = new createjs.Container()
|
||||
@dimLayer.mouseEnabled = @dimLayer.mouseChildren = false
|
||||
@dimLayer.layerIndex = -11
|
||||
@dimLayer.addChild @dimScreen = new createjs.Shape()
|
||||
@dimScreen.graphics.beginFill('rgba(0,0,0,0.5)').rect 0, 0, @camera.canvasWidth, @camera.canvasHeight
|
||||
@dimLayer.alpha = 0
|
||||
@layer.addChild @dimLayer
|
||||
@dimLayer.addChild @makeProgressBar()
|
||||
@dimLayer.addChild @makeCastingText()
|
||||
|
||||
onWorldLoadProgressChanged: (e) ->
|
||||
if new Date().getTime() - @t0 > 500
|
||||
createjs.Tween.removeTweens @progressBar
|
||||
createjs.Tween.get(@progressBar).to({scaleX: e.progress}, 200)
|
||||
|
||||
makeProgressBar: ->
|
||||
BAR_PIXEL_HEIGHT = 3
|
||||
BAR_PCT_WIDTH = .75
|
||||
pixelWidth = parseInt(@camera.canvasWidth * BAR_PCT_WIDTH)
|
||||
pixelMargin = (@camera.canvasWidth - (@camera.canvasWidth * BAR_PCT_WIDTH)) / 2
|
||||
barY = 3 * (@camera.canvasHeight / 5)
|
||||
|
||||
g = new createjs.Graphics()
|
||||
g.beginFill(createjs.Graphics.getRGB(255, 255, 255))
|
||||
g.drawRoundRect(0, 0, pixelWidth, BAR_PIXEL_HEIGHT, 3)
|
||||
@progressBar = new createjs.Shape(g)
|
||||
@progressBar.x = pixelMargin
|
||||
@progressBar.y = barY
|
||||
@progressBar.scaleX = 0
|
||||
@dimLayer.addChild(@progressBar)
|
||||
|
||||
makeCastingText: ->
|
||||
size = @camera.canvasHeight / 15
|
||||
text = new createjs.Text('Casting', "#{size}px cursive", '#aaaaaa')
|
||||
text.regX = text.getMeasuredWidth() / 2
|
||||
text.regY = text.getMeasuredHeight() / 2
|
||||
text.x = @camera.canvasWidth / 2
|
||||
text.y = @camera.canvasHeight / 2
|
||||
@text = text
|
||||
return text
|
||||
|
||||
show: ->
|
||||
return if @showing
|
||||
@showing = true
|
||||
@t0 = new Date().getTime()
|
||||
|
||||
@progressBar.scaleX = 0
|
||||
@dimLayer.alpha = 0
|
||||
createjs.Tween.removeTweens @dimLayer
|
||||
createjs.Tween.get(@dimLayer).to({alpha: 1}, 500)
|
||||
|
||||
hide: ->
|
||||
return unless @showing
|
||||
@showing = false
|
||||
|
||||
createjs.Tween.removeTweens @progressBar
|
||||
createjs.Tween.removeTweens @dimLayer
|
||||
createjs.Tween.get(@dimLayer).to({alpha: 0}, 500)
|
|
@ -320,6 +320,9 @@ module.exports = CocoSprite = class CocoSprite extends CocoClass
|
|||
[@imageObject.x, @imageObject.y] = [sup.x, sup.y]
|
||||
@lastPos = p1.copy?() or _.clone(p1)
|
||||
@hasMoved = true
|
||||
if @thangType.get('name') is 'Flag' and not @notOfThisWorld
|
||||
# Let the pending flags know we're here (but not this call stack, they need to delete themselves, and we may be iterating sprites).
|
||||
_.defer => Backbone.Mediator.publish 'surface:flag-appeared', sprite: @
|
||||
|
||||
updateBaseScale: ->
|
||||
scale = 1
|
||||
|
@ -519,7 +522,7 @@ module.exports = CocoSprite = class CocoSprite extends CocoClass
|
|||
@letterboxOn = e.on
|
||||
|
||||
onMouseEvent: (e, ourEventName) ->
|
||||
return if @letterboxOn
|
||||
return if @letterboxOn or not @imageObject
|
||||
p = @imageObject
|
||||
p = p.parent while p.parent
|
||||
newEvent = sprite: @, thang: @thang, originalEvent: e, canvas:p.canvas
|
||||
|
|
|
@ -35,10 +35,6 @@ module.exports = class CoordinateDisplay extends createjs.Container
|
|||
onMouseOut: (e) -> @mouseInBounds = false
|
||||
|
||||
onMouseMove: (e) ->
|
||||
if @mouseInBounds and key.shift
|
||||
$('#surface').addClass('flag-cursor') unless $('#surface').hasClass('flag-cursor')
|
||||
else if @mouseInBounds
|
||||
$('#surface').removeClass('flag-cursor') if $('#surface').hasClass('flag-cursor')
|
||||
wop = @camera.screenToWorld x: e.x, y: e.y
|
||||
wop.x = Math.round(wop.x)
|
||||
wop.y = Math.round(wop.y)
|
||||
|
|
34
app/lib/surface/FlagSprite.coffee
Normal file
34
app/lib/surface/FlagSprite.coffee
Normal file
|
@ -0,0 +1,34 @@
|
|||
IndieSprite = require 'lib/surface/IndieSprite'
|
||||
{me} = require 'lib/auth'
|
||||
|
||||
module.exports = class FlagSprite extends IndieSprite
|
||||
subscriptions:
|
||||
'surface:mouse-moved': 'onMouseMoved'
|
||||
|
||||
#shortcuts:
|
||||
|
||||
defaultPos: -> x: 20, y: 20, z: 1
|
||||
|
||||
constructor: (thangType, options) ->
|
||||
super thangType, options
|
||||
@toggleCursor options.isCursor
|
||||
|
||||
makeIndieThang: (thangType, options) ->
|
||||
thang = super thangType, options
|
||||
thang.width = thang.height = thang.depth = 2
|
||||
thang.pos.z = 1
|
||||
thang.isSelectable = false
|
||||
thang.color = options.color
|
||||
thang.team = options.team
|
||||
thang
|
||||
|
||||
onMouseMoved: (e) ->
|
||||
return unless @options.isCursor
|
||||
wop = @options.camera.screenToWorld x: e.x, y: e.y
|
||||
@thang.pos.x = wop.x
|
||||
@thang.pos.y = wop.y
|
||||
|
||||
toggleCursor: (to) ->
|
||||
@options.isCursor = to
|
||||
@thang.alpha = if to then 0.33 else 0.67 # 1.0 is for flags that have been placed
|
||||
@updateAlpha()
|
|
@ -1,8 +1,5 @@
|
|||
{me} = require('lib/auth')
|
||||
Thang = require 'lib/world/thang'
|
||||
Vector = require 'lib/world/vector'
|
||||
CocoSprite = require 'lib/surface/CocoSprite'
|
||||
Camera = require './Camera'
|
||||
|
||||
module.exports = IndieSprite = class IndieSprite extends CocoSprite
|
||||
notOfThisWorld: true
|
||||
|
@ -11,16 +8,16 @@ module.exports = IndieSprite = class IndieSprite extends CocoSprite
|
|||
'note-group-ended': 'onNoteGroupEnded'
|
||||
|
||||
constructor: (thangType, options) ->
|
||||
options.thang = @makeIndieThang thangType, options.thangID, options.pos
|
||||
options.thang = @makeIndieThang thangType, options
|
||||
super thangType, options
|
||||
@shadow = @thang
|
||||
|
||||
makeIndieThang: (thangType, thangID, pos) ->
|
||||
@thang = thang = new Thang null, thangType.get('name'), thangID
|
||||
makeIndieThang: (thangType, options) ->
|
||||
@thang = thang = new Thang null, thangType.get('name'), options.thangID
|
||||
# Build needed results of what used to be Exists, Physical, Acts, and Selectable Components
|
||||
thang.exists = true
|
||||
thang.width = thang.height = thang.depth = 4
|
||||
thang.pos = pos ? @defaultPos()
|
||||
thang.pos = options.pos ? @defaultPos()
|
||||
thang.pos.z = thang.depth / 2
|
||||
thang.shape = 'ellipsoid'
|
||||
thang.rotation = 0
|
||||
|
|
|
@ -37,6 +37,8 @@ module.exports = class Label extends CocoClass
|
|||
build: ->
|
||||
@layer.removeChild @background if @background
|
||||
@layer.removeChild @label if @label
|
||||
@label = null
|
||||
@background = null
|
||||
return unless @text # null or '' should both be skipped
|
||||
o = @buildLabelOptions()
|
||||
@layer.addChild @label = @buildLabel o
|
||||
|
@ -53,6 +55,17 @@ module.exports = class Label extends CocoClass
|
|||
@label.y = @background.y = @sprite.imageObject.y + offset.y
|
||||
null
|
||||
|
||||
show: ->
|
||||
return unless @label
|
||||
@layer.addChild @label
|
||||
@layer.addChild @background
|
||||
@layer.updateLayerOrder()
|
||||
|
||||
hide: ->
|
||||
return unless @label
|
||||
@layer.removeChild @background
|
||||
@layer.removeChild @label
|
||||
|
||||
buildLabelOptions: ->
|
||||
o = {}
|
||||
st = {dialogue: 'D', say: 'S', name: 'N'}[@style]
|
||||
|
|
|
@ -3,6 +3,7 @@ CocoClass = require 'lib/CocoClass'
|
|||
Layer = require './Layer'
|
||||
IndieSprite = require 'lib/surface/IndieSprite'
|
||||
WizardSprite = require 'lib/surface/WizardSprite'
|
||||
FlagSprite = require 'lib/surface/FlagSprite'
|
||||
CocoSprite = require 'lib/surface/CocoSprite'
|
||||
Mark = require './Mark'
|
||||
Grid = require 'lib/world/Grid'
|
||||
|
@ -19,8 +20,13 @@ module.exports = class SpriteBoss extends CocoClass
|
|||
'level-lock-select': 'onSetLockSelect'
|
||||
'level:restarted': 'onLevelRestarted'
|
||||
'god:new-world-created': 'onNewWorld'
|
||||
'god:streaming-world-updated': 'onNewWorld'
|
||||
'camera:dragged': 'onCameraDragged'
|
||||
'sprite:loaded': -> @update(true)
|
||||
'level:flag-color-selected': 'onFlagColorSelected'
|
||||
'level:flag-updated': 'onFlagUpdated'
|
||||
'surface:flag-appeared': 'onFlagAppeared'
|
||||
'surface:remove-selected-flag': 'onRemoveSelectedFlag'
|
||||
|
||||
constructor: (@options) ->
|
||||
super()
|
||||
|
@ -36,6 +42,7 @@ module.exports = class SpriteBoss extends CocoClass
|
|||
@selfWizardSprite = null
|
||||
@createLayers()
|
||||
@spriteSheetCache = {}
|
||||
@pendingFlags = []
|
||||
|
||||
destroy: ->
|
||||
@removeSprite sprite for thangID, sprite of @sprites
|
||||
|
@ -112,7 +119,7 @@ module.exports = class SpriteBoss extends CocoClass
|
|||
sprite = @createWizardSprite thangID: opponent.id, name: opponent.name, codeLanguage: opponent.codeLanguage
|
||||
if not opponent.levelSlug or opponent.levelSlug is 'brawlwood'
|
||||
sprite.targetPos = if opponent.team is 'ogres' then {x: 52, y: 52} else {x: 28, y: 28}
|
||||
else if opponent.levelSlug is 'dungeon-arena'
|
||||
else if opponent.levelSlug in ['dungeon-arena', 'sky-span']
|
||||
sprite.targetPos = if opponent.team is 'ogres' then {x: 72, y: 39} else {x: 9, y: 39}
|
||||
else if opponent.levelSlug is 'criss-cross'
|
||||
sprite.targetPos = if opponent.team is 'ogres' then {x: 50, y: 12} else {x: 0, y: 40}
|
||||
|
@ -276,6 +283,7 @@ module.exports = class SpriteBoss extends CocoClass
|
|||
return @dragged = 0 if @dragged > 3
|
||||
@dragged = 0
|
||||
sprite = if e.sprite?.thang?.isSelectable then e.sprite else null
|
||||
return if @flagCursorSprite and sprite?.thangType.get('name') is 'Flag'
|
||||
@selectSprite e, sprite
|
||||
|
||||
onStageMouseDown: (e) ->
|
||||
|
@ -315,6 +323,45 @@ module.exports = class SpriteBoss extends CocoClass
|
|||
instance.addEventListener 'complete', ->
|
||||
Backbone.Mediator.publish 'thang-finished-talking', thang: sprite?.thang
|
||||
|
||||
onFlagColorSelected: (e) ->
|
||||
@removeSprite @flagCursorSprite if @flagCursorSprite
|
||||
@flagCursorSprite = null
|
||||
for flagSprite in @spriteArray when flagSprite.thangType.get('name') is 'Flag'
|
||||
flagSprite.imageObject.cursor = if e.color then 'crosshair' else 'pointer'
|
||||
return unless e.color
|
||||
@flagCursorSprite = new FlagSprite @thangTypeFor('Flag'), @createSpriteOptions(thangID: 'Flag Cursor', color: e.color, team: me.team, isCursor: true, pos: e.pos)
|
||||
@addSprite @flagCursorSprite, @flagCursorSprite.thang.id, @spriteLayers['Floating']
|
||||
|
||||
onFlagUpdated: (e) ->
|
||||
return unless e.active
|
||||
pendingFlag = new FlagSprite @thangTypeFor('Flag'), @createSpriteOptions(thangID: 'Pending Flag ' + Math.random(), color: e.color, team: me.team, isCursor: false, pos: e.pos)
|
||||
@addSprite pendingFlag, pendingFlag.thang.id, @spriteLayers['Floating']
|
||||
@pendingFlags.push pendingFlag
|
||||
|
||||
onFlagAppeared: (e) ->
|
||||
# Remove the pending flag that matches this one's color/team/position, and any color/team matches placed earlier.
|
||||
t1 = e.sprite.thang
|
||||
pending = (@pendingFlags ? []).slice()
|
||||
foundExactMatch = false
|
||||
for i in [pending.length - 1 .. 0] by -1
|
||||
pendingFlag = pending[i]
|
||||
t2 = pendingFlag.thang
|
||||
matchedType = t1.color is t2.color and t1.team is t2.team
|
||||
matched = matchedType and (foundExactMatch or Math.abs(t1.pos.x - t2.pos.x) < 0.00001 and Math.abs(t1.pos.y - t2.pos.y) < 0.00001)
|
||||
if matched
|
||||
foundExactMatch = true
|
||||
@pendingFlags.splice(i, 1)
|
||||
@removeSprite pendingFlag
|
||||
e.sprite.imageObject.cursor = if @flagCursorSprite then 'crosshair' else 'pointer'
|
||||
null
|
||||
|
||||
onRemoveSelectedFlag: (e) ->
|
||||
# Remove the selected sprite if it's a flag, or any flag of the given color if a color is given.
|
||||
flagSprite = _.find [@selectedSprite].concat(@spriteArray), (sprite) ->
|
||||
sprite and sprite.thangType.get('name') is 'Flag' and sprite.thang.team is me.team and (sprite.thang.color is e.color or not e.color) and not sprite.notOfThisWorld
|
||||
return unless flagSprite
|
||||
Backbone.Mediator.publish 'surface:remove-flag', color: flagSprite.thang.color
|
||||
|
||||
# Marks
|
||||
|
||||
updateSelection: ->
|
||||
|
|
|
@ -8,7 +8,6 @@ CameraBorder = require './CameraBorder'
|
|||
Layer = require './Layer'
|
||||
Letterbox = require './Letterbox'
|
||||
Dimmer = require './Dimmer'
|
||||
CastingScreen = require './CastingScreen'
|
||||
PlaybackOverScreen = require './PlaybackOverScreen'
|
||||
DebugDisplay = require './DebugDisplay'
|
||||
CoordinateDisplay = require './CoordinateDisplay'
|
||||
|
@ -63,10 +62,15 @@ module.exports = Surface = class Surface extends CocoClass
|
|||
'level-set-surface-camera': 'onSetCamera'
|
||||
'level:restarted': 'onLevelRestarted'
|
||||
'god:new-world-created': 'onNewWorld'
|
||||
'god:streaming-world-updated': 'onNewWorld'
|
||||
'tome:cast-spells': 'onCastSpells'
|
||||
'level-set-letterbox': 'onSetLetterbox'
|
||||
'application:idle-changed': 'onIdleChanged'
|
||||
'camera:zoom-updated': 'onZoomUpdated'
|
||||
'playback:real-time-playback-started': 'onRealTimePlaybackStarted'
|
||||
'playback:real-time-playback-ended': 'onRealTimePlaybackEnded'
|
||||
'level:flag-color-selected': 'onFlagColorSelected'
|
||||
#'god:world-load-progress-changed': -> console.log 'it is actually', @world.age
|
||||
|
||||
shortcuts:
|
||||
'ctrl+\\, ⌘+\\': 'onToggleDebug'
|
||||
|
@ -82,7 +86,7 @@ module.exports = Surface = class Surface extends CocoClass
|
|||
@options = _.extend(@options, givenOptions) if givenOptions
|
||||
@initEasel()
|
||||
@initAudio()
|
||||
@onResize = _.debounce @onResize, 500
|
||||
@onResize = _.debounce @onResize, 250
|
||||
$(window).on 'resize', @onResize
|
||||
if @world.ended
|
||||
_.defer => @setWorld @world
|
||||
|
@ -96,7 +100,6 @@ module.exports = Surface = class Surface extends CocoClass
|
|||
@spriteBoss.destroy()
|
||||
@chooser?.destroy()
|
||||
@dimmer?.destroy()
|
||||
@castingScreen?.destroy()
|
||||
@playbackOverScreen?.destroy()
|
||||
@stage.clear()
|
||||
@musicPlayer?.destroy()
|
||||
|
@ -115,14 +118,14 @@ module.exports = Surface = class Surface extends CocoClass
|
|||
|
||||
setWorld: (@world) ->
|
||||
@worldLoaded = true
|
||||
lastFrame = Math.min(@getCurrentFrame(), @world.totalFrames - 1)
|
||||
lastFrame = Math.min(@getCurrentFrame(), @world.frames.length - 1)
|
||||
@world.getFrame(lastFrame).restoreState() unless @options.choosing
|
||||
@spriteBoss.world = @world
|
||||
|
||||
@showLevel()
|
||||
@updateState true if @loaded
|
||||
# TODO: synchronize both ways of choosing whether to show coords (@world via UI System or @options via World Select modal)
|
||||
if @world.showCoordinates and @options.coords
|
||||
if @world.showCoordinates and @options.coords and not @coordinateDisplay
|
||||
@coordinateDisplay = new CoordinateDisplay camera: @camera
|
||||
@surfaceTextLayer.addChild @coordinateDisplay
|
||||
@onFrameChanged()
|
||||
|
@ -190,19 +193,19 @@ module.exports = Surface = class Surface extends CocoClass
|
|||
setProgress: (progress, scrubDuration=500) ->
|
||||
progress = Math.max(Math.min(progress, 1), 0.0)
|
||||
|
||||
@fastForwardingToFrame = null
|
||||
@scrubbing = true
|
||||
onTweenEnd = =>
|
||||
@scrubbingTo = null
|
||||
@scrubbing = false
|
||||
@scrubbingPlaybackSpeed = null
|
||||
@fastForwarding = false
|
||||
|
||||
if @scrubbingTo?
|
||||
# cut to the chase for existing tween
|
||||
createjs.Tween.removeTweens(@)
|
||||
@currentFrame = @scrubbingTo
|
||||
|
||||
@scrubbingTo = Math.min(Math.round(progress * @world.totalFrames), @world.totalFrames)
|
||||
@scrubbingTo = Math.min(Math.round(progress * @world.frames.length), @world.frames.length)
|
||||
@scrubbingPlaybackSpeed = Math.sqrt(Math.abs(@scrubbingTo - @currentFrame) * @world.dt / (scrubDuration or 0.5))
|
||||
if scrubDuration
|
||||
t = createjs.Tween
|
||||
|
@ -244,7 +247,7 @@ module.exports = Surface = class Surface extends CocoClass
|
|||
getCurrentFrame: ->
|
||||
return Math.max(0, Math.min(Math.floor(@currentFrame), @world.frames.length - 1))
|
||||
|
||||
getProgress: -> @currentFrame / @world.totalFrames
|
||||
getProgress: -> @currentFrame / @world.frames.length
|
||||
|
||||
onLevelRestarted: (e) ->
|
||||
@setProgress 0, 0
|
||||
|
@ -286,26 +289,26 @@ module.exports = Surface = class Surface extends CocoClass
|
|||
@setPlayingCalled = true
|
||||
if @playing and @currentFrame >= (@world.totalFrames - 5)
|
||||
@currentFrame = 0
|
||||
if @fastForwarding and not @playing
|
||||
@setProgress @currentFrame / @world.totalFrames
|
||||
if @fastForwardingToFrame and not @playing
|
||||
@fastForwardingToFrame = null
|
||||
|
||||
onSetTime: (e) ->
|
||||
toFrame = @currentFrame
|
||||
if e.time?
|
||||
@worldLifespan = @world.totalFrames / @world.frameRate
|
||||
@worldLifespan = @world.frames.length / @world.frameRate
|
||||
e.ratio = e.time / @worldLifespan
|
||||
if e.ratio?
|
||||
toFrame = @world.totalFrames * e.ratio
|
||||
toFrame = @world.frames.length * e.ratio
|
||||
if e.frameOffset
|
||||
toFrame += e.frameOffset
|
||||
if e.ratioOffset
|
||||
toFrame += @world.totalFrames * e.ratioOffset
|
||||
toFrame += @world.frames.length * e.ratioOffset
|
||||
unless _.isNumber(toFrame) and not _.isNaN(toFrame)
|
||||
return console.error('set-time event', e, 'produced invalid target frame', toFrame)
|
||||
@setProgress(toFrame / @world.totalFrames, e.scrubDuration)
|
||||
@setProgress(toFrame / @world.frames.length, e.scrubDuration)
|
||||
|
||||
onFrameChanged: (force) ->
|
||||
@currentFrame = Math.min(@currentFrame, @world.totalFrames)
|
||||
@currentFrame = Math.min(@currentFrame, @world.frames.length)
|
||||
@debugDisplay?.updateFrame @currentFrame
|
||||
return if @currentFrame is @lastFrame and not force
|
||||
progress = @getProgress()
|
||||
|
@ -317,7 +320,7 @@ module.exports = Surface = class Surface extends CocoClass
|
|||
world: @world
|
||||
)
|
||||
|
||||
if @lastFrame < @world.totalFrames and @currentFrame >= @world.totalFrames - 1
|
||||
if @lastFrame < @world.frames.length and @currentFrame >= @world.totalFrames - 1
|
||||
@ended = true
|
||||
@setPaused true
|
||||
Backbone.Mediator.publish 'surface:playback-ended'
|
||||
|
@ -353,48 +356,34 @@ module.exports = Surface = class Surface extends CocoClass
|
|||
return if e.preload
|
||||
@setPaused false if @ended
|
||||
@casting = true
|
||||
@wasPlayingWhenCastingBegan = @playing
|
||||
Backbone.Mediator.publish 'level-set-playing', {playing: false}
|
||||
@setPlayingCalled = false # don't overwrite playing settings if they changed by, say, scripts
|
||||
|
||||
if @coordinateDisplay?
|
||||
@surfaceTextLayer.removeChild @coordinateDisplay
|
||||
@coordinateDisplay.destroy()
|
||||
|
||||
createjs.Tween.removeTweens(@surfaceLayer)
|
||||
createjs.Tween.get(@surfaceLayer).to({alpha: 0.9}, 1000, createjs.Ease.getPowOut(4.0))
|
||||
@setPlayingCalled = false # Don't overwrite playing settings if they changed by, say, scripts.
|
||||
@frameBeforeCast = @currentFrame
|
||||
@setProgress 0
|
||||
|
||||
onNewWorld: (event) ->
|
||||
return unless event.world.name is @world.name
|
||||
@casting = false
|
||||
if @ended and not @wasPlayingWhenCastingBegan
|
||||
@setPaused true
|
||||
else
|
||||
@spriteBoss.play()
|
||||
|
||||
# This has a tendency to break scripts that are waiting for playback to change when the level is loaded
|
||||
# so only run it after the first world is created.
|
||||
Backbone.Mediator.publish 'level-set-playing', {playing: @wasPlayingWhenCastingBegan} unless event.firstWorld or @setPlayingCalled
|
||||
Backbone.Mediator.publish 'level-set-playing', {playing: true} unless event.firstWorld or @setPlayingCalled
|
||||
|
||||
fastForwardTo = null
|
||||
if @playing
|
||||
fastForwardTo = Math.min event.world.firstChangedFrame, @currentFrame
|
||||
@currentFrame = 0
|
||||
|
||||
createjs.Tween.removeTweens(@surfaceLayer)
|
||||
f = =>
|
||||
@setWorld event.world
|
||||
@onFrameChanged(true)
|
||||
if fastForwardTo and @playing
|
||||
fastForwardToRatio = fastForwardTo / @world.totalFrames
|
||||
fastForwardToTime = fastForwardTo * @world.dt
|
||||
fastForwardSpeed = Math.max 4, fastForwardToTime / 3
|
||||
@setProgress fastForwardToRatio, 1000 * fastForwardToTime / fastForwardSpeed
|
||||
@fastForwarding = true
|
||||
createjs.Tween.get(@surfaceLayer)
|
||||
.to({alpha: 0.0}, 50)
|
||||
.call(f)
|
||||
.to({alpha: 1.0}, 2000, createjs.Ease.getPowOut(2.0))
|
||||
fastForwardBuffer = 2
|
||||
if @playing and not @realTime and (ffToFrame = Math.min(event.firstChangedFrame, @frameBeforeCast, @world.frames.length)) and ffToFrame > @currentFrame + fastForwardBuffer * @world.frameRate
|
||||
@fastForwardingToFrame = ffToFrame
|
||||
@fastForwardingSpeed = Math.max 4, 4 * 90 / (@world.maxTotalFrames * @world.dt)
|
||||
else if @realTime
|
||||
lag = (@world.frames.length - 1) * @world.dt - @world.age
|
||||
intendedLag = @world.realTimeBufferMax + @world.dt
|
||||
if lag > intendedLag * 1.2
|
||||
@fastForwardingToFrame = @world.frames.length - @world.realTimeBufferMax * @world.frameRate
|
||||
@fastForwardingSpeed = lag / intendedLag
|
||||
else
|
||||
@fastForwardingToFrame = @fastForwardingSpeed = null
|
||||
#console.log "on new world, lag", lag, "intended lag", intendedLag, "fastForwardingToFrame", @fastForwardingToFrame, "speed", @fastForwardingSpeed, "cause we are at", @world.age, "of", @world.frames.length * @world.dt
|
||||
|
||||
# initialization
|
||||
|
||||
|
@ -413,7 +402,6 @@ module.exports = Surface = class Surface extends CocoClass
|
|||
@surfaceLayer.addChild @cameraBorder = new CameraBorder bounds: @camera.bounds
|
||||
@screenLayer.addChild new Letterbox canvasWidth: canvasWidth, canvasHeight: canvasHeight
|
||||
@spriteBoss = new SpriteBoss camera: @camera, surfaceLayer: @surfaceLayer, surfaceTextLayer: @surfaceTextLayer, world: @world, thangTypes: @options.thangTypes, choosing: @options.choosing, navigateToSelection: @options.navigateToSelection, showInvisible: @options.showInvisible
|
||||
@castingScreen ?= new CastingScreen camera: @camera, layer: @screenLayer
|
||||
@playbackOverScreen ?= new PlaybackOverScreen camera: @camera, layer: @screenLayer
|
||||
@stage.enableMouseOver(10)
|
||||
@stage.addEventListener 'stagemousemove', @onMouseMove
|
||||
|
@ -428,8 +416,17 @@ module.exports = Surface = class Surface extends CocoClass
|
|||
onResize: (e) =>
|
||||
oldWidth = parseInt @canvas.attr('width'), 10
|
||||
oldHeight = parseInt @canvas.attr('height'), 10
|
||||
newWidth = @canvas.width()
|
||||
newHeight = @canvas.height()
|
||||
aspectRatio = oldWidth / oldHeight
|
||||
pageWidth = $('#page-container').width() - 17 # 17px nano scroll bar
|
||||
if @realTime
|
||||
pageHeight = $('#page-container').height() - $('#control-bar-view').outerHeight() - $('#playback-view').outerHeight()
|
||||
newWidth = Math.min pageWidth, pageHeight * aspectRatio
|
||||
newHeight = newWidth / aspectRatio
|
||||
else
|
||||
newWidth = 0.55 * pageWidth
|
||||
newHeight = newWidth / aspectRatio
|
||||
@canvas.width newWidth
|
||||
@canvas.height newHeight
|
||||
return unless newWidth > 0 and newHeight > 0
|
||||
#if InstallTrigger? # Firefox rendering performance goes down as canvas size goes up
|
||||
# newWidth = Math.min 924, newWidth
|
||||
|
@ -540,7 +537,9 @@ module.exports = Surface = class Surface extends CocoClass
|
|||
onMouseDown: (e) =>
|
||||
return if @disabled
|
||||
onBackground = not @stage.hitTest e.stageX, e.stageY
|
||||
Backbone.Mediator.publish 'surface:stage-mouse-down', onBackground: onBackground, x: e.stageX, y: e.stageY, originalEvent: e
|
||||
worldPos = @camera.screenToWorld x: e.stageX, y: e.stageY
|
||||
event = onBackground: onBackground, x: e.stageX, y: e.stageY, originalEvent: e, worldPos: worldPos
|
||||
Backbone.Mediator.publish 'surface:stage-mouse-down', event
|
||||
|
||||
onMouseUp: (e) =>
|
||||
return if @disabled
|
||||
|
@ -569,14 +568,19 @@ module.exports = Surface = class Surface extends CocoClass
|
|||
# seems to be a bug where only one object can register with the Ticker...
|
||||
oldFrame = @currentFrame
|
||||
oldWorldFrame = Math.floor oldFrame
|
||||
lastFrame = @world.totalFrames - 1
|
||||
lastFrame = @world.frames.length - 1
|
||||
while true
|
||||
Dropper.tick()
|
||||
@trailmaster.tick() if @trailmaster
|
||||
# Skip some frame updates unless we're playing and not at end (or we haven't drawn much yet)
|
||||
frameAdvanced = (@playing and @currentFrame < lastFrame) or @totalFramesDrawn < 2
|
||||
if frameAdvanced and @playing
|
||||
@currentFrame += @world.frameRate / @options.frameRate
|
||||
advanceBy = @world.frameRate / @options.frameRate
|
||||
if @fastForwardingToFrame and @currentFrame < @fastForwardingToFrame - advanceBy
|
||||
advanceBy = Math.min(@currentFrame + advanceBy * @fastForwardingSpeed, @fastForwardingToFrame) - @currentFrame
|
||||
else if @fastForwardingToFrame
|
||||
@fastForwardingToFrame = @fastForwardingSpeed = null
|
||||
@currentFrame += advanceBy
|
||||
@currentFrame = Math.min @currentFrame, lastFrame
|
||||
newWorldFrame = Math.floor @currentFrame
|
||||
worldFrameAdvanced = newWorldFrame isnt oldWorldFrame
|
||||
|
@ -603,8 +607,8 @@ module.exports = Surface = class Surface extends CocoClass
|
|||
restoreWorldState: ->
|
||||
frame = @world.getFrame(@getCurrentFrame())
|
||||
frame.restoreState()
|
||||
current = Math.max(0, Math.min(@currentFrame, @world.totalFrames - 1))
|
||||
if current - Math.floor(current) > 0.01
|
||||
current = Math.max(0, Math.min(@currentFrame, @world.frames.length - 1))
|
||||
if current - Math.floor(current) > 0.01 and Math.ceil(current) < @world.frames.length - 1
|
||||
next = Math.ceil current
|
||||
ratio = current % 1
|
||||
@world.frames[next].restorePartialState ratio if next > 1
|
||||
|
@ -614,13 +618,29 @@ module.exports = Surface = class Surface extends CocoClass
|
|||
updateState: (frameChanged) ->
|
||||
# world state must have been restored in @restoreWorldState
|
||||
@camera.updateZoom()
|
||||
@spriteBoss.update frameChanged unless @casting
|
||||
@spriteBoss.update frameChanged
|
||||
@dimmer?.setSprites @spriteBoss.sprites
|
||||
|
||||
drawCurrentFrame: (e) ->
|
||||
++@totalFramesDrawn
|
||||
@stage.update e
|
||||
|
||||
# Real-time playback
|
||||
onRealTimePlaybackStarted: (e) ->
|
||||
@realTime = true
|
||||
@onResize()
|
||||
@spriteBoss.selfWizardSprite?.toggle false
|
||||
|
||||
onRealTimePlaybackEnded: (e) ->
|
||||
@realTime = false
|
||||
@onResize()
|
||||
@spriteBoss.selfWizardSprite?.toggle true
|
||||
@canvas.removeClass 'flag-color-selected'
|
||||
|
||||
onFlagColorSelected: (e) ->
|
||||
@canvas.toggleClass 'flag-color-selected', Boolean(e.color)
|
||||
e.pos = @camera.screenToWorld @mouseScreenPos if @mouseScreenPos
|
||||
|
||||
# paths - TODO: move to SpriteBoss? but only update on frame drawing instead of on every frame update?
|
||||
|
||||
updatePaths: ->
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
IndieSprite = require 'lib/surface/IndieSprite'
|
||||
Camera = require './Camera'
|
||||
{me} = require 'lib/auth'
|
||||
|
||||
module.exports = class WizardSprite extends IndieSprite
|
||||
|
@ -35,12 +34,11 @@ module.exports = class WizardSprite extends IndieSprite
|
|||
@targetPos = @thang.pos
|
||||
if @isSelf
|
||||
@setNameLabel me.displayName()
|
||||
@setColorHue me.get('wizardColor1')
|
||||
else if options.name
|
||||
@setNameLabel options.name
|
||||
|
||||
makeIndieThang: (thangType, thangID, pos) ->
|
||||
thang = super thangType, thangID, pos
|
||||
makeIndieThang: (thangType, options) ->
|
||||
thang = super thangType, options
|
||||
thang.isSelectable = false
|
||||
thang.bobHeight = 0.75
|
||||
thang.bobTime = 2
|
||||
|
@ -59,6 +57,11 @@ module.exports = class WizardSprite extends IndieSprite
|
|||
name += " (#{@options.codeLanguage})" # TODO: move on second line, capitalize properly
|
||||
super name
|
||||
|
||||
toggle: (to) ->
|
||||
@imageObject?.visible = to
|
||||
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
|
||||
continue unless playerID is @thang.id
|
||||
|
@ -66,7 +69,6 @@ module.exports = class WizardSprite extends IndieSprite
|
|||
continue if playerID is me.id # ignore changes for self wizard sprite
|
||||
@setNameLabel state.name
|
||||
continue unless state.wizard?
|
||||
@setColorHue state.wizard.wizardColor1
|
||||
if targetID = state.wizard.targetSprite
|
||||
return console.warn 'Wizard Sprite couldn\'t find target sprite', targetID unless targetID of @options.sprites
|
||||
@setTarget @options.sprites[targetID]
|
||||
|
@ -91,17 +93,13 @@ module.exports = class WizardSprite extends IndieSprite
|
|||
@imageObject.scaleX = @imageObject.scaleY = @imageObject.alpha = 0
|
||||
createjs.Tween.get(@imageObject)
|
||||
.to({scaleX: 1, scaleY: 1, alpha: 1}, 1000, createjs.Ease.getPowInOut(2.2))
|
||||
@labels.name?.show()
|
||||
|
||||
animateOut: (callback) ->
|
||||
tween = createjs.Tween.get(@imageObject)
|
||||
.to({scaleX: 0, scaleY: 0, alpha: 0}, 1000, createjs.Ease.getPowInOut(2.2))
|
||||
tween.call(callback) if callback
|
||||
|
||||
setColorHue: (newColorHue) ->
|
||||
# TODO: is this needed any more?
|
||||
return if @colorHue is newColorHue
|
||||
@colorHue = newColorHue
|
||||
#@updateColorFilters()
|
||||
@labels.name?.hide()
|
||||
|
||||
setEditing: (@editing) ->
|
||||
if @editing
|
||||
|
|
|
@ -85,7 +85,7 @@ module.exports = class Thang
|
|||
throw new Error "Two types were specified for trackable property #{prop}: #{oldType} and #{type}."
|
||||
|
||||
keepTrackedProperty: (prop) ->
|
||||
# Hmm; can we do this faster?
|
||||
# Wish we could do this faster, but I can't think of how.
|
||||
propIndex = @trackedPropertiesKeys.indexOf prop
|
||||
if propIndex isnt -1
|
||||
@trackedPropertiesUsed[propIndex] = true
|
||||
|
@ -147,6 +147,8 @@ module.exports = class Thang
|
|||
for trackedFinalProperty in @trackedFinalProperties ? []
|
||||
# TODO: take some (but not all) of serialize logic from ThangState to handle other types
|
||||
o.finalState[trackedFinalProperty] = @[trackedFinalProperty]
|
||||
# Since we might keep tracked properties later during streaming, we need to know which we think are unused.
|
||||
o.unusedTrackedPropertyKeys = (@trackedPropertiesKeys[propIndex] for used, propIndex in @trackedPropertiesUsed when not used)
|
||||
o
|
||||
|
||||
@deserialize: (o, world, classMap) ->
|
||||
|
@ -154,6 +156,8 @@ module.exports = class Thang
|
|||
for [componentClassName, componentConfig] in o.components
|
||||
componentClass = classMap[componentClassName]
|
||||
t.addComponents [componentClass, componentConfig]
|
||||
t.unusedTrackedPropertyKeys = o.unusedTrackedPropertyKeys
|
||||
t.unusedTrackedPropertyValues = (t[prop] for prop in o.unusedTrackedPropertyKeys)
|
||||
for prop, val of o.finalState
|
||||
# TODO: take some (but not all) of deserialize logic from ThangState to handle other types
|
||||
t[prop] = val
|
||||
|
@ -164,7 +168,16 @@ module.exports = class Thang
|
|||
|
||||
getSpriteOptions: ->
|
||||
colorConfigs = @world?.getTeamColors() or {}
|
||||
options = {}
|
||||
if @team and colorConfigs[@team]
|
||||
options.colorConfig = {team: colorConfigs[@team]}
|
||||
options = {colorConfig: {}}
|
||||
if @team and teamColor = colorConfigs[@team]
|
||||
options.colorConfig.team = teamColor
|
||||
if @color and color = @grabColorConfig @color
|
||||
options.colorConfig.color = color
|
||||
options
|
||||
|
||||
grabColorConfig: (color) ->
|
||||
{
|
||||
green: {hue: 0.33, saturation: 0.5, lightness: 0.5}
|
||||
black: {hue: 0, saturation: 0, lightness: 0.25}
|
||||
violet: {hue: 0.83, saturation: 0.5, lightness: 0.5}
|
||||
}[color]
|
||||
|
|
|
@ -64,7 +64,10 @@ module.exports = class ThangState
|
|||
# Get the property, whether we have it stored in @props or in @trackedPropertyValues. Optimize it.
|
||||
# Figured based on http://jsperf.com/object-vs-array-vs-native-linked-list/13 that it should be faster with small arrays to do the indexOf reads (each up to 24x faster) than to do a single object read, and then we don't have to maintain an extra @props object; just keep array
|
||||
propIndex = @trackedPropertyKeys.indexOf prop
|
||||
return null if propIndex is -1
|
||||
if propIndex is -1
|
||||
initialPropIndex = @thang.unusedTrackedPropertyKeys.indexOf prop
|
||||
return null if initialPropIndex is -1
|
||||
return @thang.unusedTrackedPropertyValues[initialPropIndex]
|
||||
value = @props[propIndex]
|
||||
return value if value isnt undefined or @hasRestored
|
||||
return @props[propIndex] = @getStoredProp propIndex
|
||||
|
@ -73,22 +76,28 @@ module.exports = class ThangState
|
|||
# Restore trackedProperties' values to @thang, retrieving them from @trackedPropertyValues if needed. Optimize it.
|
||||
return @ if @thang._state is @ and not @thang.partialState
|
||||
unless @hasRestored # Restoring in a deserialized World for first time
|
||||
for prop, propIndex in @thang.unusedTrackedPropertyKeys when @trackedPropertyKeys.indexOf(prop) is -1
|
||||
@thang[prop] = @thang.unusedTrackedPropertyValues[propIndex]
|
||||
props = []
|
||||
for prop, propIndex in @trackedPropertyKeys
|
||||
type = @trackedPropertyTypes[propIndex]
|
||||
storage = @trackedPropertyValues[propIndex]
|
||||
props.push(@thang[prop] = @getStoredProp propIndex, type, storage)
|
||||
props.push @thang[prop] = @getStoredProp propIndex, type, storage
|
||||
#console.log @frameIndex, @thang.id, prop, propIndex, type, storage, 'got', @thang[prop]
|
||||
@props = props
|
||||
@trackedPropertyTypes = @trackedPropertyValues = @specialKeysToValues = null # leave @trackedPropertyKeys for indexing
|
||||
@hasRestored = true
|
||||
else # Restoring later times
|
||||
for prop, propIndex in @thang.unusedTrackedPropertyKeys when @trackedPropertyKeys.indexOf(prop) is -1
|
||||
@thang[prop] = @thang.unusedTrackedPropertyValues[propIndex]
|
||||
for prop, propIndex in @trackedPropertyKeys
|
||||
@thang[prop] = @props[propIndex]
|
||||
@thang.partialState = false
|
||||
@
|
||||
|
||||
restorePartial: (ratio) ->
|
||||
# Don't think we need to worry about unusedTrackedPropertyValues here.
|
||||
# If it's not tracked yet, it'll very rarely partially change between frames; we can afford to miss the first one.
|
||||
inverse = 1 - ratio
|
||||
for prop, propIndex in @trackedPropertyKeys when prop is 'pos' or prop is 'rotation'
|
||||
if @hasRestored
|
||||
|
|
|
@ -10,8 +10,11 @@ WorldScriptNote = require './world_script_note'
|
|||
{now, consolidateThangs, typedArraySupport} = require './world_utils'
|
||||
Component = require 'lib/world/component'
|
||||
System = require 'lib/world/system'
|
||||
PROGRESS_UPDATE_INTERVAL = 200
|
||||
DESERIALIZATION_INTERVAL = 20
|
||||
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
|
||||
ITEM_ORIGINAL = '53e12043b82921000051cdf9'
|
||||
|
||||
module.exports = class World
|
||||
|
@ -21,7 +24,9 @@ module.exports = class World
|
|||
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
|
||||
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}
|
||||
|
@ -33,6 +38,7 @@ module.exports = class World
|
|||
@systems = []
|
||||
@systemMap = {}
|
||||
@scriptNotes = []
|
||||
@flagHistory = []
|
||||
@rand = new Rand 0 # Existence System may change this seed
|
||||
@frames = [new WorldFrame(@, 0)]
|
||||
|
||||
|
@ -65,10 +71,11 @@ module.exports = class World
|
|||
@thangs[i] = thang
|
||||
@thangMap[thang.id] = thang
|
||||
|
||||
thangDialogueSounds: ->
|
||||
if @frames.length < @totalFrames then throw new Error('World should be over before grabbing dialogue')
|
||||
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
|
||||
|
@ -85,50 +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
|
||||
if loadUntilFrame
|
||||
frameToLoadUntil = loadUntilFrame + 1
|
||||
else
|
||||
frameToLoadUntil = @totalFrames
|
||||
@worldLoadStartTime ?= t1
|
||||
@lastRealTimeUpdate ?= 0
|
||||
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
|
||||
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
|
||||
while i < frameToLoadUntil and 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
|
||||
++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 = []
|
||||
t2 = now()
|
||||
if t2 - t1 > PROGRESS_UPDATE_INTERVAL
|
||||
loadProgressCallback? i / @totalFrames unless @preloading
|
||||
t1 = t2
|
||||
if t2 - @t0 > 1000
|
||||
console.log ' Loaded', i, 'of', @totalFrames, '(+' + (t2 - @t0).toFixed(0) + 'ms)'
|
||||
@t0 = t2
|
||||
continueFn = =>
|
||||
return if @destroyed
|
||||
if loadUntilFrame
|
||||
@loadFrames(loadedCallback,errorCallback,loadProgressCallback, skipDeferredLoading, loadUntilFrame)
|
||||
else
|
||||
@loadFrames(loadedCallback, errorCallback, loadProgressCallback, skipDeferredLoading)
|
||||
if skipDeferredLoading
|
||||
continueFn()
|
||||
else
|
||||
setTimeout(continueFn, 0)
|
||||
return
|
||||
@finishLoadingFrames loadProgressCallback, loadedCallback
|
||||
|
||||
finishLoadingFrames: (loadProgressCallback, loadedCallback) ->
|
||||
unless @debugging
|
||||
@ended = true
|
||||
system.finish @thangs for system in @systems
|
||||
|
@ -136,6 +123,53 @@ module.exports = class World
|
|||
loadProgressCallback? 1
|
||||
loadedCallback()
|
||||
|
||||
shouldDelayRealTimeSimulation: (t) ->
|
||||
return false unless @realTime
|
||||
timeSinceStart = t - @worldLoadStartTime
|
||||
timeLoaded = @frames.length * @dt * 1000
|
||||
timeBuffered = timeLoaded - timeSinceStart
|
||||
timeBuffered > REAL_TIME_BUFFER_MAX
|
||||
|
||||
shouldUpdateRealTimePlayback: (t) ->
|
||||
return false unless @realTime
|
||||
return false if @frames.length * @dt is @lastRealTimeUpdate
|
||||
timeLoaded = @frames.length * @dt * 1000
|
||||
timeSinceStart = t - @worldLoadStartTime
|
||||
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
|
||||
|
@ -143,6 +177,9 @@ module.exports = class World
|
|||
abort: ->
|
||||
@aborted = true
|
||||
|
||||
addFlagEvent: (flagEvent) ->
|
||||
@flagHistory.push flagEvent
|
||||
|
||||
loadFromLevel: (level, willSimulate=true) ->
|
||||
@levelComponents = level.levelComponents
|
||||
@thangTypes = level.thangTypes
|
||||
|
@ -289,7 +326,9 @@ module.exports = class World
|
|||
|
||||
serialize: ->
|
||||
# Code hotspot; optimize it
|
||||
if @frames.length < @totalFrames then throw new Error('World Should Be Over Before Serialization')
|
||||
startFrame = @framesSerializedSoFar
|
||||
endFrame = @frames.length
|
||||
#console.log "... world serializing frames from", startFrame, "to", endFrame, "of", @totalFrames
|
||||
[transferableObjects, nontransferableObjects] = [0, 0]
|
||||
o = {totalFrames: @totalFrames, maxTotalFrames: @maxTotalFrames, frameRate: @frameRate, dt: @dt, victory: @victory, userCodeMap: {}, trackedProperties: {}}
|
||||
o.trackedProperties[prop] = @[prop] for prop in @trackedProperties or []
|
||||
|
@ -305,12 +344,14 @@ module.exports = class World
|
|||
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
|
||||
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 = @frames.length
|
||||
nFrames = endFrame - startFrame
|
||||
streaming = nFrames < @totalFrames
|
||||
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).
|
||||
# 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 = []
|
||||
|
@ -357,8 +398,8 @@ module.exports = class World
|
|||
|
||||
t1 = now()
|
||||
o.frameHashes = []
|
||||
for frame, frameIndex in @frames
|
||||
o.frameHashes.push frame.serialize(frameIndex, o.trackedPropertiesThangIDs, o.trackedPropertiesPerThangIndices, o.trackedPropertiesPerThangTypes, trackedPropertiesPerThangValues, o.specialValuesToKeys, o.specialKeysToValues)
|
||||
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
|
||||
|
@ -368,27 +409,33 @@ module.exports = class World
|
|||
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) / @frames.length).toFixed(3), 'ms per frame'
|
||||
#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
|
||||
{serializedWorld: o, transferableObjects: [o.storageBuffer]}
|
||||
{serializedWorld: o, transferableObjects: [o.storageBuffer], startFrame: startFrame, endFrame: endFrame}
|
||||
|
||||
@deserialize: (o, classMap, oldSerializedWorldFrames, finishedWorldCallback) ->
|
||||
@deserialize: (o, classMap, oldSerializedWorldFrames, finishedWorldCallback, startFrame, endFrame, 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()
|
||||
w = new World o.userCodeMap, classMap
|
||||
nFrames = endFrame - startFrame
|
||||
w = streamingWorld ? new World o.userCodeMap, classMap
|
||||
[w.totalFrames, w.maxTotalFrames, w.frameRate, w.dt, w.scriptNotes, w.victory] = [o.totalFrames, o.maxTotalFrames, o.frameRate, o.dt, o.scriptNotes ? [], o.victory]
|
||||
w[prop] = val for prop, val of o.trackedProperties
|
||||
|
||||
perf.t1 = now()
|
||||
if w.thangs.length
|
||||
for thangConfig in o.thangs when not w.thangMap[thangConfig.id]
|
||||
w.thangs.push thang = Thang.deserialize(thangConfig, w, classMap)
|
||||
w.setThang thang
|
||||
else
|
||||
w.thangs = (Thang.deserialize(thang, w, classMap) 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)
|
||||
|
@ -400,7 +447,7 @@ module.exports = class World
|
|||
o.trackedPropertiesPerThangValues.push (trackedPropertiesValues = [])
|
||||
trackedPropertiesValuesOffsets = o.trackedPropertiesPerThangValuesOffsets[thangIndex]
|
||||
for type, propIndex in trackedPropertyTypes
|
||||
storage = ThangState.createArrayForType(type, o.totalFrames, o.storageBuffer, trackedPropertiesValuesOffsets[propIndex])[0]
|
||||
storage = ThangState.createArrayForType(type, nFrames, o.storageBuffer, trackedPropertiesValuesOffsets[propIndex])[0]
|
||||
unless typedArraySupport
|
||||
# This could be more efficient
|
||||
i = trackedPropertiesValuesOffsets[propIndex]
|
||||
|
@ -409,45 +456,52 @@ module.exports = class World
|
|||
perf.t3 = now()
|
||||
|
||||
perf.batches = 0
|
||||
w.frames = []
|
||||
_.delay @deserializeSomeFrames, 1, o, w, finishedWorldCallback, perf
|
||||
perf.framesCPUTime = 0
|
||||
w.frames = [] unless streamingWorld
|
||||
clearTimeout @deserializationTimeout if @deserializationTimeout
|
||||
@deserializationTimeout = _.delay @deserializeSomeFrames, 1, o, w, finishedWorldCallback, perf, startFrame, endFrame
|
||||
|
||||
# Spread deserialization out across multiple calls so the interface stays responsive
|
||||
@deserializeSomeFrames: (o, w, finishedWorldCallback, perf) =>
|
||||
@deserializeSomeFrames: (o, w, finishedWorldCallback, perf, startFrame, endFrame) =>
|
||||
++perf.batches
|
||||
startTime = now()
|
||||
for frameIndex in [w.frames.length ... o.totalFrames]
|
||||
w.frames.push WorldFrame.deserialize(w, frameIndex, o.trackedPropertiesThangIDs, o.trackedPropertiesThangs, o.trackedPropertiesPerThangKeys, o.trackedPropertiesPerThangTypes, o.trackedPropertiesPerThangValues, o.specialKeysToValues, o.frameHashes[frameIndex])
|
||||
if (now() - startTime) > DESERIALIZATION_INTERVAL
|
||||
_.delay @deserializeSomeFrames, 1, o, w, finishedWorldCallback, perf
|
||||
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
|
||||
@finishDeserializing w, finishedWorldCallback, perf
|
||||
@deserializationTimeout = null
|
||||
perf.framesCPUTime += elapsed
|
||||
@finishDeserializing w, finishedWorldCallback, perf, startFrame, endFrame
|
||||
|
||||
@finishDeserializing: (w, finishedWorldCallback, perf) ->
|
||||
@finishDeserializing: (w, finishedWorldCallback, perf, startFrame, endFrame) ->
|
||||
perf.t4 = now()
|
||||
w.ended = true
|
||||
w.getFrame(w.totalFrames - 1).restoreState()
|
||||
perf.t5 = now()
|
||||
console.log 'Deserialization:', (perf.t5 - perf.t0).toFixed(0) + 'ms (' + ((perf.t5 - perf.t0) / w.frames.length).toFixed(3) + 'ms per frame).', perf.batches, 'batches.'
|
||||
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'
|
||||
console.log ' Deserializing--restoring last WorldFrame:', (perf.t5 - perf.t4).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 @firstChangedFrame = 0 unless oldWorld
|
||||
return 0 unless oldWorld
|
||||
for newFrame, i in @frames
|
||||
oldFrame = oldWorld.frames[i]
|
||||
break unless oldFrame and newFrame.hash is oldFrame.hash
|
||||
@firstChangedFrame = 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
|
||||
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
|
||||
firstChangedFrame
|
||||
|
||||
pointsForThang: (thangID, frameStart=0, frameEnd=null, camera=null, resolution=4) ->
|
||||
# Optimized
|
||||
|
|
|
@ -60,9 +60,9 @@ module.exports = class WorldFrame
|
|||
thangState.serialize(frameIndex, trackedPropertiesPerThangIndices[thangIndex], trackedPropertiesPerThangTypes[thangIndex], trackedPropertiesPerThangValues[thangIndex], specialValuesToKeys, specialKeysToValues)
|
||||
@hash
|
||||
|
||||
@deserialize: (world, frameIndex, trackedPropertiesThangIDs, trackedPropertiesThangs, trackedPropertiesPerThangKeys, trackedPropertiesPerThangTypes, trackedPropertiesPerThangValues, specialKeysToValues, hash) ->
|
||||
@deserialize: (world, frameIndex, trackedPropertiesThangIDs, trackedPropertiesThangs, trackedPropertiesPerThangKeys, trackedPropertiesPerThangTypes, trackedPropertiesPerThangValues, specialKeysToValues, hash, age) ->
|
||||
# Optimize
|
||||
wf = new WorldFrame null, world.dt * frameIndex
|
||||
wf = new WorldFrame null, age
|
||||
wf.world = world
|
||||
wf.hash = hash
|
||||
for thangID, thangIndex in trackedPropertiesThangIDs
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -874,7 +874,7 @@ module.exports = nativeDescription: "Português (Portugal)", englishDescription:
|
|||
tournament_ended: "O Torneio acabou"
|
||||
tournament_rules: "Regras do Torneio"
|
||||
tournament_blurb: "Escreva código, recolha ouro, construa exércitos, esmague inimigos, ganhe prémios e melhore a sua carreira no nosso torneio $40,000 Greed! Confira os detalhes"
|
||||
# tournament_blurb_criss_cross: "Win bids, construct paths, outwit opponents, grab gems, and upgrade your career in our Criss-Cross tournament! Check out the details"
|
||||
tournament_blurb_criss_cross: "Ganhe ofertas, construa caminhos, supere os adversários, apanhe gemas e melhore a sua carreira no nosso torneio Criss-Cross! Confira os detalhes"
|
||||
tournament_blurb_blog: "no nosso blog"
|
||||
rules: "Regras"
|
||||
winners: "Vencedores"
|
||||
|
@ -945,7 +945,7 @@ module.exports = nativeDescription: "Português (Portugal)", englishDescription:
|
|||
candidate_sessions: "Sessões de Candidatos"
|
||||
user_remark: "Observação do Utilizador"
|
||||
versions: "Versões"
|
||||
# items: "Items"
|
||||
items: "Itens"
|
||||
|
||||
delta:
|
||||
added: "Adicionados/as"
|
||||
|
|
|
@ -26,14 +26,14 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
|||
minutes: "分钟"
|
||||
hour: "小时"
|
||||
hours: "小时"
|
||||
# day: "day"
|
||||
# days: "days"
|
||||
# week: "week"
|
||||
# weeks: "weeks"
|
||||
# month: "month"
|
||||
# months: "months"
|
||||
# year: "year"
|
||||
# years: "years"
|
||||
day: "日"
|
||||
days: "日"
|
||||
week: "星期"
|
||||
weeks: "星期"
|
||||
month: "月"
|
||||
months: "月"
|
||||
year: "年"
|
||||
years: "年"
|
||||
|
||||
modal:
|
||||
close: "关闭"
|
||||
|
@ -49,9 +49,9 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
|||
blog: "博客"
|
||||
forum: "论坛"
|
||||
account: "账号"
|
||||
# profile: "Profile"
|
||||
# stats: "Stats"
|
||||
# code: "Code"
|
||||
profile: "资料"
|
||||
stats: "成就"
|
||||
code: "代码"
|
||||
admin: "管理"
|
||||
home: "首页"
|
||||
contribute: "贡献"
|
||||
|
@ -90,7 +90,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
|||
sign_up: "注册"
|
||||
log_in: "登录"
|
||||
social_signup: "或者,你可以通过Facebook或G+注册:"
|
||||
# required: "You need to log in before you can go that way."
|
||||
required: "在做这件事情之前你必须先注册。"
|
||||
|
||||
home:
|
||||
slogan: "通过游戏学习编程"
|
||||
|
@ -98,17 +98,17 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
|||
no_mobile: "CodeCombat 不是针对手机设备设计的,所以可能无法达到最好的体验!"
|
||||
play: "开始游戏"
|
||||
old_browser: "噢, 你的浏览器太老了, 不能运行CodeCombat. 抱歉!"
|
||||
old_browser_suffix: "尽管你可以多试几次, 但也许不会管用."
|
||||
old_browser_suffix: "你可以继续重试下去,但八成不起作用,更新浏览器吧亲~"
|
||||
campaign: "战役模式"
|
||||
for_beginners: "适合初学者"
|
||||
multiplayer: "多人游戏"
|
||||
for_developers: "适合开发者"
|
||||
# javascript_blurb: "The language of the web. Great for writing websites, web apps, HTML5 games, and servers."
|
||||
# python_blurb: "Simple yet powerful, Python is a great general purpose programming language."
|
||||
# coffeescript_blurb: "Nicer JavaScript syntax."
|
||||
# clojure_blurb: "A modern Lisp."
|
||||
# lua_blurb: "Game scripting language."
|
||||
# io_blurb: "Simple but obscure."
|
||||
javascript_blurb: "为web开发而生的语言。 非常适合制作网站, 网站apps, HTML5游戏或开发服务器。"
|
||||
python_blurb: "简单而强大, Python是一个伟大的通用编程语言。"
|
||||
coffeescript_blurb: "一种更好的JavaScript语法."
|
||||
clojure_blurb: "一种现代的列表处理语言。"
|
||||
lua_blurb: "一种游戏脚本语言。"
|
||||
io_blurb: "简单而晦涩。"
|
||||
|
||||
play:
|
||||
choose_your_level: "选择关卡"
|
||||
|
@ -158,7 +158,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
|||
trim: "条纹"
|
||||
cloud: "云"
|
||||
team: "队伍"
|
||||
spell: "魔法球"
|
||||
spell: "法球"
|
||||
boots: "鞋子"
|
||||
hue: "颜色"
|
||||
saturation: "饱和度"
|
||||
|
@ -178,17 +178,17 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
|||
wizard_color: "巫师 衣服 颜色"
|
||||
new_password: "新密码"
|
||||
new_password_verify: "核实"
|
||||
email_subscriptions: "邮箱验证"
|
||||
# email_subscriptions_none: "No Email Subscriptions."
|
||||
email_subscriptions: "邮箱订阅"
|
||||
email_subscriptions_none: "取消订阅"
|
||||
email_announcements: "通知"
|
||||
email_announcements_description: "接收关于 CodeCombat 的邮件。"
|
||||
email_notifications: "通知"
|
||||
# email_notifications_summary: "Controls for personalized, automatic email notifications related to your CodeCombat activity."
|
||||
email_notifications_summary: "私人定制, 自动通知与您有关的 CodeCombat 活动。"
|
||||
email_any_notes: "任何通知"
|
||||
email_any_notes_description: "取消接收所有活动提醒邮件"
|
||||
# email_news: "News"
|
||||
email_news: "新消息"
|
||||
email_recruit_notes: "工作机会"
|
||||
# email_recruit_notes_description: "If you play really well, we may contact you about getting you a (better) job."
|
||||
email_recruit_notes_description: "如果你干的不错, 我们会联系并提供你更好的工作。"
|
||||
contributor_emails: "贡献者通知"
|
||||
contribute_prefix: "我们在寻找志同道合的人!请到"
|
||||
contribute_page: "贡献页面"
|
||||
|
@ -197,17 +197,17 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
|||
error_saving: "保存时出错"
|
||||
saved: "更改已保存"
|
||||
password_mismatch: "密码不匹配。"
|
||||
# password_repeat: "Please repeat your password."
|
||||
password_repeat: "请重新键入密码。"
|
||||
job_profile: "工作经历"
|
||||
job_profile_approved: "你填写的工作经历将由CodeCombat认证. 雇主将看到这些信息,除非你将它设置为不启用状态或者连续四周没有更新."
|
||||
job_profile_explanation: "你好! 填写这些信息, 我们将使用它帮你寻找一份软件开发的工作."
|
||||
# sample_profile: "See a sample profile"
|
||||
job_profile_explanation: "你好! 请填写下列信息, 我们将使用它帮你寻找一份软件开发的工作."
|
||||
sample_profile: "查看示例"
|
||||
view_profile: "浏览个人信息"
|
||||
|
||||
account_profile:
|
||||
# settings: "Settings"
|
||||
# edit_profile: "Edit Profile"
|
||||
# done_editing: "Done Editing"
|
||||
settings: "设置"
|
||||
edit_profile: "编辑资料"
|
||||
done_editing: "完成编辑"
|
||||
profile_for_prefix: "关于他的基本资料:"
|
||||
profile_for_suffix: ""
|
||||
# featured: "Featured"
|
||||
|
@ -215,8 +215,8 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
|||
looking_for: "寻找"
|
||||
last_updated: "最后一次更新:"
|
||||
contact: "联系"
|
||||
# active: "Looking for interview offers now"
|
||||
# inactive: "Not looking for offers right now"
|
||||
active: "正期待面试offer"
|
||||
inactive: "并不期待面试offer"
|
||||
# complete: "complete"
|
||||
# next: "Next"
|
||||
# next_city: "city?"
|
||||
|
@ -485,31 +485,31 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
|||
|
||||
multiplayer:
|
||||
multiplayer_title: "多人游戏设置"
|
||||
# multiplayer_toggle: "Enable multiplayer"
|
||||
# multiplayer_toggle_description: "Allow others to join your game."
|
||||
multiplayer_toggle: "开启多人模式"
|
||||
multiplayer_toggle_description: "允许其他人加入游戏。"
|
||||
multiplayer_link_description: "把这个链接告诉小伙伴们,一起玩吧。"
|
||||
multiplayer_hint_label: "提示:"
|
||||
multiplayer_hint: " 点击全选,然后按 Apple-C(苹果电脑)或 Ctrl-C 复制链接。"
|
||||
multiplayer_coming_soon: "多人游戏的更多特性!"
|
||||
# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard."
|
||||
multiplayer_sign_in_leaderboard: "注册并登录账号,就可以将你的成就发布到排行榜上。"
|
||||
|
||||
# keyboard_shortcuts:
|
||||
# keyboard_shortcuts: "Keyboard Shortcuts"
|
||||
# space: "Space"
|
||||
# enter: "Enter"
|
||||
keyboard_shortcuts:
|
||||
keyboard_shortcuts: "热键"
|
||||
space: "空格"
|
||||
enter: "回车"
|
||||
# escape: "Escape"
|
||||
# cast_spell: "Cast current spell."
|
||||
cast_spell: "演示当前咒语"
|
||||
# continue_script: "Continue past current script."
|
||||
# skip_scripts: "Skip past all skippable scripts."
|
||||
# toggle_playback: "Toggle play/pause."
|
||||
toggle_playback: "继续/暂停按钮"
|
||||
# scrub_playback: "Scrub back and forward through time."
|
||||
# single_scrub_playback: "Scrub back and forward through time by a single frame."
|
||||
# scrub_execution: "Scrub through current spell execution."
|
||||
# toggle_debug: "Toggle debug display."
|
||||
# toggle_grid: "Toggle grid overlay."
|
||||
# toggle_pathfinding: "Toggle pathfinding overlay."
|
||||
# beautify: "Beautify your code by standardizing its formatting."
|
||||
# move_wizard: "Move your Wizard around the level."
|
||||
beautify: "利用标准编码格式美化你的代码。"
|
||||
move_wizard: "在关卡中移动你的巫师角色。"
|
||||
|
||||
admin:
|
||||
av_title: "管理员视图"
|
||||
|
@ -521,43 +521,43 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
|||
av_other_debug_base_url: "Base(用于调试 base.jade)"
|
||||
u_title: "用户列表"
|
||||
lg_title: "最新的游戏"
|
||||
# clas: "CLAs"
|
||||
clas: "贡献者许可协议"
|
||||
|
||||
community:
|
||||
main_title: "CodeCombat 社区"
|
||||
# introduction: "Check out the ways you can get involved below and decide what sounds the most fun. We look forward to working with you!"
|
||||
# level_editor_prefix: "Use the CodeCombat"
|
||||
# level_editor_suffix: "to create and edit levels. Users have created levels for their classes, friends, hackathons, students, and siblings. If create a new level sounds intimidating you can start by forking one of ours!"
|
||||
# thang_editor_prefix: "We call units within the game 'thangs'. Use the"
|
||||
# thang_editor_suffix: "to modify the CodeCombat source artwork. Allow units to throw projectiles, alter the direction of an animation, change a unit's hit points, or upload your own vector sprites."
|
||||
# article_editor_prefix: "See a mistake in some of our docs? Want to make some instructions for your own creations? Check out the"
|
||||
# article_editor_suffix: "and help CodeCombat players get the most out of their playtime."
|
||||
# find_us: "Find us on these sites"
|
||||
# contribute_to_the_project: "Contribute to the project"
|
||||
introduction: "看看下面这些你可以参与的项目,如果有你喜欢的就加入进来吧。 我们期待着与您一起工作。"
|
||||
level_editor_prefix: "使用"
|
||||
level_editor_suffix: "来创建和编辑关卡。你可以通过这个工具来给你的同学,朋友,兄弟姐妹们设计谜题,或者用于教学或比赛。如果你觉得直接开始建立一个关卡可能非常困难,那么可以先从一个现成(但尚未完成)的关卡开始做起。"
|
||||
thang_editor_prefix: "我们管游戏中的单位叫 '实体'。 利用"
|
||||
thang_editor_suffix: "来改良 CodeCombat 中的原材料。让游戏中的东西可以被捡起来扔出去,改变游戏动画的指向,调整一些东西的生命值,或上传您自制的素材。"
|
||||
article_editor_prefix: "你在游戏中发现了错误了吗?想要自己设计一些指令吗?来看看我们的"
|
||||
article_editor_suffix: "来帮助玩家从游戏中学到更多的知识。"
|
||||
find_us: "通过这些站点联系我们"
|
||||
contribute_to_the_project: "为项目做贡献"
|
||||
|
||||
editor:
|
||||
main_title: "CodeCombat 编辑器"
|
||||
main_description: "建立你自己的关卡、 战役、单元和教育内容。我们会提供所有你需要的工具!"
|
||||
article_title: "提示编辑器"
|
||||
article_description: "编写提示,让玩家可以使用编程概念来通过各种关卡和战役。"
|
||||
thang_title: "物体编辑器"
|
||||
thang_description: "创建单元,并定义单元的逻辑、图形和音频。目前只支持导入 Flash 导出的矢量图形。"
|
||||
main_description: "建立你自己的关卡、 战役、单元和新手教程。我们会提供所有你需要的工具!"
|
||||
article_title: "指令编辑器"
|
||||
article_description: "编写指令,让玩家可以使用编程概念来通过各种关卡和战役。"
|
||||
thang_title: "实体编辑器"
|
||||
thang_description: "创建单位,并定义单位的逻辑、图形和音频。目前只支持导入 Flash 导出的矢量图形。"
|
||||
level_title: "关卡编辑器"
|
||||
level_description: "所有用来创造所有难度的关卡的工具,包括脚本、上传音频和构建自定义逻辑。"
|
||||
# achievement_title: "Achievement Editor"
|
||||
achievement_title: "目标编辑器"
|
||||
got_questions: "使用CodeCombat编辑器有问题?"
|
||||
contact_us: "联系我们!"
|
||||
hipchat_prefix: "你也可以在这里找到我们"
|
||||
hipchat_url: "HipChat 房间。"
|
||||
hipchat_url: "HipChat 聊天室。"
|
||||
back: "后退"
|
||||
revert: "还原"
|
||||
revert_models: "还原模式"
|
||||
# pick_a_terrain: "Pick A Terrain"
|
||||
# small: "Small"
|
||||
# grassy: "Grassy"
|
||||
pick_a_terrain: "选择地形"
|
||||
small: "小的"
|
||||
grassy: "草地"
|
||||
fork_title: "派生新版本"
|
||||
fork_creating: "正在执行派生..."
|
||||
# randomize: "Randomize"
|
||||
randomize: "随机生成"
|
||||
more: "更多"
|
||||
wiki: "维基"
|
||||
live_chat: "在线聊天"
|
||||
|
@ -591,29 +591,29 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
|||
new_article_title: "创建一个新物品"
|
||||
new_thang_title: "创建一个新物品类型"
|
||||
new_level_title: "创建一个新关卡"
|
||||
# new_article_title_login: "Log In to Create a New Article"
|
||||
# new_thang_title_login: "Log In to Create a New Thang Type"
|
||||
# new_level_title_login: "Log In to Create a New Level"
|
||||
# new_achievement_title: "Create a New Achievement"
|
||||
# new_achievement_title_login: "Log In to Create a New Achievement"
|
||||
new_article_title_login: "登录以创建新指令"
|
||||
new_thang_title_login: "登录以创建新实体"
|
||||
new_level_title_login: "登录以创建新关卡"
|
||||
new_achievement_title: "创建新目标"
|
||||
new_achievement_title_login: "登录以创建新目标"
|
||||
article_search_title: "在这里搜索物品"
|
||||
thang_search_title: "在这里搜索物品类型"
|
||||
level_search_title: "在这里搜索关卡"
|
||||
# achievement_search_title: "Search Achievements"
|
||||
achievement_search_title: "搜索目标"
|
||||
read_only_warning2: "提示:你不能保存任何编辑,因为你没有登陆"
|
||||
# no_achievements: "No achievements have been added for this level yet."
|
||||
no_achievements: "这个关卡还没有被赋予任何目标。"
|
||||
# achievement_query_misc: "Key achievement off of miscellanea"
|
||||
# achievement_query_goals: "Key achievement off of level goals"
|
||||
# level_completion: "Level Completion"
|
||||
level_completion: "关卡完成"
|
||||
|
||||
article:
|
||||
edit_btn_preview: "预览"
|
||||
edit_article_title: "编辑提示"
|
||||
|
||||
general:
|
||||
and: "和"
|
||||
and: "与"
|
||||
name: "名字"
|
||||
# date: "Date"
|
||||
date: "日期"
|
||||
body: "正文"
|
||||
version: "版本"
|
||||
commit_msg: "提交信息"
|
||||
|
@ -657,7 +657,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
|||
why_paragraph_4: "如果你一定要对游戏上瘾,那就对这个游戏上瘾,然后成为科技时代的法师吧。"
|
||||
why_ending: "再说,这游戏还是免费的。"
|
||||
why_ending_url: "开始学习法术!"
|
||||
# george_description: "CEO, business guy, web designer, game designer, and champion of beginning programmers everywhere."
|
||||
george_description: "这里到处都是CEO, 商人, 网站设计师, 游戏设计师和编程新星。"
|
||||
# scott_description: "Programmer extraordinaire, software architect, kitchen wizard, and master of finances. Scott is the reasonable one."
|
||||
# nick_description: "Programming wizard, eccentric motivation mage, and upside-down experimenter. Nick can do anything and chooses to build CodeCombat."
|
||||
# jeremy_description: "Customer support mage, usability tester, and community organizer; you've probably already spoken with Jeremy."
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -110,8 +110,8 @@ NoteGroupSchema = c.object {title: 'Note Group', description: 'A group of notes
|
|||
letterbox: {type: 'boolean', title: 'Letterbox', description: 'Turn letterbox mode on or off. Disables surface and playback controls.'}
|
||||
|
||||
goals: c.object {title: 'Goals (Old)', description: 'Deprecated. Goals added here have no effect. Add goals in the level settings instead.'},
|
||||
add: c.array {title: 'Add', description: 'Deprecated. Goals added here have no effect. Add goals in the level settings instead.'}, GoalSchema
|
||||
remove: c.array {title: 'Remove', description: 'Deprecated. Goals removed here have no effect. Adjust goals in the level settings instead.'}, GoalSchema
|
||||
add: c.array {title: 'Add', description: 'Deprecated. Goals added here have no effect. Add goals in the level settings instead.'}, {}
|
||||
remove: c.array {title: 'Remove', description: 'Deprecated. Goals removed here have no effect. Adjust goals in the level settings instead.'}, {}
|
||||
|
||||
playback: c.object {title: 'Playback', description: 'Control the playback of the level.'},
|
||||
playing: {type: 'boolean', title: 'Set Playing', description: 'Set whether playback is playing or paused.'}
|
||||
|
|
|
@ -57,6 +57,48 @@ module.exports =
|
|||
'level:victory-hidden':
|
||||
{} # TODO schema
|
||||
|
||||
'level:flag-color-selected':
|
||||
type: 'object'
|
||||
additionalProperties: false
|
||||
properties:
|
||||
color:
|
||||
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
|
||||
required: ['x', 'y']
|
||||
properties:
|
||||
x: {type: 'number'}
|
||||
y: {type: 'number'}
|
||||
|
||||
'level:flag-updated':
|
||||
type: 'object'
|
||||
additionalProperties: false
|
||||
required: ['player', 'color', 'time', 'active']
|
||||
properties:
|
||||
player:
|
||||
type: 'string'
|
||||
team:
|
||||
type: 'string'
|
||||
color:
|
||||
type: 'string'
|
||||
enum: ['green', 'black', 'violet']
|
||||
time:
|
||||
type: 'number'
|
||||
minimum: 0
|
||||
active:
|
||||
type: 'boolean'
|
||||
pos:
|
||||
type: 'object'
|
||||
additionalProperties: false
|
||||
required: ['x', 'y']
|
||||
properties:
|
||||
x: {type: 'number'}
|
||||
y: {type: 'number'}
|
||||
|
||||
'next-game-pressed':
|
||||
{} # TODO schema
|
||||
|
||||
|
@ -105,6 +147,14 @@ module.exports =
|
|||
'playback:manually-scrubbed':
|
||||
{} # TODO schema
|
||||
|
||||
'playback:real-time-playback-started':
|
||||
type: 'object'
|
||||
additionalProperties: false
|
||||
|
||||
'playback:real-time-playback-ended':
|
||||
type: 'object'
|
||||
additionalProperties: false
|
||||
|
||||
'change:editor-config':
|
||||
{} # TODO schema
|
||||
|
||||
|
|
|
@ -94,3 +94,15 @@ module.exports = # /app/lib/surface
|
|||
|
||||
'echo-all-wizard-sprites':
|
||||
{} # TODO schema
|
||||
|
||||
'surface:flag-appeared':
|
||||
type: 'object'
|
||||
additionalProperties: false
|
||||
required: ['sprite']
|
||||
properties:
|
||||
sprite:
|
||||
type: 'object'
|
||||
|
||||
'surface:remove-selected-flag':
|
||||
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
|
||||
|
||||
|
|
|
@ -11,5 +11,8 @@ module.exports =
|
|||
'god:new-world-created':
|
||||
{} # TODO schema
|
||||
|
||||
'god:streaming-world-updated':
|
||||
{} # TODO schema
|
||||
|
||||
'god:world-load-progress-changed':
|
||||
{} # TODO schema
|
||||
|
|
|
@ -225,10 +225,6 @@ table.table
|
|||
.ui-slider-handle
|
||||
border: 1px solid black !important
|
||||
|
||||
.flag-cursor
|
||||
cursor: crosshair
|
||||
|
||||
|
||||
// Fonts
|
||||
|
||||
.header-font
|
||||
|
|
|
@ -10,6 +10,34 @@ body.is-playing
|
|||
margin: 0 auto
|
||||
@include user-select(none)
|
||||
|
||||
&.real-time
|
||||
// Hmm, somehow the #page-container is cutting us off by ~17px on the right, looks a bit off.
|
||||
|
||||
#canvas-wrapper
|
||||
width: 100%
|
||||
canvas#surface
|
||||
margin: 0 auto
|
||||
#control-bar-view
|
||||
width: 100%
|
||||
#playback-view
|
||||
$flags-width: 200px
|
||||
width: 90%
|
||||
width: -webkit-calc(100% - $flags-width)
|
||||
width: calc(100% - $flags-width)
|
||||
left: $flags-width
|
||||
#code-area, #thang-hud, #goals-view
|
||||
display: none
|
||||
visibility: hidden
|
||||
#gold-view
|
||||
right: 1%
|
||||
#control-bar-view .title
|
||||
left: 20%
|
||||
width: 60%
|
||||
text-align: center
|
||||
|
||||
.level-content
|
||||
margin: 0px auto
|
||||
|
||||
.level-content
|
||||
position: relative
|
||||
|
||||
|
@ -17,12 +45,16 @@ body.is-playing
|
|||
width: 55%
|
||||
position: relative
|
||||
overflow: hidden
|
||||
@include transition(0.5s ease-out)
|
||||
|
||||
canvas#surface
|
||||
background-color: #333
|
||||
width: 100%
|
||||
display: block
|
||||
z-index: 1
|
||||
@include transition(0.5s ease-out)
|
||||
|
||||
&.flag-color-selected
|
||||
cursor: crosshair
|
||||
|
||||
min-width: 1024px
|
||||
position: relative
|
||||
|
|
|
@ -6,9 +6,8 @@
|
|||
position: absolute
|
||||
right: 46%
|
||||
top: 42px
|
||||
user-select: none
|
||||
-webkit-user-select: none
|
||||
@include transition(box-shadow .2s linear)
|
||||
@include user-select(none)
|
||||
padding: 4px
|
||||
background: transparent url(/images/level/gold_background.png) no-repeat
|
||||
background-size: 100% 100%
|
||||
|
|
53
app/styles/play/level/level-flags-view.sass
Normal file
53
app/styles/play/level/level-flags-view.sass
Normal file
|
@ -0,0 +1,53 @@
|
|||
@import "app/styles/mixins"
|
||||
@import "app/styles/bootstrap/mixins"
|
||||
|
||||
#level-flags-view
|
||||
display: none
|
||||
position: absolute
|
||||
bottom: 0
|
||||
left: 0
|
||||
width: 200px
|
||||
// Width must match level.sass playback offset
|
||||
z-index: 1
|
||||
@include transition(box-shadow .2s linear)
|
||||
@include user-select(none)
|
||||
padding: 4px
|
||||
background: transparent url(/images/level/gold_background.png) no-repeat
|
||||
background-size: 100% 100%
|
||||
border-radius: 4px
|
||||
|
||||
&:hover
|
||||
box-shadow: 2px 2px 2px black
|
||||
|
||||
.flag-button
|
||||
margin: 3px
|
||||
font-size: 14px
|
||||
position: relative
|
||||
padding: 2px 15px 18px 15px
|
||||
margin-left: 5px
|
||||
|
||||
.glyphicon
|
||||
font-size: 24px
|
||||
|
||||
.flag-shortcut
|
||||
text-decoration: underline
|
||||
|
||||
&.green-flag
|
||||
.glyphicon, .flag-shortcut
|
||||
color: hsl(120, 50%, 50%)
|
||||
&.black-flag
|
||||
.glyphicon, .flag-shortcut
|
||||
color: hsl(0, 0%, 25%)
|
||||
&.violet-flag
|
||||
.glyphicon, .flag-shortcut
|
||||
color: hsl(300, 50%, 50%)
|
||||
|
||||
.flag-caption
|
||||
position: absolute
|
||||
background-color: rgba(0, 0, 0, 0.1)
|
||||
color: black
|
||||
bottom: 0
|
||||
left: 0
|
||||
width: 100%
|
||||
border-bottom-right-radius: 6px
|
||||
border-bottom-left-radius: 6px
|
|
@ -97,6 +97,8 @@
|
|||
background-image: none
|
||||
border-radius: 0
|
||||
border: 0
|
||||
// Can't do this transition because handle then jitters, but would be good for streaming.
|
||||
//@include transition(width .2s linear)
|
||||
|
||||
&.disabled
|
||||
cursor: default
|
||||
|
|
|
@ -82,7 +82,9 @@
|
|||
height: 40px
|
||||
|
||||
.cast-button
|
||||
width: 100%
|
||||
width: 80%
|
||||
width: -webkit-calc(100% - 40px)
|
||||
width: calc(100% - 40px)
|
||||
border-top-left-radius: 6px
|
||||
border-bottom-left-radius: 6px
|
||||
|
||||
|
@ -92,3 +94,8 @@
|
|||
font-size: 14px
|
||||
@media screen and (max-width: 1024px)
|
||||
font-size: 12px
|
||||
|
||||
.cast-real-time-button
|
||||
width: 20%
|
||||
width: -webkit-calc(40px)
|
||||
width: calc(40px)
|
||||
|
|
|
@ -19,11 +19,11 @@
|
|||
.title
|
||||
position: absolute
|
||||
display: inline-block
|
||||
margin-left: 50%
|
||||
right: 0
|
||||
color: #BEBEBE
|
||||
line-height: 15px
|
||||
left: 0
|
||||
left: 20%
|
||||
width: 60%
|
||||
text-align: center
|
||||
|
||||
max-width: 1920px
|
||||
margin: 0 auto
|
||||
|
|
|
@ -13,6 +13,8 @@
|
|||
#canvas-top-gradient.gradient
|
||||
#goals-view.secret
|
||||
|
||||
#level-flags-view.secret
|
||||
|
||||
#gold-view.secret.expanded
|
||||
|
||||
#level-chat-view
|
||||
|
|
15
app/templates/play/level/level-flags-view.jade
Normal file
15
app/templates/play/level/level-flags-view.jade
Normal file
|
@ -0,0 +1,15 @@
|
|||
button.flag-button.btn.btn-lg.green-flag(title="G: Place a green flag")
|
||||
span.glyphicon.glyphicon-flag
|
||||
span.flag-caption
|
||||
span.flag-shortcut G
|
||||
| reen
|
||||
button.flag-button.btn.btn-lg.black-flag(title="B: Place a black flag")
|
||||
span.glyphicon.glyphicon-flag
|
||||
span.flag-caption
|
||||
span.flag-shortcut B
|
||||
| lack
|
||||
button.flag-button.btn.btn-lg.violet-flag(title="V: Place a violet flag")
|
||||
span.glyphicon.glyphicon-flag
|
||||
span.flag-caption
|
||||
span.flag-shortcut V
|
||||
| iolet
|
|
@ -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,3 +1,5 @@
|
|||
div.btn-group.btn-group-lg.cast-button-group.dropup
|
||||
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-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
|
|
@ -83,5 +83,5 @@ module.exports = class SearchView extends RootView
|
|||
|
||||
newModel: (e) ->
|
||||
modal = new NewModelModal model: @model, modelLabel: @modelLabel
|
||||
modal.once 'success', @onNewModelSaved
|
||||
modal.once 'model-created', @onNewModelSaved
|
||||
@openModalView modal
|
||||
|
|
|
@ -55,6 +55,7 @@ module.exports = class SpectateLevelView extends RootView
|
|||
'surface:world-set-up': 'onSurfaceSetUpNewWorld'
|
||||
'level:set-team': 'setTeam'
|
||||
'god:new-world-created': 'loadSoundsForWorld'
|
||||
'god:streaming-world-updated': 'loadSoundsForWorld'
|
||||
'next-game-pressed': 'onNextGamePressed'
|
||||
'level:started': 'onLevelStarted'
|
||||
'level:loading-view-unveiled': 'onLoadingViewUnveiled'
|
||||
|
|
|
@ -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])
|
||||
|
||||
|
|
74
app/views/play/level/LevelFlagsView.coffee
Normal file
74
app/views/play/level/LevelFlagsView.coffee
Normal file
|
@ -0,0 +1,74 @@
|
|||
CocoView = require 'views/kinds/CocoView'
|
||||
template = require 'templates/play/level/level-flags-view'
|
||||
{me} = require 'lib/auth'
|
||||
|
||||
module.exports = class LevelFlagsView extends CocoView
|
||||
id: 'level-flags-view'
|
||||
template: template
|
||||
|
||||
subscriptions:
|
||||
'playback:real-time-playback-started': 'onRealTimePlaybackStarted'
|
||||
'playback:real-time-playback-ended': 'onRealTimePlaybackEnded'
|
||||
'surface:stage-mouse-down': 'onStageMouseDown'
|
||||
'god:new-world-created': 'onNewWorld'
|
||||
'god:streaming-world-updated': 'onNewWorld'
|
||||
'surface:remove-flag': 'onRemoveFlag'
|
||||
|
||||
events:
|
||||
'click .green-flag': -> @onFlagSelected color: 'green', source: 'button'
|
||||
'click .black-flag': -> @onFlagSelected color: 'black', source: 'button'
|
||||
'click .violet-flag': -> @onFlagSelected color: 'violet', source: 'button'
|
||||
|
||||
shortcuts:
|
||||
'g': -> @onFlagSelected color: 'green', source: 'shortcut'
|
||||
'b': -> @onFlagSelected color: 'black', source: 'shortcut'
|
||||
'v': -> @onFlagSelected color: 'violet', source: 'shortcut'
|
||||
'esc': -> @onFlagSelected color: null, source: 'shortcut'
|
||||
'delete, del, backspace': 'onDeletePressed'
|
||||
|
||||
constructor: (options) ->
|
||||
super options
|
||||
@world = options.world
|
||||
|
||||
onRealTimePlaybackStarted: (e) ->
|
||||
@realTime = true
|
||||
@$el.show()
|
||||
@flags = {}
|
||||
@flagHistory = []
|
||||
|
||||
onRealTimePlaybackEnded: (e) ->
|
||||
@onFlagSelected color: null
|
||||
@realTime = false
|
||||
@$el.hide()
|
||||
|
||||
onFlagSelected: (e) ->
|
||||
return unless @realTime
|
||||
color = if e.color is @flagColor then null else e.color
|
||||
@flagColor = color
|
||||
Backbone.Mediator.publish 'level:flag-color-selected', color: color
|
||||
@$el.find('.flag-button').removeClass('active')
|
||||
@$el.find(".#{color}-flag").addClass('active') if color
|
||||
|
||||
onStageMouseDown: (e) ->
|
||||
return unless @flagColor and @realTime
|
||||
pos = x: e.worldPos.x, y: e.worldPos.y
|
||||
flag = player: me.id, team: me.team, color: @flagColor, pos: pos, time: @world.dt * @world.frames.length, active: true
|
||||
@flags[@flagColor] = flag
|
||||
@flagHistory.push flag
|
||||
Backbone.Mediator.publish 'level:flag-updated', flag
|
||||
#console.log 'trying to place flag at', @world.age, 'and think it will happen by', flag.time
|
||||
|
||||
onDeletePressed: (e) ->
|
||||
return unless @realTime
|
||||
Backbone.Mediator.publish 'surface:remove-selected-flag', {}
|
||||
|
||||
onRemoveFlag: (e) ->
|
||||
delete @flags[e.color]
|
||||
flag = player: me.id, team: me.team, color: e.color, time: @world.dt * @world.frames.length, active: false
|
||||
@flagHistory.push flag
|
||||
Backbone.Mediator.publish 'level:flag-updated', flag
|
||||
#console.log e.color, 'deleted at time', flag.time
|
||||
|
||||
onNewWorld: (event) ->
|
||||
return unless event.world.name is @world.name
|
||||
@world = @options.world = event.world
|
|
@ -70,6 +70,8 @@ module.exports = class LevelHUDView extends CocoView
|
|||
@thang = e.world.thangMap[@thang.id] if @thang
|
||||
if hadThang and not @thang
|
||||
@setThang null, null
|
||||
else if @thang
|
||||
@createActions() # Make sure it updates its actions.
|
||||
|
||||
setThang: (thang, thangType) ->
|
||||
unless @speaker
|
||||
|
|
|
@ -22,8 +22,10 @@ module.exports = class LevelPlaybackView extends CocoView
|
|||
'level-toggle-grid': 'onToggleGrid'
|
||||
'surface:frame-changed': 'onFrameChanged'
|
||||
'god:new-world-created': 'onNewWorld'
|
||||
'god:streaming-world-updated': 'onNewWorld'
|
||||
'level-set-letterbox': 'onSetLetterbox'
|
||||
'tome:cast-spells': 'onCastSpells'
|
||||
'tome:cast-spells': 'onTomeCast'
|
||||
'playback:real-time-playback-ended': 'onRealTimePlaybackEnded'
|
||||
|
||||
events:
|
||||
'click #debug-toggle': 'onToggleDebug'
|
||||
|
@ -36,7 +38,7 @@ module.exports = class LevelPlaybackView extends CocoView
|
|||
'click #zoom-out-button': -> Backbone.Mediator.publish('camera-zoom-out') unless @shouldIgnore()
|
||||
'click #volume-button': 'onToggleVolume'
|
||||
'click #play-button': 'onTogglePlay'
|
||||
'click': -> Backbone.Mediator.publish 'tome:focus-editor'
|
||||
'click': -> Backbone.Mediator.publish 'tome:focus-editor' unless @realTime
|
||||
'mouseenter #timeProgress': 'onProgressEnter'
|
||||
'mouseleave #timeProgress': 'onProgressLeave'
|
||||
'mousemove #timeProgress': 'onProgressHover'
|
||||
|
@ -114,6 +116,15 @@ module.exports = class LevelPlaybackView extends CocoView
|
|||
ua = navigator.userAgent.toLowerCase()
|
||||
if /safari/.test(ua) and not /chrome/.test(ua)
|
||||
@$el.find('.toggle-fullscreen').hide()
|
||||
@timePopup ?= new HoverPopup
|
||||
t = $.i18n.t
|
||||
@second = t 'units.second'
|
||||
@seconds = t 'units.seconds'
|
||||
@minute = t 'units.minute'
|
||||
@minutes = t 'units.minutes'
|
||||
@goto = t 'play_level.time_goto'
|
||||
@current = t 'play_level.time_current'
|
||||
@total = t 'play_level.time_total'
|
||||
|
||||
updatePopupContent: ->
|
||||
@timePopup?.updateContent "<h2>#{@timeToString @newTime}</h2>#{@formatTime(@current, @currentTime)}<br/>#{@formatTime(@total, @totalTime)}"
|
||||
|
@ -142,32 +153,34 @@ module.exports = class LevelPlaybackView extends CocoView
|
|||
@$el.find('#music-button').toggleClass('music-on', me.get('music'))
|
||||
|
||||
onSetLetterbox: (e) ->
|
||||
buttons = @$el.find '#play-button, .scrubber-handle'
|
||||
buttons.css 'visibility', if e.on then 'hidden' else 'visible'
|
||||
return if @realTime
|
||||
@togglePlaybackControls !e.on
|
||||
@disabled = e.on
|
||||
|
||||
togglePlaybackControls: (to) ->
|
||||
buttons = @$el.find '#play-button, .scrubber-handle'
|
||||
buttons.css 'visibility', if to then 'visible' else 'hidden'
|
||||
|
||||
onTomeCast: (e) ->
|
||||
return unless e.realTime
|
||||
@realTime = true
|
||||
@togglePlaybackControls false
|
||||
Backbone.Mediator.publish 'playback:real-time-playback-started', {}
|
||||
|
||||
onWindowResize: (s...) =>
|
||||
@barWidth = $('.progress', @$el).width()
|
||||
|
||||
onNewWorld: (e) ->
|
||||
@totalTime = e.world.totalFrames / e.world.frameRate
|
||||
pct = parseInt(100 * e.world.totalFrames / e.world.maxTotalFrames) + '%'
|
||||
@updateBarWidth e.world.frames.length, e.world.maxTotalFrames, e.world.dt
|
||||
|
||||
updateBarWidth: (loadedFrameCount, maxTotalFrames, dt) ->
|
||||
@totalTime = loadedFrameCount * dt
|
||||
pct = parseInt(100 * loadedFrameCount / maxTotalFrames) + '%'
|
||||
@barWidth = $('.progress', @$el).css('width', pct).show().width()
|
||||
@casting = false
|
||||
$('.scrubber .progress', @$el).slider('enable', true)
|
||||
@newTime = 0
|
||||
@currentTime = 0
|
||||
|
||||
@timePopup ?= new HoverPopup
|
||||
|
||||
t = $.i18n.t
|
||||
@second = t 'units.second'
|
||||
@seconds = t 'units.seconds'
|
||||
@minute = t 'units.minute'
|
||||
@minutes = t 'units.minutes'
|
||||
@goto = t 'play_level.time_goto'
|
||||
@current = t 'play_level.time_current'
|
||||
@total = t 'play_level.time_total'
|
||||
@lastLoadedFrameCount = loadedFrameCount
|
||||
|
||||
onToggleDebug: ->
|
||||
return if @shouldIgnore()
|
||||
|
@ -188,11 +201,6 @@ module.exports = class LevelPlaybackView extends CocoView
|
|||
onViewKeyboardShortcuts: ->
|
||||
@openModalView new KeyboardShortcutsModal()
|
||||
|
||||
onCastSpells: (e) ->
|
||||
return if e.preload
|
||||
@casting = true
|
||||
@$progressScrubber.slider('disable', true)
|
||||
|
||||
onDisableControls: (e) ->
|
||||
if not e.controls or 'playback' in e.controls
|
||||
@disabled = true
|
||||
|
@ -205,6 +213,7 @@ module.exports = class LevelPlaybackView extends CocoView
|
|||
$('#volume-button', @$el).removeClass('disabled')
|
||||
|
||||
onEnableControls: (e) ->
|
||||
return if @realTime
|
||||
if not e.controls or 'playback' in e.controls
|
||||
@disabled = false
|
||||
$('button', @$el).removeClass('disabled')
|
||||
|
@ -255,7 +264,7 @@ module.exports = class LevelPlaybackView extends CocoView
|
|||
# @currentTime = @totalTime if Math.abs(@totalTime - @currentTime) < 0.04
|
||||
@updatePopupContent() if @timePopup?.shown
|
||||
|
||||
@updateProgress(e.progress)
|
||||
@updateProgress(e.progress, e.world)
|
||||
@updatePlayButton(e.progress)
|
||||
@lastProgress = e.progress
|
||||
|
||||
|
@ -277,16 +286,28 @@ module.exports = class LevelPlaybackView extends CocoView
|
|||
if @timePopup and Math.abs(@currentTime - @newTime) < 1 and not @timePopup.shown
|
||||
@timePopup.show()
|
||||
|
||||
updateProgress: (progress) ->
|
||||
$('.scrubber .progress-bar', @$el).css('width', "#{progress*100}%")
|
||||
updateProgress: (progress, world) ->
|
||||
if world.frames.length isnt @lastLoadedFrameCount
|
||||
@updateBarWidth world.frames.length, world.maxTotalFrames, world.dt
|
||||
wasLoaded = @worldCompletelyLoaded
|
||||
@worldCompletelyLoaded = world.frames.length is world.totalFrames
|
||||
if @realTime and @worldCompletelyLoaded and not wasLoaded
|
||||
Backbone.Mediator.publish 'playback:real-time-playback-ended', {}
|
||||
$('.scrubber .progress-bar', @$el).css('width', "#{progress * 100}%")
|
||||
|
||||
updatePlayButton: (progress) ->
|
||||
if progress >= 0.99 and @lastProgress < 0.99
|
||||
if @worldCompletelyLoaded and progress >= 0.99 and @lastProgress < 0.99
|
||||
$('#play-button').removeClass('playing').removeClass('paused').addClass('ended')
|
||||
Backbone.Mediator.publish 'playback:real-time-playback-ended', {} if @realTime
|
||||
if progress < 0.99 and @lastProgress >= 0.99
|
||||
b = $('#play-button').removeClass('ended')
|
||||
if @playing then b.addClass('playing') else b.addClass('paused')
|
||||
|
||||
onRealTimePlaybackEnded: (e) ->
|
||||
return unless @realTime
|
||||
@realTime = false
|
||||
@togglePlaybackControls true
|
||||
|
||||
onSetDebug: (e) ->
|
||||
flag = $('#debug-toggle i.icon-ok')
|
||||
flag.toggleClass 'invisible', not e.debug
|
||||
|
@ -299,20 +320,22 @@ module.exports = class LevelPlaybackView extends CocoView
|
|||
|
||||
hookUpScrubber: ->
|
||||
@sliderIncrements = 500 # max slider width before we skip pixels
|
||||
@clickingSlider = false # whether the mouse has been pressed down without moving
|
||||
@$progressScrubber.slider(
|
||||
max: @sliderIncrements
|
||||
animate: 'slow'
|
||||
slide: (event, ui) =>
|
||||
return if @shouldIgnore()
|
||||
@scrubTo ui.value / @sliderIncrements
|
||||
@slideCount += 1
|
||||
|
||||
start: (event, ui) =>
|
||||
return if @shouldIgnore()
|
||||
@slideCount = 0
|
||||
@wasPlaying = @playing
|
||||
Backbone.Mediator.publish 'level-set-playing', {playing: false}
|
||||
|
||||
stop: (event, ui) =>
|
||||
return if @shouldIgnore()
|
||||
@actualProgress = ui.value / @sliderIncrements
|
||||
Backbone.Mediator.publish 'playback:manually-scrubbed', ratio: @actualProgress
|
||||
Backbone.Mediator.publish 'level-set-playing', {playing: @wasPlaying}
|
||||
|
@ -329,7 +352,7 @@ module.exports = class LevelPlaybackView extends CocoView
|
|||
return if @shouldIgnore()
|
||||
Backbone.Mediator.publish 'level-set-time', ratio: ratio, scrubDuration: duration
|
||||
|
||||
shouldIgnore: -> return @disabled or @casting or false
|
||||
shouldIgnore: -> return @disabled or @realTime
|
||||
|
||||
onTogglePlay: (e) ->
|
||||
e?.preventDefault()
|
||||
|
|
|
@ -29,6 +29,7 @@ HUDView = require './LevelHUDView'
|
|||
ControlBarView = require './ControlBarView'
|
||||
LevelPlaybackView = require './LevelPlaybackView'
|
||||
GoalsView = require './LevelGoalsView'
|
||||
LevelFlagsView = require './LevelFlagsView'
|
||||
GoldView = require './LevelGoldView'
|
||||
VictoryModal = require './modal/VictoryModal'
|
||||
InfiniteLoopModal = require './modal/InfiniteLoopModal'
|
||||
|
@ -53,6 +54,7 @@ module.exports = class PlayLevelView extends RootView
|
|||
'level-disable-controls': 'onDisableControls'
|
||||
'level-enable-controls': 'onEnableControls'
|
||||
'god:new-world-created': 'onNewWorld'
|
||||
'god:streaming-world-updated': 'onNewWorld'
|
||||
'god:infinite-loop': 'onInfiniteLoop'
|
||||
'level-reload-from-data': 'onLevelReloadFromData'
|
||||
'level-reload-thang-type': 'onLevelReloadThangType'
|
||||
|
@ -63,6 +65,8 @@ module.exports = class PlayLevelView extends RootView
|
|||
'level:set-team': 'setTeam'
|
||||
'level:started': 'onLevelStarted'
|
||||
'level:loading-view-unveiled': 'onLoadingViewUnveiled'
|
||||
'playback:real-time-playback-started': 'onRealTimePlaybackStarted'
|
||||
'playback:real-time-playback-ended': 'onRealTimePlaybackEnded'
|
||||
|
||||
events:
|
||||
'click #level-done-button': 'onDonePressed'
|
||||
|
@ -232,6 +236,7 @@ module.exports = class PlayLevelView extends RootView
|
|||
@insertSubView @tome = new TomeView levelID: @levelID, session: @session, otherSession: @otherSession, thangs: @world.thangs, supermodel: @supermodel
|
||||
@insertSubView new LevelPlaybackView session: @session
|
||||
@insertSubView new GoalsView {}
|
||||
@insertSubView new LevelFlagsView world: @world
|
||||
@insertSubView new GoldView {}
|
||||
@insertSubView new HUDView {}
|
||||
@insertSubView new ChatView levelID: @levelID, sessionID: @session.id, session: @session
|
||||
|
@ -507,11 +512,25 @@ 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
|
||||
|
||||
# Real-time playback
|
||||
onRealTimePlaybackStarted: (e) ->
|
||||
@$el.addClass('real-time').focus()
|
||||
@onWindowResize()
|
||||
|
||||
onRealTimePlaybackEnded: (e) ->
|
||||
@$el.removeClass 'real-time'
|
||||
@onWindowResize()
|
||||
|
||||
destroy: ->
|
||||
@levelLoader?.destroy()
|
||||
@surface?.destroy()
|
||||
|
|
|
@ -70,7 +70,7 @@ module.exports = class ThangAvatarView extends CocoView
|
|||
@setProblems myProblems.length, worstLevel
|
||||
|
||||
onNewWorld: (e) ->
|
||||
@options.thang = @thang = e.world.thangMap[@thang.id] if @thang
|
||||
@options.thang = @thang = e.world.thangMap[@thang.id] if @thang and e.world.thangMap[@thang.id]
|
||||
|
||||
destroy: ->
|
||||
super()
|
||||
|
|
|
@ -8,7 +8,7 @@ module.exports = class CastButtonView extends CocoView
|
|||
|
||||
events:
|
||||
'click .cast-button': 'onCastButtonClick'
|
||||
'click .autocast-delays a': 'onCastOptionsClick'
|
||||
'click .cast-real-time-button': 'onCastRealTimeButtonClick'
|
||||
|
||||
subscriptions:
|
||||
'tome:spell-changed': 'onSpellChanged'
|
||||
|
@ -21,11 +21,15 @@ module.exports = class CastButtonView extends CocoView
|
|||
@spells = options.spells
|
||||
@levelID = options.levelID
|
||||
@castShortcut = '⇧↵'
|
||||
@castShortcutVerbose = 'Shift+Enter'
|
||||
|
||||
getRenderData: (context={}) ->
|
||||
context = super context
|
||||
context.castShortcutVerbose = @castShortcutVerbose
|
||||
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: ->
|
||||
|
@ -34,9 +38,7 @@ module.exports = class CastButtonView extends CocoView
|
|||
@castButtonGroup = $('.cast-button-group', @$el)
|
||||
@castOptions = $('.autocast-delays', @$el)
|
||||
delay = me.get('autocastDelay')
|
||||
delay ?= 5000
|
||||
unless @levelID in ['rescue-mission', 'grab-the-mushroom', 'drink-me', 'its-a-trap', 'break-the-prison', 'taunt', 'cowardly-taunt', 'commanding-followers', 'mobile-artillery']
|
||||
delay = 90019001
|
||||
delay ?= 90019001
|
||||
@setAutocastDelay delay
|
||||
|
||||
attachTo: (spellView) ->
|
||||
|
@ -45,6 +47,9 @@ module.exports = class CastButtonView extends CocoView
|
|||
onCastButtonClick: (e) ->
|
||||
Backbone.Mediator.publish 'tome:manual-cast', {}
|
||||
|
||||
onCastRealTimeButtonClick: (e) ->
|
||||
Backbone.Mediator.publish 'tome:manual-cast', {realTime: true}
|
||||
|
||||
onCastOptionsClick: (e) =>
|
||||
Backbone.Mediator.publish 'tome:focus-editor'
|
||||
@castButtonGroup.removeClass 'open'
|
||||
|
@ -98,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,6 +40,7 @@ module.exports = class Spell
|
|||
if @permissions.readwrite.length and sessionSource = @session.getSourceFor(@spellKey)
|
||||
@source = sessionSource
|
||||
@thangs = {}
|
||||
if @canRead() # We can avoid creating these views if we'll never use them.
|
||||
@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
|
||||
|
@ -48,8 +49,8 @@ module.exports = class Spell
|
|||
Backbone.Mediator.publish 'tome:spell-created', spell: @
|
||||
|
||||
destroy: ->
|
||||
@view.destroy()
|
||||
@tabView.destroy()
|
||||
@view?.destroy()
|
||||
@tabView?.destroy()
|
||||
@thangs = null
|
||||
@worker = null
|
||||
|
||||
|
@ -72,7 +73,7 @@ module.exports = class Spell
|
|||
(team ? me.team) in @permissions.readwrite
|
||||
|
||||
getSource: ->
|
||||
@view.getSource()
|
||||
@view?.getSource() ? @source
|
||||
|
||||
transpile: (source) ->
|
||||
if source
|
||||
|
|
|
@ -114,6 +114,10 @@ module.exports = class SpellView extends CocoView
|
|||
name: 'run-code'
|
||||
bindKey: {win: 'Shift-Enter|Ctrl-Enter', mac: 'Shift-Enter|Command-Enter|Ctrl-Enter'}
|
||||
exec: -> Backbone.Mediator.publish 'tome:manual-cast', {}
|
||||
addCommand
|
||||
name: 'run-code-real-time'
|
||||
bindKey: {win: 'Ctrl-Shift-Enter', mac: 'Command-Shift-Enter|Ctrl-Shift-Enter'}
|
||||
exec: -> Backbone.Mediator.publish 'tome:manual-cast', {realTime: true}
|
||||
addCommand
|
||||
name: 'no-op'
|
||||
bindKey: {win: 'Ctrl-S', mac: 'Command-S|Ctrl-S'}
|
||||
|
@ -269,8 +273,8 @@ module.exports = class SpellView extends CocoView
|
|||
# @addZatannaSnippets()
|
||||
@highlightCurrentLine()
|
||||
|
||||
cast: (preload=false) ->
|
||||
Backbone.Mediator.publish 'tome:cast-spell', spell: @spell, thang: @thang, preload: preload
|
||||
cast: (preload=false, realTime=false) ->
|
||||
Backbone.Mediator.publish 'tome:cast-spell', spell: @spell, thang: @thang, preload: preload, realTime: realTime
|
||||
|
||||
notifySpellChanged: =>
|
||||
Backbone.Mediator.publish 'tome:spell-changed', spell: @spell
|
||||
|
@ -285,7 +289,7 @@ module.exports = class SpellView extends CocoView
|
|||
|
||||
onManualCast: (e) ->
|
||||
cast = @$el.parent().length
|
||||
@recompile cast
|
||||
@recompile cast, e.realTime
|
||||
@focus() if cast
|
||||
|
||||
onCodeReload: (e) ->
|
||||
|
@ -299,12 +303,15 @@ module.exports = class SpellView extends CocoView
|
|||
recompileIfNeeded: =>
|
||||
@recompile() if @recompileNeeded
|
||||
|
||||
recompile: (cast=true) ->
|
||||
recompile: (cast=true, realTime=false) ->
|
||||
@setRecompileNeeded false
|
||||
return if @spell.source is @getSource()
|
||||
hasChanged = @spell.source isnt @getSource()
|
||||
if hasChanged
|
||||
@spell.transpile @getSource()
|
||||
@updateAether true, false
|
||||
@cast() if cast
|
||||
if cast and (hasChanged or realTime)
|
||||
@cast(false, realTime)
|
||||
if hasChanged
|
||||
@notifySpellChanged()
|
||||
|
||||
updateACEText: (source) ->
|
||||
|
@ -472,7 +479,7 @@ module.exports = class SpellView extends CocoView
|
|||
onSessionWillSave: (e) ->
|
||||
return unless @spellHasChanged
|
||||
setTimeout(=>
|
||||
unless @spellHasChanged
|
||||
unless @destroyed or @spellHasChanged
|
||||
@$el.find('.save-status').finish().show().fadeOut(2000)
|
||||
, 1000)
|
||||
@spellHasChanged = false
|
||||
|
@ -510,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()
|
||||
|
|
|
@ -142,6 +142,9 @@ module.exports = class TomeView extends CocoView
|
|||
else
|
||||
delete @thangSpells[thangID]
|
||||
spell.removeThangID thangID for spell in @spells
|
||||
for spellKey, spell of @spells when not spell.canRead() # Make sure these get transpiled (they have no views).
|
||||
spell.transpile()
|
||||
spell.loaded = true
|
||||
null
|
||||
|
||||
onSpellLoaded: (e) ->
|
||||
|
@ -152,10 +155,10 @@ module.exports = class TomeView extends CocoView
|
|||
onCastSpell: (e) ->
|
||||
# A single spell is cast.
|
||||
# Hmm; do we need to make sure other spells are all cast here?
|
||||
@cast e?.preload
|
||||
@cast e?.preload, e?.realTime
|
||||
|
||||
cast: (preload=false) ->
|
||||
Backbone.Mediator.publish 'tome:cast-spells', spells: @spells, preload: preload
|
||||
cast: (preload=false, realTime=false) ->
|
||||
Backbone.Mediator.publish 'tome:cast-spells', spells: @spells, preload: preload, realTime: realTime
|
||||
|
||||
onToggleSpellList: (e) ->
|
||||
@spellList.rerenderEntries()
|
||||
|
@ -209,6 +212,7 @@ module.exports = class TomeView extends CocoView
|
|||
|
||||
spellFor: (thang, spellName) ->
|
||||
return null unless thang?.isProgrammable
|
||||
return unless @thangSpells[thang.id] # Probably in streaming mode, where we don't update until it's done.
|
||||
selectedThangSpells = (@spells[spellKey] for spellKey in @thangSpells[thang.id])
|
||||
if spellName
|
||||
spell = _.find selectedThangSpells, {name: spellName}
|
||||
|
|
Loading…
Reference in a new issue