Merge branch 'master' into production

This commit is contained in:
Nick Winter 2014-08-25 14:02:53 -07:00
commit 14de27c923
56 changed files with 941 additions and 449 deletions

View file

@ -2,7 +2,7 @@ var window = self;
var Global = self; var Global = self;
importScripts("/javascripts/lodash.js", "/javascripts/aether.js"); 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 aethers = {};
var createAether = function (spellKey, options) var createAether = function (spellKey, options)

View file

@ -300,7 +300,7 @@ self.setupDebugWorldToRunUntilFrame = function (args) {
} }
Math.random = self.debugWorld.rand.randf; // so user code is predictable Math.random = self.debugWorld.rand.randf; // so user code is predictable
Aether.replaceBuiltin("Math", Math); Aether.replaceBuiltin("Math", Math);
replacedLoDash = _.runInContext(self); var replacedLoDash = _.runInContext(self);
for(var key in replacedLoDash) for(var key in replacedLoDash)
_[key] = replacedLoDash[key]; _[key] = replacedLoDash[key];
} }
@ -346,6 +346,7 @@ self.runWorld = function runWorld(args) {
self.world.loadFromLevel(args.level, true); self.world.loadFromLevel(args.level, true);
self.world.preloading = args.preload; self.world.preloading = args.preload;
self.world.headless = args.headless; self.world.headless = args.headless;
self.world.realTime = args.realTime;
self.goalManager = new GoalManager(self.world); self.goalManager = new GoalManager(self.world);
self.goalManager.setGoals(args.goals); self.goalManager.setGoals(args.goals);
self.goalManager.setCode(args.userCodeMap); 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 Math.random = self.world.rand.randf; // so user code is predictable
Aether.replaceBuiltin("Math", Math); Aether.replaceBuiltin("Math", Math);
replacedLoDash = _.runInContext(self); var replacedLoDash = _.runInContext(self);
for(var key in replacedLoDash) for(var key in replacedLoDash)
_[key] = replacedLoDash[key]; _[key] = replacedLoDash[key];
self.postMessage({type: 'start-load-frames'}); self.postMessage({type: 'start-load-frames'});
self.world.loadFrames(self.onWorldLoaded, self.onWorldError, self.onWorldLoadProgress); 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() { self.onWorldLoaded = function onWorldLoaded() {
if(self.world.framesSerializedSoFar == self.world.frames.length) return;
if(self.world.ended)
self.goalManager.worldGenerationEnded(); self.goalManager.worldGenerationEnded();
var goalStates = self.goalManager.getGoalStates(); var goalStates = self.goalManager.getGoalStates();
if(self.world.ended)
self.postMessage({type: 'end-load-frames', goalStates: goalStates}); self.postMessage({type: 'end-load-frames', goalStates: goalStates});
var t1 = new Date(); var t1 = new Date();
var diff = t1 - self.t0; var diff = t1 - self.t0;
@ -381,10 +392,12 @@ self.onWorldLoaded = function onWorldLoaded() {
catch(error) { catch(error) {
console.log("World serialization error:", error.toString() + "\n" + error.stack || error.stackTrace); console.log("World serialization error:", error.toString() + "\n" + error.stack || error.stackTrace);
} }
var t2 = new Date(); var t2 = new Date();
//console.log("About to transfer", serialized.serializedWorld.trackedPropertiesPerThangValues, serialized.transferableObjects); //console.log("About to transfer", serialized.serializedWorld.trackedPropertiesPerThangValues, serialized.transferableObjects);
var messageType = self.world.ended ? 'new-world' : 'some-frames-serialized';
try { 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) if(transferableSupported)
self.postMessage(message, serialized.transferableObjects); self.postMessage(message, serialized.transferableObjects);
else else
@ -393,11 +406,14 @@ self.onWorldLoaded = function onWorldLoaded() {
catch(error) { catch(error) {
console.log("World delivery error:", error.toString() + "\n" + error.stack || error.stackTrace); console.log("World delivery error:", error.toString() + "\n" + error.stack || error.stackTrace);
} }
if(self.world.ended) {
var t3 = new Date(); 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"); 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.goalManager.destroy();
self.world.destroy(); self.world.destroy();
self.world = null; self.world = null;
}
}; };
self.onWorldError = function onWorldError(error) { self.onWorldError = function onWorldError(error) {
@ -443,6 +459,11 @@ self.finalizePreload = function finalizePreload() {
self.world.finalizePreload(self.onWorldLoaded); self.world.finalizePreload(self.onWorldLoaded);
}; };
self.addFlagEvent = function addFlagEvent(flagEvent) {
if(!self.world) return;
self.world.addFlagEvent(flagEvent);
};
self.addEventListener('message', function(event) { self.addEventListener('message', function(event) {
self[event.data.func](event.data.args); self[event.data.func](event.data.args);
}); });

View file

@ -12,6 +12,9 @@ module.exports = class Angel extends CocoClass
infiniteLoopTimeoutDuration: 7500 # wait this long for a response when checking infiniteLoopTimeoutDuration: 7500 # wait this long for a response when checking
abortTimeoutDuration: 500 # give in-process or dying workers this long to give up abortTimeoutDuration: 500 # give in-process or dying workers this long to give up
subscriptions:
'level:flag-updated': 'onFlagEvent'
constructor: (@shared) -> constructor: (@shared) ->
super() super()
@say 'Got my wings.' @say 'Got my wings.'
@ -64,14 +67,6 @@ module.exports = class Angel extends CocoClass
clearTimeout @condemnTimeout clearTimeout @condemnTimeout
@beholdGoalStates event.data.goalStates # Work ends here if we're headless. @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 # We have to abort like an infinite loop if we see one of these; they're not really recoverable
when 'non-user-code-problem' when 'non-user-code-problem'
Backbone.Mediator.publish 'god:non-user-code-problem', problem: event.data.problem Backbone.Mediator.publish 'god:non-user-code-problem', problem: event.data.problem
@ -80,9 +75,7 @@ module.exports = class Angel extends CocoClass
else else
@fireWorker() @fireWorker()
# Either the world finished simulating successfully, or we abort the worker. # If it didn't finish simulating successfully, or we abort the worker.
when 'new-world'
@beholdWorld event.data.serialized, event.data.goalStates
when 'abort' when 'abort'
@say 'Aborted.', event.data @say 'Aborted.', event.data
clearTimeout @abortTimeout clearTimeout @abortTimeout
@ -91,6 +84,23 @@ module.exports = class Angel extends CocoClass
_.remove @shared.busyAngels, @ _.remove @shared.busyAngels, @
@doWork() @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 else
@log 'Received unsupported message:', event.data @log 'Received unsupported message:', event.data
@ -99,27 +109,37 @@ module.exports = class Angel extends CocoClass
Backbone.Mediator.publish 'god:goals-calculated', goalStates: goalStates Backbone.Mediator.publish 'god:goals-calculated', goalStates: goalStates
@finishWork() if @shared.headless @finishWork() if @shared.headless
beholdWorld: (serialized, goalStates) -> beholdWorld: (serialized, goalStates, startFrame, endFrame, streamingWorld) ->
return if @aborting 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. # 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 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 window.BOX2D_ENABLED = true
@shared.lastSerializedWorldFrames = serialized.frames @shared.lastSerializedWorldFrames = serialized.frames
finishBeholdingWorld: (goalStates) -> (world) => finishBeholdingWorld: (goalStates) -> (world) =>
return if @aborting 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 @shared.world = world
errorCount = (t for t in @shared.world.thangs when t.errorsOut).length Backbone.Mediator.publish eventType, world: world, firstWorld: @shared.firstWorld, goalStates: goalStates, team: me.team, firstChangedFrame: firstChangedFrame
Backbone.Mediator.publish 'god:new-world-created', world: world, firstWorld: @shared.firstWorld, errorCount: errorCount, goalStates: goalStates, team: me.team if finished
for scriptNote in @shared.world.scriptNotes for scriptNote in @shared.world.scriptNotes
Backbone.Mediator.publish scriptNote.channel, scriptNote.event Backbone.Mediator.publish scriptNote.channel, scriptNote.event
@shared.goalManager?.world = world @shared.goalManager?.world = world
@finishWork() @finishWork()
else
@deserializationQueue.shift() # Finished with this deserialization.
if deserializationArgs = @deserializationQueue[0] # Start another?
@beholdWorld deserializationArgs...
finishWork: -> finishWork: ->
@streamingWorld = null
@shared.firstWorld = false @shared.firstWorld = false
@deserializationQueue = []
@running = false @running = false
_.remove @shared.busyAngels, @ _.remove @shared.busyAngels, @
@doWork() @doWork()
@ -127,6 +147,7 @@ module.exports = class Angel extends CocoClass
finalizePreload: -> finalizePreload: ->
@say 'Finalize preload.' @say 'Finalize preload.'
@worker.postMessage func: 'finalizePreload' @worker.postMessage func: 'finalizePreload'
@work.preload = false
infinitelyLooped: => infinitelyLooped: =>
@say 'On infinitely looped! Aborting?', @aborting @say 'On infinitely looped! Aborting?', @aborting
@ -145,6 +166,7 @@ module.exports = class Angel extends CocoClass
@say 'Running world...' @say 'Running world...'
@running = true @running = true
@shared.busyAngels.push @ @shared.busyAngels.push @
@deserializationQueue = []
@worker.postMessage func: 'runWorld', args: @work @worker.postMessage func: 'runWorld', args: @work
clearTimeout @purgatoryTimer clearTimeout @purgatoryTimer
@say 'Infinite loop timer started at interval of', @infiniteLoopIntervalDuration @say 'Infinite loop timer started at interval of', @infiniteLoopIntervalDuration
@ -158,6 +180,8 @@ module.exports = class Angel extends CocoClass
@say 'Aborting...' @say 'Aborting...'
@running = false @running = false
@work = null @work = null
@streamingWorld = null
@deserializationQueue = null
_.remove @shared.busyAngels, @ _.remove @shared.busyAngels, @
@abortTimeout = _.delay @fireWorker, @abortTimeoutDuration @abortTimeout = _.delay @fireWorker, @abortTimeoutDuration
@aborting = true @aborting = true
@ -184,6 +208,10 @@ module.exports = class Angel extends CocoClass
@worker.addEventListener 'message', @onWorkerMessage @worker.addEventListener 'message', @onWorkerMessage
@worker.creationTime = new Date() @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) #### #### Synchronous code for running worlds on main thread (profiling / IE9) ####
simulateSync: (work) => simulateSync: (work) =>

View file

@ -53,9 +53,9 @@ module.exports = class God extends CocoClass
setWorldClassMap: (worldClassMap) -> @angelsShare.worldClassMap = worldClassMap setWorldClassMap: (worldClassMap) -> @angelsShare.worldClassMap = worldClassMap
onTomeCast: (e) -> 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})" console.log "#{@nick}: Let there be light upon #{@level.name}! (preload: #{preload})"
userCodeMap = @getUserCodeMap spells userCodeMap = @getUserCodeMap spells
@ -84,12 +84,14 @@ module.exports = class God extends CocoClass
headless: @angelsShare.headless headless: @angelsShare.headless
preload: preload preload: preload
synchronous: not Worker? # Profiling world simulation is easier on main thread, or we are IE9. synchronous: not Worker? # Profiling world simulation is easier on main thread, or we are IE9.
realTime: realTime
angel.workIfIdle() for angel in @angelsShare.angels angel.workIfIdle() for angel in @angelsShare.angels
getUserCodeMap: (spells) -> getUserCodeMap: (spells) ->
userCodeMap = {} userCodeMap = {}
for spellKey, spell of spells for spellKey, spell of spells
for thangID, spellThang of spell.thangs for thangID, spellThang of spell.thangs
continue if spellThang.thang?.programmableMethods[spell.name].cloneOf
(userCodeMap[thangID] ?= {})[spell.name] = spellThang.aether.serialize() (userCodeMap[thangID] ?= {})[spell.name] = spellThang.aether.serialize()
userCodeMap userCodeMap

View file

@ -197,6 +197,8 @@ module.exports = class LevelBus extends Bus
onNewGoalStates: ({goalStates})-> onNewGoalStates: ({goalStates})->
state = @session.get 'state' state = @session.get 'state'
unless utils.kindaEqual state.goalStates, goalStates # Only save when goals really change 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 state.goalStates = goalStates
@session.set 'state', state @session.set 'state', state
@changedSessionProperties.state = true @changedSessionProperties.state = true

View file

@ -78,6 +78,10 @@ module.exports = class LevelLoader extends CocoClass
@listenToOnce @opponentSession, 'sync', @loadDependenciesForSession @listenToOnce @opponentSession, 'sync', @loadDependenciesForSession
loadDependenciesForSession: (session) -> 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') if heroConfig = session.get('heroConfig')
url = "/db/thang.type/#{heroConfig.thangType}/version?project=name,components,original" url = "/db/thang.type/#{heroConfig.thangType}/version?project=name,components,original"
@worldNecessities.push @maybeLoadURL(url, ThangType, 'thang') @worldNecessities.push @maybeLoadURL(url, ThangType, 'thang')
@ -107,7 +111,8 @@ module.exports = class LevelLoader extends CocoClass
systemVersions = [] systemVersions = []
articleVersions = [] 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 thangIDs.push thang.thangType
@loadItemThangsEquippedByLevelThang(thang) @loadItemThangsEquippedByLevelThang(thang)
for comp in thang.components or [] for comp in thang.components or []

View file

@ -14,7 +14,7 @@ module.exports = LinkedInHandler = class LinkedInHandler extends CocoClass
onLinkedInLoaded: (e) -> onLinkedInLoaded: (e) ->
IN.Event.on IN, 'auth', @onLinkedInAuth IN.Event.on IN, 'auth', @onLinkedInAuth
onLinkedInAuth: (e) => console.log 'Authorized with LinkedIn' onLinkedInAuth: (e) => #console.log 'Authorized with LinkedIn'
constructEmployerAgreementObject: (cb) => constructEmployerAgreementObject: (cb) =>
IN.API.Profile('me') IN.API.Profile('me')

View file

@ -269,6 +269,7 @@ module.exports = ScriptManager = class ScriptManager extends CocoClass
@publishNote(note) @publishNote(note)
publishNote: (note) -> publishNote: (note) ->
Backbone.Mediator.publish 'playback:real-time-playback-ended', {}
Backbone.Mediator.publish(note.channel, note.event) Backbone.Mediator.publish(note.channel, note.event)
# ENDING NOTES # ENDING NOTES

View file

@ -1,6 +1,6 @@
module.exports = initializeLinkedIn = -> module.exports = initializeLinkedIn = ->
window.linkedInAsyncInit = -> window.linkedInAsyncInit = ->
console.log 'Linkedin async init success!' #console.log 'Linkedin async init success!'
Backbone.Mediator.publish 'linkedin-loaded' Backbone.Mediator.publish 'linkedin-loaded'
linkedInSnippet = linkedInSnippet =

View file

@ -184,7 +184,7 @@ module.exports = class Simulator extends CocoClass
try try
@commenceSimulationAndSetupCallback() @commenceSimulationAndSetupCallback()
catch err 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() @simulateAnotherTaskAfterDelay()
assignWorldAndLevelFromLevelLoaderAndDestroyIt: -> assignWorldAndLevelFromLevelLoaderAndDestroyIt: ->

View file

@ -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)

View file

@ -320,6 +320,9 @@ module.exports = CocoSprite = class CocoSprite extends CocoClass
[@imageObject.x, @imageObject.y] = [sup.x, sup.y] [@imageObject.x, @imageObject.y] = [sup.x, sup.y]
@lastPos = p1.copy?() or _.clone(p1) @lastPos = p1.copy?() or _.clone(p1)
@hasMoved = true @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: -> updateBaseScale: ->
scale = 1 scale = 1
@ -519,7 +522,7 @@ module.exports = CocoSprite = class CocoSprite extends CocoClass
@letterboxOn = e.on @letterboxOn = e.on
onMouseEvent: (e, ourEventName) -> onMouseEvent: (e, ourEventName) ->
return if @letterboxOn return if @letterboxOn or not @imageObject
p = @imageObject p = @imageObject
p = p.parent while p.parent p = p.parent while p.parent
newEvent = sprite: @, thang: @thang, originalEvent: e, canvas:p.canvas newEvent = sprite: @, thang: @thang, originalEvent: e, canvas:p.canvas

View file

@ -35,10 +35,6 @@ module.exports = class CoordinateDisplay extends createjs.Container
onMouseOut: (e) -> @mouseInBounds = false onMouseOut: (e) -> @mouseInBounds = false
onMouseMove: (e) -> 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 = @camera.screenToWorld x: e.x, y: e.y
wop.x = Math.round(wop.x) wop.x = Math.round(wop.x)
wop.y = Math.round(wop.y) wop.y = Math.round(wop.y)

View 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()

View file

@ -1,8 +1,5 @@
{me} = require('lib/auth')
Thang = require 'lib/world/thang' Thang = require 'lib/world/thang'
Vector = require 'lib/world/vector'
CocoSprite = require 'lib/surface/CocoSprite' CocoSprite = require 'lib/surface/CocoSprite'
Camera = require './Camera'
module.exports = IndieSprite = class IndieSprite extends CocoSprite module.exports = IndieSprite = class IndieSprite extends CocoSprite
notOfThisWorld: true notOfThisWorld: true
@ -11,16 +8,16 @@ module.exports = IndieSprite = class IndieSprite extends CocoSprite
'note-group-ended': 'onNoteGroupEnded' 'note-group-ended': 'onNoteGroupEnded'
constructor: (thangType, options) -> constructor: (thangType, options) ->
options.thang = @makeIndieThang thangType, options.thangID, options.pos options.thang = @makeIndieThang thangType, options
super thangType, options super thangType, options
@shadow = @thang @shadow = @thang
makeIndieThang: (thangType, thangID, pos) -> makeIndieThang: (thangType, options) ->
@thang = thang = new Thang null, thangType.get('name'), thangID @thang = thang = new Thang null, thangType.get('name'), options.thangID
# Build needed results of what used to be Exists, Physical, Acts, and Selectable Components # Build needed results of what used to be Exists, Physical, Acts, and Selectable Components
thang.exists = true thang.exists = true
thang.width = thang.height = thang.depth = 4 thang.width = thang.height = thang.depth = 4
thang.pos = pos ? @defaultPos() thang.pos = options.pos ? @defaultPos()
thang.pos.z = thang.depth / 2 thang.pos.z = thang.depth / 2
thang.shape = 'ellipsoid' thang.shape = 'ellipsoid'
thang.rotation = 0 thang.rotation = 0

View file

@ -37,6 +37,8 @@ module.exports = class Label extends CocoClass
build: -> build: ->
@layer.removeChild @background if @background @layer.removeChild @background if @background
@layer.removeChild @label if @label @layer.removeChild @label if @label
@label = null
@background = null
return unless @text # null or '' should both be skipped return unless @text # null or '' should both be skipped
o = @buildLabelOptions() o = @buildLabelOptions()
@layer.addChild @label = @buildLabel o @layer.addChild @label = @buildLabel o
@ -53,6 +55,17 @@ module.exports = class Label extends CocoClass
@label.y = @background.y = @sprite.imageObject.y + offset.y @label.y = @background.y = @sprite.imageObject.y + offset.y
null null
show: ->
return unless @label
@layer.addChild @label
@layer.addChild @background
@layer.updateLayerOrder()
hide: ->
return unless @label
@layer.removeChild @background
@layer.removeChild @label
buildLabelOptions: -> buildLabelOptions: ->
o = {} o = {}
st = {dialogue: 'D', say: 'S', name: 'N'}[@style] st = {dialogue: 'D', say: 'S', name: 'N'}[@style]

View file

@ -3,6 +3,7 @@ CocoClass = require 'lib/CocoClass'
Layer = require './Layer' Layer = require './Layer'
IndieSprite = require 'lib/surface/IndieSprite' IndieSprite = require 'lib/surface/IndieSprite'
WizardSprite = require 'lib/surface/WizardSprite' WizardSprite = require 'lib/surface/WizardSprite'
FlagSprite = require 'lib/surface/FlagSprite'
CocoSprite = require 'lib/surface/CocoSprite' CocoSprite = require 'lib/surface/CocoSprite'
Mark = require './Mark' Mark = require './Mark'
Grid = require 'lib/world/Grid' Grid = require 'lib/world/Grid'
@ -19,8 +20,13 @@ module.exports = class SpriteBoss extends CocoClass
'level-lock-select': 'onSetLockSelect' 'level-lock-select': 'onSetLockSelect'
'level:restarted': 'onLevelRestarted' 'level:restarted': 'onLevelRestarted'
'god:new-world-created': 'onNewWorld' 'god:new-world-created': 'onNewWorld'
'god:streaming-world-updated': 'onNewWorld'
'camera:dragged': 'onCameraDragged' 'camera:dragged': 'onCameraDragged'
'sprite:loaded': -> @update(true) 'sprite:loaded': -> @update(true)
'level:flag-color-selected': 'onFlagColorSelected'
'level:flag-updated': 'onFlagUpdated'
'surface:flag-appeared': 'onFlagAppeared'
'surface:remove-selected-flag': 'onRemoveSelectedFlag'
constructor: (@options) -> constructor: (@options) ->
super() super()
@ -36,6 +42,7 @@ module.exports = class SpriteBoss extends CocoClass
@selfWizardSprite = null @selfWizardSprite = null
@createLayers() @createLayers()
@spriteSheetCache = {} @spriteSheetCache = {}
@pendingFlags = []
destroy: -> destroy: ->
@removeSprite sprite for thangID, sprite of @sprites @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 sprite = @createWizardSprite thangID: opponent.id, name: opponent.name, codeLanguage: opponent.codeLanguage
if not opponent.levelSlug or opponent.levelSlug is 'brawlwood' 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} 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} sprite.targetPos = if opponent.team is 'ogres' then {x: 72, y: 39} else {x: 9, y: 39}
else if opponent.levelSlug is 'criss-cross' else if opponent.levelSlug is 'criss-cross'
sprite.targetPos = if opponent.team is 'ogres' then {x: 50, y: 12} else {x: 0, y: 40} 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 return @dragged = 0 if @dragged > 3
@dragged = 0 @dragged = 0
sprite = if e.sprite?.thang?.isSelectable then e.sprite else null sprite = if e.sprite?.thang?.isSelectable then e.sprite else null
return if @flagCursorSprite and sprite?.thangType.get('name') is 'Flag'
@selectSprite e, sprite @selectSprite e, sprite
onStageMouseDown: (e) -> onStageMouseDown: (e) ->
@ -315,6 +323,45 @@ module.exports = class SpriteBoss extends CocoClass
instance.addEventListener 'complete', -> instance.addEventListener 'complete', ->
Backbone.Mediator.publish 'thang-finished-talking', thang: sprite?.thang 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 # Marks
updateSelection: -> updateSelection: ->

View file

@ -8,7 +8,6 @@ CameraBorder = require './CameraBorder'
Layer = require './Layer' Layer = require './Layer'
Letterbox = require './Letterbox' Letterbox = require './Letterbox'
Dimmer = require './Dimmer' Dimmer = require './Dimmer'
CastingScreen = require './CastingScreen'
PlaybackOverScreen = require './PlaybackOverScreen' PlaybackOverScreen = require './PlaybackOverScreen'
DebugDisplay = require './DebugDisplay' DebugDisplay = require './DebugDisplay'
CoordinateDisplay = require './CoordinateDisplay' CoordinateDisplay = require './CoordinateDisplay'
@ -63,10 +62,15 @@ module.exports = Surface = class Surface extends CocoClass
'level-set-surface-camera': 'onSetCamera' 'level-set-surface-camera': 'onSetCamera'
'level:restarted': 'onLevelRestarted' 'level:restarted': 'onLevelRestarted'
'god:new-world-created': 'onNewWorld' 'god:new-world-created': 'onNewWorld'
'god:streaming-world-updated': 'onNewWorld'
'tome:cast-spells': 'onCastSpells' 'tome:cast-spells': 'onCastSpells'
'level-set-letterbox': 'onSetLetterbox' 'level-set-letterbox': 'onSetLetterbox'
'application:idle-changed': 'onIdleChanged' 'application:idle-changed': 'onIdleChanged'
'camera:zoom-updated': 'onZoomUpdated' '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: shortcuts:
'ctrl+\\, ⌘+\\': 'onToggleDebug' 'ctrl+\\, ⌘+\\': 'onToggleDebug'
@ -82,7 +86,7 @@ module.exports = Surface = class Surface extends CocoClass
@options = _.extend(@options, givenOptions) if givenOptions @options = _.extend(@options, givenOptions) if givenOptions
@initEasel() @initEasel()
@initAudio() @initAudio()
@onResize = _.debounce @onResize, 500 @onResize = _.debounce @onResize, 250
$(window).on 'resize', @onResize $(window).on 'resize', @onResize
if @world.ended if @world.ended
_.defer => @setWorld @world _.defer => @setWorld @world
@ -96,7 +100,6 @@ module.exports = Surface = class Surface extends CocoClass
@spriteBoss.destroy() @spriteBoss.destroy()
@chooser?.destroy() @chooser?.destroy()
@dimmer?.destroy() @dimmer?.destroy()
@castingScreen?.destroy()
@playbackOverScreen?.destroy() @playbackOverScreen?.destroy()
@stage.clear() @stage.clear()
@musicPlayer?.destroy() @musicPlayer?.destroy()
@ -115,14 +118,14 @@ module.exports = Surface = class Surface extends CocoClass
setWorld: (@world) -> setWorld: (@world) ->
@worldLoaded = true @worldLoaded = true
lastFrame = Math.min(@getCurrentFrame(), @world.totalFrames - 1) lastFrame = Math.min(@getCurrentFrame(), @world.frames.length - 1)
@world.getFrame(lastFrame).restoreState() unless @options.choosing @world.getFrame(lastFrame).restoreState() unless @options.choosing
@spriteBoss.world = @world @spriteBoss.world = @world
@showLevel() @showLevel()
@updateState true if @loaded @updateState true if @loaded
# TODO: synchronize both ways of choosing whether to show coords (@world via UI System or @options via World Select modal) # 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 @coordinateDisplay = new CoordinateDisplay camera: @camera
@surfaceTextLayer.addChild @coordinateDisplay @surfaceTextLayer.addChild @coordinateDisplay
@onFrameChanged() @onFrameChanged()
@ -190,19 +193,19 @@ module.exports = Surface = class Surface extends CocoClass
setProgress: (progress, scrubDuration=500) -> setProgress: (progress, scrubDuration=500) ->
progress = Math.max(Math.min(progress, 1), 0.0) progress = Math.max(Math.min(progress, 1), 0.0)
@fastForwardingToFrame = null
@scrubbing = true @scrubbing = true
onTweenEnd = => onTweenEnd = =>
@scrubbingTo = null @scrubbingTo = null
@scrubbing = false @scrubbing = false
@scrubbingPlaybackSpeed = null @scrubbingPlaybackSpeed = null
@fastForwarding = false
if @scrubbingTo? if @scrubbingTo?
# cut to the chase for existing tween # cut to the chase for existing tween
createjs.Tween.removeTweens(@) createjs.Tween.removeTweens(@)
@currentFrame = @scrubbingTo @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)) @scrubbingPlaybackSpeed = Math.sqrt(Math.abs(@scrubbingTo - @currentFrame) * @world.dt / (scrubDuration or 0.5))
if scrubDuration if scrubDuration
t = createjs.Tween t = createjs.Tween
@ -244,7 +247,7 @@ module.exports = Surface = class Surface extends CocoClass
getCurrentFrame: -> getCurrentFrame: ->
return Math.max(0, Math.min(Math.floor(@currentFrame), @world.frames.length - 1)) return Math.max(0, Math.min(Math.floor(@currentFrame), @world.frames.length - 1))
getProgress: -> @currentFrame / @world.totalFrames getProgress: -> @currentFrame / @world.frames.length
onLevelRestarted: (e) -> onLevelRestarted: (e) ->
@setProgress 0, 0 @setProgress 0, 0
@ -286,26 +289,26 @@ module.exports = Surface = class Surface extends CocoClass
@setPlayingCalled = true @setPlayingCalled = true
if @playing and @currentFrame >= (@world.totalFrames - 5) if @playing and @currentFrame >= (@world.totalFrames - 5)
@currentFrame = 0 @currentFrame = 0
if @fastForwarding and not @playing if @fastForwardingToFrame and not @playing
@setProgress @currentFrame / @world.totalFrames @fastForwardingToFrame = null
onSetTime: (e) -> onSetTime: (e) ->
toFrame = @currentFrame toFrame = @currentFrame
if e.time? if e.time?
@worldLifespan = @world.totalFrames / @world.frameRate @worldLifespan = @world.frames.length / @world.frameRate
e.ratio = e.time / @worldLifespan e.ratio = e.time / @worldLifespan
if e.ratio? if e.ratio?
toFrame = @world.totalFrames * e.ratio toFrame = @world.frames.length * e.ratio
if e.frameOffset if e.frameOffset
toFrame += e.frameOffset toFrame += e.frameOffset
if e.ratioOffset if e.ratioOffset
toFrame += @world.totalFrames * e.ratioOffset toFrame += @world.frames.length * e.ratioOffset
unless _.isNumber(toFrame) and not _.isNaN(toFrame) unless _.isNumber(toFrame) and not _.isNaN(toFrame)
return console.error('set-time event', e, 'produced invalid target frame', 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) -> onFrameChanged: (force) ->
@currentFrame = Math.min(@currentFrame, @world.totalFrames) @currentFrame = Math.min(@currentFrame, @world.frames.length)
@debugDisplay?.updateFrame @currentFrame @debugDisplay?.updateFrame @currentFrame
return if @currentFrame is @lastFrame and not force return if @currentFrame is @lastFrame and not force
progress = @getProgress() progress = @getProgress()
@ -317,7 +320,7 @@ module.exports = Surface = class Surface extends CocoClass
world: @world world: @world
) )
if @lastFrame < @world.totalFrames and @currentFrame >= @world.totalFrames - 1 if @lastFrame < @world.frames.length and @currentFrame >= @world.totalFrames - 1
@ended = true @ended = true
@setPaused true @setPaused true
Backbone.Mediator.publish 'surface:playback-ended' Backbone.Mediator.publish 'surface:playback-ended'
@ -353,48 +356,34 @@ module.exports = Surface = class Surface extends CocoClass
return if e.preload return if e.preload
@setPaused false if @ended @setPaused false if @ended
@casting = true @casting = true
@wasPlayingWhenCastingBegan = @playing @setPlayingCalled = false # Don't overwrite playing settings if they changed by, say, scripts.
Backbone.Mediator.publish 'level-set-playing', {playing: false} @frameBeforeCast = @currentFrame
@setPlayingCalled = false # don't overwrite playing settings if they changed by, say, scripts @setProgress 0
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))
onNewWorld: (event) -> onNewWorld: (event) ->
return unless event.world.name is @world.name return unless event.world.name is @world.name
@casting = false @casting = false
if @ended and not @wasPlayingWhenCastingBegan
@setPaused true
else
@spriteBoss.play() @spriteBoss.play()
# This has a tendency to break scripts that are waiting for playback to change when the level is loaded # 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. # 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 @setWorld event.world
@onFrameChanged(true) @onFrameChanged(true)
if fastForwardTo and @playing fastForwardBuffer = 2
fastForwardToRatio = fastForwardTo / @world.totalFrames if @playing and not @realTime and (ffToFrame = Math.min(event.firstChangedFrame, @frameBeforeCast, @world.frames.length)) and ffToFrame > @currentFrame + fastForwardBuffer * @world.frameRate
fastForwardToTime = fastForwardTo * @world.dt @fastForwardingToFrame = ffToFrame
fastForwardSpeed = Math.max 4, fastForwardToTime / 3 @fastForwardingSpeed = Math.max 4, 4 * 90 / (@world.maxTotalFrames * @world.dt)
@setProgress fastForwardToRatio, 1000 * fastForwardToTime / fastForwardSpeed else if @realTime
@fastForwarding = true lag = (@world.frames.length - 1) * @world.dt - @world.age
createjs.Tween.get(@surfaceLayer) intendedLag = @world.realTimeBufferMax + @world.dt
.to({alpha: 0.0}, 50) if lag > intendedLag * 1.2
.call(f) @fastForwardingToFrame = @world.frames.length - @world.realTimeBufferMax * @world.frameRate
.to({alpha: 1.0}, 2000, createjs.Ease.getPowOut(2.0)) @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 # initialization
@ -413,7 +402,6 @@ module.exports = Surface = class Surface extends CocoClass
@surfaceLayer.addChild @cameraBorder = new CameraBorder bounds: @camera.bounds @surfaceLayer.addChild @cameraBorder = new CameraBorder bounds: @camera.bounds
@screenLayer.addChild new Letterbox canvasWidth: canvasWidth, canvasHeight: canvasHeight @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 @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 @playbackOverScreen ?= new PlaybackOverScreen camera: @camera, layer: @screenLayer
@stage.enableMouseOver(10) @stage.enableMouseOver(10)
@stage.addEventListener 'stagemousemove', @onMouseMove @stage.addEventListener 'stagemousemove', @onMouseMove
@ -428,8 +416,17 @@ module.exports = Surface = class Surface extends CocoClass
onResize: (e) => onResize: (e) =>
oldWidth = parseInt @canvas.attr('width'), 10 oldWidth = parseInt @canvas.attr('width'), 10
oldHeight = parseInt @canvas.attr('height'), 10 oldHeight = parseInt @canvas.attr('height'), 10
newWidth = @canvas.width() aspectRatio = oldWidth / oldHeight
newHeight = @canvas.height() 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 return unless newWidth > 0 and newHeight > 0
#if InstallTrigger? # Firefox rendering performance goes down as canvas size goes up #if InstallTrigger? # Firefox rendering performance goes down as canvas size goes up
# newWidth = Math.min 924, newWidth # newWidth = Math.min 924, newWidth
@ -540,7 +537,9 @@ module.exports = Surface = class Surface extends CocoClass
onMouseDown: (e) => onMouseDown: (e) =>
return if @disabled return if @disabled
onBackground = not @stage.hitTest e.stageX, e.stageY 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) => onMouseUp: (e) =>
return if @disabled 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... # seems to be a bug where only one object can register with the Ticker...
oldFrame = @currentFrame oldFrame = @currentFrame
oldWorldFrame = Math.floor oldFrame oldWorldFrame = Math.floor oldFrame
lastFrame = @world.totalFrames - 1 lastFrame = @world.frames.length - 1
while true while true
Dropper.tick() Dropper.tick()
@trailmaster.tick() if @trailmaster @trailmaster.tick() if @trailmaster
# Skip some frame updates unless we're playing and not at end (or we haven't drawn much yet) # 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 frameAdvanced = (@playing and @currentFrame < lastFrame) or @totalFramesDrawn < 2
if frameAdvanced and @playing 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 @currentFrame = Math.min @currentFrame, lastFrame
newWorldFrame = Math.floor @currentFrame newWorldFrame = Math.floor @currentFrame
worldFrameAdvanced = newWorldFrame isnt oldWorldFrame worldFrameAdvanced = newWorldFrame isnt oldWorldFrame
@ -603,8 +607,8 @@ module.exports = Surface = class Surface extends CocoClass
restoreWorldState: -> restoreWorldState: ->
frame = @world.getFrame(@getCurrentFrame()) frame = @world.getFrame(@getCurrentFrame())
frame.restoreState() frame.restoreState()
current = Math.max(0, Math.min(@currentFrame, @world.totalFrames - 1)) current = Math.max(0, Math.min(@currentFrame, @world.frames.length - 1))
if current - Math.floor(current) > 0.01 if current - Math.floor(current) > 0.01 and Math.ceil(current) < @world.frames.length - 1
next = Math.ceil current next = Math.ceil current
ratio = current % 1 ratio = current % 1
@world.frames[next].restorePartialState ratio if next > 1 @world.frames[next].restorePartialState ratio if next > 1
@ -614,13 +618,29 @@ module.exports = Surface = class Surface extends CocoClass
updateState: (frameChanged) -> updateState: (frameChanged) ->
# world state must have been restored in @restoreWorldState # world state must have been restored in @restoreWorldState
@camera.updateZoom() @camera.updateZoom()
@spriteBoss.update frameChanged unless @casting @spriteBoss.update frameChanged
@dimmer?.setSprites @spriteBoss.sprites @dimmer?.setSprites @spriteBoss.sprites
drawCurrentFrame: (e) -> drawCurrentFrame: (e) ->
++@totalFramesDrawn ++@totalFramesDrawn
@stage.update e @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? # paths - TODO: move to SpriteBoss? but only update on frame drawing instead of on every frame update?
updatePaths: -> updatePaths: ->

View file

@ -1,5 +1,4 @@
IndieSprite = require 'lib/surface/IndieSprite' IndieSprite = require 'lib/surface/IndieSprite'
Camera = require './Camera'
{me} = require 'lib/auth' {me} = require 'lib/auth'
module.exports = class WizardSprite extends IndieSprite module.exports = class WizardSprite extends IndieSprite
@ -35,12 +34,11 @@ module.exports = class WizardSprite extends IndieSprite
@targetPos = @thang.pos @targetPos = @thang.pos
if @isSelf if @isSelf
@setNameLabel me.displayName() @setNameLabel me.displayName()
@setColorHue me.get('wizardColor1')
else if options.name else if options.name
@setNameLabel options.name @setNameLabel options.name
makeIndieThang: (thangType, thangID, pos) -> makeIndieThang: (thangType, options) ->
thang = super thangType, thangID, pos thang = super thangType, options
thang.isSelectable = false thang.isSelectable = false
thang.bobHeight = 0.75 thang.bobHeight = 0.75
thang.bobTime = 2 thang.bobTime = 2
@ -59,6 +57,11 @@ module.exports = class WizardSprite extends IndieSprite
name += " (#{@options.codeLanguage})" # TODO: move on second line, capitalize properly name += " (#{@options.codeLanguage})" # TODO: move on second line, capitalize properly
super name 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) -> onPlayerStatesChanged: (e) ->
for playerID, state of e.states for playerID, state of e.states
continue unless playerID is @thang.id 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 continue if playerID is me.id # ignore changes for self wizard sprite
@setNameLabel state.name @setNameLabel state.name
continue unless state.wizard? continue unless state.wizard?
@setColorHue state.wizard.wizardColor1
if targetID = state.wizard.targetSprite if targetID = state.wizard.targetSprite
return console.warn 'Wizard Sprite couldn\'t find target sprite', targetID unless targetID of @options.sprites return console.warn 'Wizard Sprite couldn\'t find target sprite', targetID unless targetID of @options.sprites
@setTarget @options.sprites[targetID] @setTarget @options.sprites[targetID]
@ -91,17 +93,13 @@ module.exports = class WizardSprite extends IndieSprite
@imageObject.scaleX = @imageObject.scaleY = @imageObject.alpha = 0 @imageObject.scaleX = @imageObject.scaleY = @imageObject.alpha = 0
createjs.Tween.get(@imageObject) createjs.Tween.get(@imageObject)
.to({scaleX: 1, scaleY: 1, alpha: 1}, 1000, createjs.Ease.getPowInOut(2.2)) .to({scaleX: 1, scaleY: 1, alpha: 1}, 1000, createjs.Ease.getPowInOut(2.2))
@labels.name?.show()
animateOut: (callback) -> animateOut: (callback) ->
tween = createjs.Tween.get(@imageObject) tween = createjs.Tween.get(@imageObject)
.to({scaleX: 0, scaleY: 0, alpha: 0}, 1000, createjs.Ease.getPowInOut(2.2)) .to({scaleX: 0, scaleY: 0, alpha: 0}, 1000, createjs.Ease.getPowInOut(2.2))
tween.call(callback) if callback tween.call(callback) if callback
@labels.name?.hide()
setColorHue: (newColorHue) ->
# TODO: is this needed any more?
return if @colorHue is newColorHue
@colorHue = newColorHue
#@updateColorFilters()
setEditing: (@editing) -> setEditing: (@editing) ->
if @editing if @editing

View file

@ -85,7 +85,7 @@ module.exports = class Thang
throw new Error "Two types were specified for trackable property #{prop}: #{oldType} and #{type}." throw new Error "Two types were specified for trackable property #{prop}: #{oldType} and #{type}."
keepTrackedProperty: (prop) -> 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 propIndex = @trackedPropertiesKeys.indexOf prop
if propIndex isnt -1 if propIndex isnt -1
@trackedPropertiesUsed[propIndex] = true @trackedPropertiesUsed[propIndex] = true
@ -147,6 +147,8 @@ module.exports = class Thang
for trackedFinalProperty in @trackedFinalProperties ? [] for trackedFinalProperty in @trackedFinalProperties ? []
# TODO: take some (but not all) of serialize logic from ThangState to handle other types # TODO: take some (but not all) of serialize logic from ThangState to handle other types
o.finalState[trackedFinalProperty] = @[trackedFinalProperty] 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 o
@deserialize: (o, world, classMap) -> @deserialize: (o, world, classMap) ->
@ -154,6 +156,8 @@ module.exports = class Thang
for [componentClassName, componentConfig] in o.components for [componentClassName, componentConfig] in o.components
componentClass = classMap[componentClassName] componentClass = classMap[componentClassName]
t.addComponents [componentClass, componentConfig] t.addComponents [componentClass, componentConfig]
t.unusedTrackedPropertyKeys = o.unusedTrackedPropertyKeys
t.unusedTrackedPropertyValues = (t[prop] for prop in o.unusedTrackedPropertyKeys)
for prop, val of o.finalState for prop, val of o.finalState
# TODO: take some (but not all) of deserialize logic from ThangState to handle other types # TODO: take some (but not all) of deserialize logic from ThangState to handle other types
t[prop] = val t[prop] = val
@ -164,7 +168,16 @@ module.exports = class Thang
getSpriteOptions: -> getSpriteOptions: ->
colorConfigs = @world?.getTeamColors() or {} colorConfigs = @world?.getTeamColors() or {}
options = {} options = {colorConfig: {}}
if @team and colorConfigs[@team] if @team and teamColor = colorConfigs[@team]
options.colorConfig = {team: colorConfigs[@team]} options.colorConfig.team = teamColor
if @color and color = @grabColorConfig @color
options.colorConfig.color = color
options 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]

View file

@ -64,7 +64,10 @@ module.exports = class ThangState
# Get the property, whether we have it stored in @props or in @trackedPropertyValues. Optimize it. # 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 # 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 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] value = @props[propIndex]
return value if value isnt undefined or @hasRestored return value if value isnt undefined or @hasRestored
return @props[propIndex] = @getStoredProp propIndex 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. # Restore trackedProperties' values to @thang, retrieving them from @trackedPropertyValues if needed. Optimize it.
return @ if @thang._state is @ and not @thang.partialState return @ if @thang._state is @ and not @thang.partialState
unless @hasRestored # Restoring in a deserialized World for first time 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 = [] props = []
for prop, propIndex in @trackedPropertyKeys for prop, propIndex in @trackedPropertyKeys
type = @trackedPropertyTypes[propIndex] type = @trackedPropertyTypes[propIndex]
storage = @trackedPropertyValues[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] #console.log @frameIndex, @thang.id, prop, propIndex, type, storage, 'got', @thang[prop]
@props = props @props = props
@trackedPropertyTypes = @trackedPropertyValues = @specialKeysToValues = null # leave @trackedPropertyKeys for indexing @trackedPropertyTypes = @trackedPropertyValues = @specialKeysToValues = null # leave @trackedPropertyKeys for indexing
@hasRestored = true @hasRestored = true
else # Restoring later times 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 for prop, propIndex in @trackedPropertyKeys
@thang[prop] = @props[propIndex] @thang[prop] = @props[propIndex]
@thang.partialState = false @thang.partialState = false
@ @
restorePartial: (ratio) -> 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 inverse = 1 - ratio
for prop, propIndex in @trackedPropertyKeys when prop is 'pos' or prop is 'rotation' for prop, propIndex in @trackedPropertyKeys when prop is 'pos' or prop is 'rotation'
if @hasRestored if @hasRestored

View file

@ -10,8 +10,11 @@ WorldScriptNote = require './world_script_note'
{now, consolidateThangs, typedArraySupport} = require './world_utils' {now, consolidateThangs, typedArraySupport} = require './world_utils'
Component = require 'lib/world/component' Component = require 'lib/world/component'
System = require 'lib/world/system' System = require 'lib/world/system'
PROGRESS_UPDATE_INTERVAL = 200 PROGRESS_UPDATE_INTERVAL = 100
DESERIALIZATION_INTERVAL = 20 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' ITEM_ORIGINAL = '53e12043b82921000051cdf9'
module.exports = class World 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 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 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 headless: false # Whether we are just simulating for goal states instead of all serialized results
framesSerializedSoFar: 0
apiProperties: ['age', 'dt'] apiProperties: ['age', 'dt']
realTimeBufferMax: REAL_TIME_BUFFER_MAX / 1000
constructor: (@userCodeMap, classMap) -> constructor: (@userCodeMap, classMap) ->
# classMap is needed for deserializing Worlds, Thangs, and other classes # classMap is needed for deserializing Worlds, Thangs, and other classes
@classMap = classMap ? {Vector: Vector, Rectangle: Rectangle, Thang: Thang, Ellipse: Ellipse, LineSegment: LineSegment} @classMap = classMap ? {Vector: Vector, Rectangle: Rectangle, Thang: Thang, Ellipse: Ellipse, LineSegment: LineSegment}
@ -33,6 +38,7 @@ module.exports = class World
@systems = [] @systems = []
@systemMap = {} @systemMap = {}
@scriptNotes = [] @scriptNotes = []
@flagHistory = []
@rand = new Rand 0 # Existence System may change this seed @rand = new Rand 0 # Existence System may change this seed
@frames = [new WorldFrame(@, 0)] @frames = [new WorldFrame(@, 0)]
@ -65,10 +71,11 @@ module.exports = class World
@thangs[i] = thang @thangs[i] = thang
@thangMap[thang.id] = thang @thangMap[thang.id] = thang
thangDialogueSounds: -> thangDialogueSounds: (startFrame=0) ->
if @frames.length < @totalFrames then throw new Error('World should be over before grabbing dialogue') return [] unless startFrame < @frames.length
[sounds, seen] = [[], {}] [sounds, seen] = [[], {}]
for frame in @frames for frameIndex in [startFrame ... @frames.length]
frame = @frames[frameIndex]
for thangID, state of frame.thangStateMap for thangID, state of frame.thangStateMap
continue unless state.thang.say and sayMessage = state.getStateForProp 'sayMessage' continue unless state.thang.say and sayMessage = state.getStateForProp 'sayMessage'
soundKey = state.thang.spriteName + ':' + sayMessage soundKey = state.thang.spriteName + ':' + sayMessage
@ -85,50 +92,30 @@ module.exports = class World
loadFrames: (loadedCallback, errorCallback, loadProgressCallback, skipDeferredLoading, loadUntilFrame) -> loadFrames: (loadedCallback, errorCallback, loadProgressCallback, skipDeferredLoading, loadUntilFrame) ->
return if @aborted return if @aborted
unless @thangs.length console.log 'Warning: loadFrames called on empty World (no thangs).' unless @thangs.length
console.log 'Warning: loadFrames called on empty World (no thangs).'
t1 = now() t1 = now()
@t0 ?= t1 @t0 ?= t1
if loadUntilFrame @worldLoadStartTime ?= t1
frameToLoadUntil = loadUntilFrame + 1 @lastRealTimeUpdate ?= 0
else continueLaterFn = =>
frameToLoadUntil = @totalFrames @loadFrames(loadedCallback, errorCallback, loadProgressCallback, skipDeferredLoading, loadUntilFrame) unless @destroyed
frameToLoadUntil = if loadUntilFrame then loadUntilFrame + 1 else @totalFrames # Might stop early if debugging.
i = @frames.length i = @frames.length
while i < frameToLoadUntil while i < frameToLoadUntil and i < @totalFrames
if @debugging return unless @shouldContinueLoading t1, loadProgressCallback, skipDeferredLoading, continueLaterFn
for thang in @thangs when thang.isProgrammable @adjustFlowSettings loadUntilFrame if @debugging
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
try try
@getFrame(i) @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 catch error
# Not an Aether.errors.UserCodeError; maybe we can't recover @addError error # Not an Aether.errors.UserCodeError; maybe we can't recover
@addError error
unless @preloading or @debugging unless @preloading or @debugging
for error in (@unhandledRuntimeErrors ? []) for error in (@unhandledRuntimeErrors ? [])
return unless errorCallback error # errorCallback tells us whether the error is recoverable return unless errorCallback error # errorCallback tells us whether the error is recoverable
@unhandledRuntimeErrors = [] @unhandledRuntimeErrors = []
t2 = now() @finishLoadingFrames loadProgressCallback, loadedCallback
if t2 - t1 > PROGRESS_UPDATE_INTERVAL
loadProgressCallback? i / @totalFrames unless @preloading finishLoadingFrames: (loadProgressCallback, loadedCallback) ->
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
unless @debugging unless @debugging
@ended = true @ended = true
system.finish @thangs for system in @systems system.finish @thangs for system in @systems
@ -136,6 +123,53 @@ module.exports = class World
loadProgressCallback? 1 loadProgressCallback? 1
loadedCallback() 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) -> finalizePreload: (loadedCallback) ->
@preloading = false @preloading = false
loadedCallback() if @ended loadedCallback() if @ended
@ -143,6 +177,9 @@ module.exports = class World
abort: -> abort: ->
@aborted = true @aborted = true
addFlagEvent: (flagEvent) ->
@flagHistory.push flagEvent
loadFromLevel: (level, willSimulate=true) -> loadFromLevel: (level, willSimulate=true) ->
@levelComponents = level.levelComponents @levelComponents = level.levelComponents
@thangTypes = level.thangTypes @thangTypes = level.thangTypes
@ -289,7 +326,9 @@ module.exports = class World
serialize: -> serialize: ->
# Code hotspot; optimize it # 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] [transferableObjects, nontransferableObjects] = [0, 0]
o = {totalFrames: @totalFrames, maxTotalFrames: @maxTotalFrames, frameRate: @frameRate, dt: @dt, victory: @victory, userCodeMap: {}, trackedProperties: {}} o = {totalFrames: @totalFrames, maxTotalFrames: @maxTotalFrames, frameRate: @frameRate, dt: @dt, victory: @victory, userCodeMap: {}, trackedProperties: {}}
o.trackedProperties[prop] = @[prop] for prop in @trackedProperties or [] o.trackedProperties[prop] = @[prop] for prop in @trackedProperties or []
@ -305,12 +344,14 @@ module.exports = class World
o.trackedPropertiesPerThangKeys = [] o.trackedPropertiesPerThangKeys = []
o.trackedPropertiesPerThangTypes = [] o.trackedPropertiesPerThangTypes = []
trackedPropertiesPerThangValues = [] # We won't send these, just the offsets and the storage buffer 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 transferableStorageBytesNeeded = 0
nFrames = @frames.length nFrames = endFrame - startFrame
streaming = nFrames < @totalFrames
for thang in @thangs for thang in @thangs
# Don't serialize empty trackedProperties for stateless Thangs which haven't changed (like obstacles). # 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) continue if thang.stateless and not _.some(thang.trackedPropertiesUsed, Boolean)
o.trackedPropertiesThangIDs.push thang.id o.trackedPropertiesThangIDs.push thang.id
trackedPropertiesIndices = [] trackedPropertiesIndices = []
@ -357,8 +398,8 @@ module.exports = class World
t1 = now() t1 = now()
o.frameHashes = [] o.frameHashes = []
for frame, frameIndex in @frames for frameIndex in [startFrame ... endFrame]
o.frameHashes.push frame.serialize(frameIndex, o.trackedPropertiesThangIDs, o.trackedPropertiesPerThangIndices, o.trackedPropertiesPerThangTypes, trackedPropertiesPerThangValues, o.specialValuesToKeys, o.specialKeysToValues) o.frameHashes.push @frames[frameIndex].serialize(frameIndex - startFrame, o.trackedPropertiesThangIDs, o.trackedPropertiesPerThangIndices, o.trackedPropertiesPerThangTypes, trackedPropertiesPerThangValues, o.specialValuesToKeys, o.specialKeysToValues)
t2 = now() t2 = now()
unless typedArraySupport unless typedArraySupport
@ -368,27 +409,33 @@ module.exports = class World
flattened.push value flattened.push value
o.storageBuffer = flattened 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' #console.log 'Got', transferableObjects, 'transferable objects and', nontransferableObjects, 'nontransferable; stored', transferableStorageBytesNeeded, 'bytes transferably'
o.thangs = (t.serialize() for t in @thangs.concat(@extraneousThangs ? [])) o.thangs = (t.serialize() for t in @thangs.concat(@extraneousThangs ? []))
o.scriptNotes = (sn.serialize() for sn in @scriptNotes) o.scriptNotes = (sn.serialize() for sn in @scriptNotes)
if o.scriptNotes.length > 200 if o.scriptNotes.length > 200
console.log 'Whoa, serializing a lot of WorldScriptNotes here:', o.scriptNotes.length 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 # Code hotspot; optimize it
#console.log 'Deserializing', o, 'length', JSON.stringify(o).length #console.log 'Deserializing', o, 'length', JSON.stringify(o).length
#console.log JSON.stringify(o) #console.log JSON.stringify(o)
#console.log 'Got special keys and values:', o.specialValuesToKeys, o.specialKeysToValues #console.log 'Got special keys and values:', o.specialValuesToKeys, o.specialKeysToValues
perf = {} perf = {}
perf.t0 = now() 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.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 w[prop] = val for prop, val of o.trackedProperties
perf.t1 = now() 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.thangs = (Thang.deserialize(thang, w, classMap) for thang in o.thangs)
w.setThang thang for thang in w.thangs w.setThang thang for thang in w.thangs
w.scriptNotes = (WorldScriptNote.deserialize(sn, w, classMap) for sn in o.scriptNotes) w.scriptNotes = (WorldScriptNote.deserialize(sn, w, classMap) for sn in o.scriptNotes)
@ -400,7 +447,7 @@ module.exports = class World
o.trackedPropertiesPerThangValues.push (trackedPropertiesValues = []) o.trackedPropertiesPerThangValues.push (trackedPropertiesValues = [])
trackedPropertiesValuesOffsets = o.trackedPropertiesPerThangValuesOffsets[thangIndex] trackedPropertiesValuesOffsets = o.trackedPropertiesPerThangValuesOffsets[thangIndex]
for type, propIndex in trackedPropertyTypes 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 unless typedArraySupport
# This could be more efficient # This could be more efficient
i = trackedPropertiesValuesOffsets[propIndex] i = trackedPropertiesValuesOffsets[propIndex]
@ -409,45 +456,52 @@ module.exports = class World
perf.t3 = now() perf.t3 = now()
perf.batches = 0 perf.batches = 0
w.frames = [] perf.framesCPUTime = 0
_.delay @deserializeSomeFrames, 1, o, w, finishedWorldCallback, perf 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 # 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 ++perf.batches
startTime = now() startTime = now()
for frameIndex in [w.frames.length ... o.totalFrames] for frameIndex in [w.frames.length ... endFrame]
w.frames.push WorldFrame.deserialize(w, frameIndex, o.trackedPropertiesThangIDs, o.trackedPropertiesThangs, o.trackedPropertiesPerThangKeys, o.trackedPropertiesPerThangTypes, o.trackedPropertiesPerThangValues, o.specialKeysToValues, o.frameHashes[frameIndex]) 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)
if (now() - startTime) > DESERIALIZATION_INTERVAL elapsed = now() - startTime
_.delay @deserializeSomeFrames, 1, o, w, finishedWorldCallback, perf 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 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() perf.t4 = now()
w.ended = true w.ended = true
w.getFrame(w.totalFrames - 1).restoreState() nFrames = endFrame - startFrame
perf.t5 = now() totalCPUTime = perf.t3 - perf.t0 + perf.framesCPUTime
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.' #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 if false
console.log ' Deserializing--constructing new World:', (perf.t1 - perf.t0).toFixed(2) + 'ms' 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--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--reallocating memory:', (perf.t3 - perf.t2).toFixed(2) + 'ms'
console.log ' Deserializing--WorldFrames:', (perf.t4 - perf.t3).toFixed(2) + 'ms' console.log ' Deserializing--WorldFrames:', (perf.t4 - perf.t3).toFixed(2) + 'ms wall clock time,', (perf.framesCPUTime).toFixed(2) + 'ms CPU time'
console.log ' Deserializing--restoring last WorldFrame:', (perf.t5 - perf.t4).toFixed(2) + 'ms'
finishedWorldCallback w finishedWorldCallback w
findFirstChangedFrame: (oldWorld) -> findFirstChangedFrame: (oldWorld) ->
return @firstChangedFrame = 0 unless oldWorld return 0 unless oldWorld
for newFrame, i in @frames for newFrame, i in @frames
oldFrame = oldWorld.frames[i] oldFrame = oldWorld.frames[i]
break unless oldFrame and newFrame.hash is oldFrame.hash 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 firstChangedFrame = i
if @frames.length is @totalFrames
if @frames[i] 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 else
console.log 'No frames were changed out of all', @frames.length console.log 'No frames were changed out of all', @frames.length
@firstChangedFrame firstChangedFrame
pointsForThang: (thangID, frameStart=0, frameEnd=null, camera=null, resolution=4) -> pointsForThang: (thangID, frameStart=0, frameEnd=null, camera=null, resolution=4) ->
# Optimized # Optimized

View file

@ -60,9 +60,9 @@ module.exports = class WorldFrame
thangState.serialize(frameIndex, trackedPropertiesPerThangIndices[thangIndex], trackedPropertiesPerThangTypes[thangIndex], trackedPropertiesPerThangValues[thangIndex], specialValuesToKeys, specialKeysToValues) thangState.serialize(frameIndex, trackedPropertiesPerThangIndices[thangIndex], trackedPropertiesPerThangTypes[thangIndex], trackedPropertiesPerThangValues[thangIndex], specialValuesToKeys, specialKeysToValues)
@hash @hash
@deserialize: (world, frameIndex, trackedPropertiesThangIDs, trackedPropertiesThangs, trackedPropertiesPerThangKeys, trackedPropertiesPerThangTypes, trackedPropertiesPerThangValues, specialKeysToValues, hash) -> @deserialize: (world, frameIndex, trackedPropertiesThangIDs, trackedPropertiesThangs, trackedPropertiesPerThangKeys, trackedPropertiesPerThangTypes, trackedPropertiesPerThangValues, specialKeysToValues, hash, age) ->
# Optimize # Optimize
wf = new WorldFrame null, world.dt * frameIndex wf = new WorldFrame null, age
wf.world = world wf.world = world
wf.hash = hash wf.hash = hash
for thangID, thangIndex in trackedPropertiesThangIDs for thangID, thangIndex in trackedPropertiesThangIDs

View file

@ -498,7 +498,9 @@
space: "Space" space: "Space"
enter: "Enter" enter: "Enter"
escape: "Escape" escape: "Escape"
shift: "Shift"
cast_spell: "Cast current spell." cast_spell: "Cast current spell."
run_real_time: "Run in real time."
continue_script: "Continue past current script." continue_script: "Continue past current script."
skip_scripts: "Skip past all skippable scripts." skip_scripts: "Skip past all skippable scripts."
toggle_playback: "Toggle play/pause." toggle_playback: "Toggle play/pause."

View file

@ -874,7 +874,7 @@ module.exports = nativeDescription: "Português (Portugal)", englishDescription:
tournament_ended: "O Torneio acabou" tournament_ended: "O Torneio acabou"
tournament_rules: "Regras do Torneio" 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: "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" tournament_blurb_blog: "no nosso blog"
rules: "Regras" rules: "Regras"
winners: "Vencedores" winners: "Vencedores"
@ -945,7 +945,7 @@ module.exports = nativeDescription: "Português (Portugal)", englishDescription:
candidate_sessions: "Sessões de Candidatos" candidate_sessions: "Sessões de Candidatos"
user_remark: "Observação do Utilizador" user_remark: "Observação do Utilizador"
versions: "Versões" versions: "Versões"
# items: "Items" items: "Itens"
delta: delta:
added: "Adicionados/as" added: "Adicionados/as"

View file

@ -26,14 +26,14 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
minutes: "分钟" minutes: "分钟"
hour: "小时" hour: "小时"
hours: "小时" hours: "小时"
# day: "day" day: ""
# days: "days" days: ""
# week: "week" week: "星期"
# weeks: "weeks" weeks: "星期"
# month: "month" month: ""
# months: "months" months: ""
# year: "year" year: ""
# years: "years" years: ""
modal: modal:
close: "关闭" close: "关闭"
@ -49,9 +49,9 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
blog: "博客" blog: "博客"
forum: "论坛" forum: "论坛"
account: "账号" account: "账号"
# profile: "Profile" profile: "资料"
# stats: "Stats" stats: "成就"
# code: "Code" code: "代码"
admin: "管理" admin: "管理"
home: "首页" home: "首页"
contribute: "贡献" contribute: "贡献"
@ -90,7 +90,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
sign_up: "注册" sign_up: "注册"
log_in: "登录" log_in: "登录"
social_signup: "或者你可以通过Facebook或G+注册:" social_signup: "或者你可以通过Facebook或G+注册:"
# required: "You need to log in before you can go that way." required: "在做这件事情之前你必须先注册。"
home: home:
slogan: "通过游戏学习编程" slogan: "通过游戏学习编程"
@ -98,17 +98,17 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
no_mobile: "CodeCombat 不是针对手机设备设计的,所以可能无法达到最好的体验!" no_mobile: "CodeCombat 不是针对手机设备设计的,所以可能无法达到最好的体验!"
play: "开始游戏" play: "开始游戏"
old_browser: "噢, 你的浏览器太老了, 不能运行CodeCombat. 抱歉!" old_browser: "噢, 你的浏览器太老了, 不能运行CodeCombat. 抱歉!"
old_browser_suffix: "尽管你可以多试几次, 但也许不会管用." old_browser_suffix: "你可以继续重试下去,但八成不起作用,更新浏览器吧亲~"
campaign: "战役模式" campaign: "战役模式"
for_beginners: "适合初学者" for_beginners: "适合初学者"
multiplayer: "多人游戏" multiplayer: "多人游戏"
for_developers: "适合开发者" for_developers: "适合开发者"
# javascript_blurb: "The language of the web. Great for writing websites, web apps, HTML5 games, and servers." javascript_blurb: "为web开发而生的语言。 非常适合制作网站, 网站apps, HTML5游戏或开发服务器。"
# python_blurb: "Simple yet powerful, Python is a great general purpose programming language." python_blurb: "简单而强大, Python是一个伟大的通用编程语言。"
# coffeescript_blurb: "Nicer JavaScript syntax." coffeescript_blurb: "一种更好的JavaScript语法."
# clojure_blurb: "A modern Lisp." clojure_blurb: "一种现代的列表处理语言。"
# lua_blurb: "Game scripting language." lua_blurb: "一种游戏脚本语言。"
# io_blurb: "Simple but obscure." io_blurb: "简单而晦涩。"
play: play:
choose_your_level: "选择关卡" choose_your_level: "选择关卡"
@ -158,7 +158,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
trim: "条纹" trim: "条纹"
cloud: "" cloud: ""
team: "队伍" team: "队伍"
spell: "法球" spell: "法球"
boots: "鞋子" boots: "鞋子"
hue: "颜色" hue: "颜色"
saturation: "饱和度" saturation: "饱和度"
@ -178,17 +178,17 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
wizard_color: "巫师 衣服 颜色" wizard_color: "巫师 衣服 颜色"
new_password: "新密码" new_password: "新密码"
new_password_verify: "核实" new_password_verify: "核实"
email_subscriptions: "邮箱验证" email_subscriptions: "邮箱订阅"
# email_subscriptions_none: "No Email Subscriptions." email_subscriptions_none: "取消订阅"
email_announcements: "通知" email_announcements: "通知"
email_announcements_description: "接收关于 CodeCombat 的邮件。" email_announcements_description: "接收关于 CodeCombat 的邮件。"
email_notifications: "通知" 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: "任何通知"
email_any_notes_description: "取消接收所有活动提醒邮件" email_any_notes_description: "取消接收所有活动提醒邮件"
# email_news: "News" email_news: "新消息"
email_recruit_notes: "工作机会" 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: "贡献者通知" contributor_emails: "贡献者通知"
contribute_prefix: "我们在寻找志同道合的人!请到" contribute_prefix: "我们在寻找志同道合的人!请到"
contribute_page: "贡献页面" contribute_page: "贡献页面"
@ -197,17 +197,17 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
error_saving: "保存时出错" error_saving: "保存时出错"
saved: "更改已保存" saved: "更改已保存"
password_mismatch: "密码不匹配。" password_mismatch: "密码不匹配。"
# password_repeat: "Please repeat your password." password_repeat: "请重新键入密码。"
job_profile: "工作经历" job_profile: "工作经历"
job_profile_approved: "你填写的工作经历将由CodeCombat认证. 雇主将看到这些信息,除非你将它设置为不启用状态或者连续四周没有更新." job_profile_approved: "你填写的工作经历将由CodeCombat认证. 雇主将看到这些信息,除非你将它设置为不启用状态或者连续四周没有更新."
job_profile_explanation: "你好! 填写这些信息, 我们将使用它帮你寻找一份软件开发的工作." job_profile_explanation: "你好! 请填写下列信息, 我们将使用它帮你寻找一份软件开发的工作."
# sample_profile: "See a sample profile" sample_profile: "查看示例"
view_profile: "浏览个人信息" view_profile: "浏览个人信息"
account_profile: account_profile:
# settings: "Settings" settings: "设置"
# edit_profile: "Edit Profile" edit_profile: "编辑资料"
# done_editing: "Done Editing" done_editing: "完成编辑"
profile_for_prefix: "关于他的基本资料:" profile_for_prefix: "关于他的基本资料:"
profile_for_suffix: "" profile_for_suffix: ""
# featured: "Featured" # featured: "Featured"
@ -215,8 +215,8 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
looking_for: "寻找" looking_for: "寻找"
last_updated: "最后一次更新:" last_updated: "最后一次更新:"
contact: "联系" contact: "联系"
# active: "Looking for interview offers now" active: "正期待面试offer"
# inactive: "Not looking for offers right now" inactive: "并不期待面试offer"
# complete: "complete" # complete: "complete"
# next: "Next" # next: "Next"
# next_city: "city?" # next_city: "city?"
@ -485,31 +485,31 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
multiplayer: multiplayer:
multiplayer_title: "多人游戏设置" multiplayer_title: "多人游戏设置"
# multiplayer_toggle: "Enable multiplayer" multiplayer_toggle: "开启多人模式"
# multiplayer_toggle_description: "Allow others to join your game." multiplayer_toggle_description: "允许其他人加入游戏。"
multiplayer_link_description: "把这个链接告诉小伙伴们,一起玩吧。" multiplayer_link_description: "把这个链接告诉小伙伴们,一起玩吧。"
multiplayer_hint_label: "提示:" multiplayer_hint_label: "提示:"
multiplayer_hint: " 点击全选,然后按 Apple-C苹果电脑或 Ctrl-C 复制链接。" multiplayer_hint: " 点击全选,然后按 Apple-C苹果电脑或 Ctrl-C 复制链接。"
multiplayer_coming_soon: "多人游戏的更多特性!" 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: "Keyboard Shortcuts" keyboard_shortcuts: "热键"
# space: "Space" space: "空格"
# enter: "Enter" enter: "回车"
# escape: "Escape" # escape: "Escape"
# cast_spell: "Cast current spell." cast_spell: "演示当前咒语"
# continue_script: "Continue past current script." # continue_script: "Continue past current script."
# skip_scripts: "Skip past all skippable scripts." # skip_scripts: "Skip past all skippable scripts."
# toggle_playback: "Toggle play/pause." toggle_playback: "继续/暂停按钮"
# scrub_playback: "Scrub back and forward through time." # scrub_playback: "Scrub back and forward through time."
# single_scrub_playback: "Scrub back and forward through time by a single frame." # single_scrub_playback: "Scrub back and forward through time by a single frame."
# scrub_execution: "Scrub through current spell execution." # scrub_execution: "Scrub through current spell execution."
# toggle_debug: "Toggle debug display." # toggle_debug: "Toggle debug display."
# toggle_grid: "Toggle grid overlay." # toggle_grid: "Toggle grid overlay."
# toggle_pathfinding: "Toggle pathfinding overlay." # toggle_pathfinding: "Toggle pathfinding overlay."
# beautify: "Beautify your code by standardizing its formatting." beautify: "利用标准编码格式美化你的代码。"
# move_wizard: "Move your Wizard around the level." move_wizard: "在关卡中移动你的巫师角色。"
admin: admin:
av_title: "管理员视图" av_title: "管理员视图"
@ -521,43 +521,43 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
av_other_debug_base_url: "Base用于调试 base.jade" av_other_debug_base_url: "Base用于调试 base.jade"
u_title: "用户列表" u_title: "用户列表"
lg_title: "最新的游戏" lg_title: "最新的游戏"
# clas: "CLAs" clas: "贡献者许可协议"
community: community:
main_title: "CodeCombat 社区" 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!" introduction: "看看下面这些你可以参与的项目,如果有你喜欢的就加入进来吧。 我们期待着与您一起工作。"
# level_editor_prefix: "Use the CodeCombat" level_editor_prefix: "使用"
# 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!" level_editor_suffix: "来创建和编辑关卡。你可以通过这个工具来给你的同学,朋友,兄弟姐妹们设计谜题,或者用于教学或比赛。如果你觉得直接开始建立一个关卡可能非常困难,那么可以先从一个现成(但尚未完成)的关卡开始做起。"
# thang_editor_prefix: "We call units within the game 'thangs'. Use the" thang_editor_prefix: "我们管游戏中的单位叫 '实体'。 利用"
# 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." thang_editor_suffix: "来改良 CodeCombat 中的原材料。让游戏中的东西可以被捡起来扔出去,改变游戏动画的指向,调整一些东西的生命值,或上传您自制的素材。"
# 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_prefix: "你在游戏中发现了错误了吗?想要自己设计一些指令吗?来看看我们的"
# article_editor_suffix: "and help CodeCombat players get the most out of their playtime." article_editor_suffix: "来帮助玩家从游戏中学到更多的知识。"
# find_us: "Find us on these sites" find_us: "通过这些站点联系我们"
# contribute_to_the_project: "Contribute to the project" contribute_to_the_project: "为项目做贡献"
editor: editor:
main_title: "CodeCombat 编辑器" main_title: "CodeCombat 编辑器"
main_description: "建立你自己的关卡、 战役、单元和教育内容。我们会提供所有你需要的工具!" main_description: "建立你自己的关卡、 战役、单元和新手教程。我们会提供所有你需要的工具!"
article_title: "提示编辑器" article_title: "指令编辑器"
article_description: "编写提示,让玩家可以使用编程概念来通过各种关卡和战役。" article_description: "编写指令,让玩家可以使用编程概念来通过各种关卡和战役。"
thang_title: "体编辑器" thang_title: "体编辑器"
thang_description: "创建单元,并定义单元的逻辑、图形和音频。目前只支持导入 Flash 导出的矢量图形。" thang_description: "创建单位,并定义单位的逻辑、图形和音频。目前只支持导入 Flash 导出的矢量图形。"
level_title: "关卡编辑器" level_title: "关卡编辑器"
level_description: "所有用来创造所有难度的关卡的工具,包括脚本、上传音频和构建自定义逻辑。" level_description: "所有用来创造所有难度的关卡的工具,包括脚本、上传音频和构建自定义逻辑。"
# achievement_title: "Achievement Editor" achievement_title: "目标编辑器"
got_questions: "使用CodeCombat编辑器有问题" got_questions: "使用CodeCombat编辑器有问题"
contact_us: "联系我们!" contact_us: "联系我们!"
hipchat_prefix: "你也可以在这里找到我们" hipchat_prefix: "你也可以在这里找到我们"
hipchat_url: "HipChat 房间" hipchat_url: "HipChat 聊天室"
back: "后退" back: "后退"
revert: "还原" revert: "还原"
revert_models: "还原模式" revert_models: "还原模式"
# pick_a_terrain: "Pick A Terrain" pick_a_terrain: "选择地形"
# small: "Small" small: "小的"
# grassy: "Grassy" grassy: "草地"
fork_title: "派生新版本" fork_title: "派生新版本"
fork_creating: "正在执行派生..." fork_creating: "正在执行派生..."
# randomize: "Randomize" randomize: "随机生成"
more: "更多" more: "更多"
wiki: "维基" wiki: "维基"
live_chat: "在线聊天" live_chat: "在线聊天"
@ -591,29 +591,29 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
new_article_title: "创建一个新物品" new_article_title: "创建一个新物品"
new_thang_title: "创建一个新物品类型" new_thang_title: "创建一个新物品类型"
new_level_title: "创建一个新关卡" new_level_title: "创建一个新关卡"
# new_article_title_login: "Log In to Create a New Article" new_article_title_login: "登录以创建新指令"
# new_thang_title_login: "Log In to Create a New Thang Type" new_thang_title_login: "登录以创建新实体"
# new_level_title_login: "Log In to Create a New Level" new_level_title_login: "登录以创建新关卡"
# new_achievement_title: "Create a New Achievement" new_achievement_title: "创建新目标"
# new_achievement_title_login: "Log In to Create a New Achievement" new_achievement_title_login: "登录以创建新目标"
article_search_title: "在这里搜索物品" article_search_title: "在这里搜索物品"
thang_search_title: "在这里搜索物品类型" thang_search_title: "在这里搜索物品类型"
level_search_title: "在这里搜索关卡" level_search_title: "在这里搜索关卡"
# achievement_search_title: "Search Achievements" achievement_search_title: "搜索目标"
read_only_warning2: "提示:你不能保存任何编辑,因为你没有登陆" 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_misc: "Key achievement off of miscellanea"
# achievement_query_goals: "Key achievement off of level goals" # achievement_query_goals: "Key achievement off of level goals"
# level_completion: "Level Completion" level_completion: "关卡完成"
article: article:
edit_btn_preview: "预览" edit_btn_preview: "预览"
edit_article_title: "编辑提示" edit_article_title: "编辑提示"
general: general:
and: "" and: ""
name: "名字" name: "名字"
# date: "Date" date: "日期"
body: "正文" body: "正文"
version: "版本" version: "版本"
commit_msg: "提交信息" commit_msg: "提交信息"
@ -657,7 +657,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
why_paragraph_4: "如果你一定要对游戏上瘾,那就对这个游戏上瘾,然后成为科技时代的法师吧。" why_paragraph_4: "如果你一定要对游戏上瘾,那就对这个游戏上瘾,然后成为科技时代的法师吧。"
why_ending: "再说,这游戏还是免费的。" why_ending: "再说,这游戏还是免费的。"
why_ending_url: "开始学习法术!" 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." # 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." # 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." # jeremy_description: "Customer support mage, usability tester, and community organizer; you've probably already spoken with Jeremy."

View file

@ -63,6 +63,7 @@ module.exports = class ThangType extends CocoModel
options = _.clone options options = _.clone options
options.resolutionFactor ?= SPRITE_RESOLUTION_FACTOR options.resolutionFactor ?= SPRITE_RESOLUTION_FACTOR
options.async ?= false options.async ?= false
options.thang = null # Don't hold onto any bad Thang references.
options options
buildSpriteSheet: (options) -> buildSpriteSheet: (options) ->

View file

@ -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.'} 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.'}, 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 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.'}, GoalSchema 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.'}, 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.'} playing: {type: 'boolean', title: 'Set Playing', description: 'Set whether playback is playing or paused.'}

View file

@ -57,6 +57,48 @@ module.exports =
'level:victory-hidden': 'level:victory-hidden':
{} # TODO schema {} # 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': 'next-game-pressed':
{} # TODO schema {} # TODO schema
@ -105,6 +147,14 @@ module.exports =
'playback:manually-scrubbed': 'playback:manually-scrubbed':
{} # TODO schema {} # TODO schema
'playback:real-time-playback-started':
type: 'object'
additionalProperties: false
'playback:real-time-playback-ended':
type: 'object'
additionalProperties: false
'change:editor-config': 'change:editor-config':
{} # TODO schema {} # TODO schema

View file

@ -94,3 +94,15 @@ module.exports = # /app/lib/surface
'echo-all-wizard-sprites': 'echo-all-wizard-sprites':
{} # TODO schema {} # TODO schema
'surface:flag-appeared':
type: 'object'
additionalProperties: false
required: ['sprite']
properties:
sprite:
type: 'object'
'surface:remove-selected-flag':
type: 'object'
additionalProperties: false

View file

@ -11,10 +11,11 @@ module.exports =
type: "object" type: "object"
preload: preload:
type: "boolean" type: "boolean"
realTime:
type: "boolean"
required: [] required: []
additionalProperties: false additionalProperties: false
# TODO do we really need both 'cast-spell' and 'cast-spells'?
"tome:cast-spells": "tome:cast-spells":
title: "Cast Spells" title: "Cast Spells"
$schema: "http://json-schema.org/draft-04/schema#" $schema: "http://json-schema.org/draft-04/schema#"
@ -25,6 +26,8 @@ module.exports =
type: "object" type: "object"
preload: preload:
type: "boolean" type: "boolean"
realTime:
type: "boolean"
required: [] required: []
additionalProperties: false additionalProperties: false
@ -33,7 +36,9 @@ module.exports =
$schema: "http://json-schema.org/draft-04/schema#" $schema: "http://json-schema.org/draft-04/schema#"
description: "Published when you wish to manually recast all spells" description: "Published when you wish to manually recast all spells"
type: "object" type: "object"
properties: {} properties:
realTime:
type: "boolean"
required: [] required: []
additionalProperties: false additionalProperties: false

View file

@ -11,5 +11,8 @@ module.exports =
'god:new-world-created': 'god:new-world-created':
{} # TODO schema {} # TODO schema
'god:streaming-world-updated':
{} # TODO schema
'god:world-load-progress-changed': 'god:world-load-progress-changed':
{} # TODO schema {} # TODO schema

View file

@ -225,10 +225,6 @@ table.table
.ui-slider-handle .ui-slider-handle
border: 1px solid black !important border: 1px solid black !important
.flag-cursor
cursor: crosshair
// Fonts // Fonts
.header-font .header-font

View file

@ -10,6 +10,34 @@ body.is-playing
margin: 0 auto margin: 0 auto
@include user-select(none) @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 .level-content
position: relative position: relative
@ -17,12 +45,16 @@ body.is-playing
width: 55% width: 55%
position: relative position: relative
overflow: hidden overflow: hidden
@include transition(0.5s ease-out)
canvas#surface canvas#surface
background-color: #333 background-color: #333
width: 100%
display: block display: block
z-index: 1 z-index: 1
@include transition(0.5s ease-out)
&.flag-color-selected
cursor: crosshair
min-width: 1024px min-width: 1024px
position: relative position: relative

View file

@ -6,9 +6,8 @@
position: absolute position: absolute
right: 46% right: 46%
top: 42px top: 42px
user-select: none
-webkit-user-select: none
@include transition(box-shadow .2s linear) @include transition(box-shadow .2s linear)
@include user-select(none)
padding: 4px padding: 4px
background: transparent url(/images/level/gold_background.png) no-repeat background: transparent url(/images/level/gold_background.png) no-repeat
background-size: 100% 100% background-size: 100% 100%

View 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

View file

@ -97,6 +97,8 @@
background-image: none background-image: none
border-radius: 0 border-radius: 0
border: 0 border: 0
// Can't do this transition because handle then jitters, but would be good for streaming.
//@include transition(width .2s linear)
&.disabled &.disabled
cursor: default cursor: default

View file

@ -82,7 +82,9 @@
height: 40px height: 40px
.cast-button .cast-button
width: 100% width: 80%
width: -webkit-calc(100% - 40px)
width: calc(100% - 40px)
border-top-left-radius: 6px border-top-left-radius: 6px
border-bottom-left-radius: 6px border-bottom-left-radius: 6px
@ -92,3 +94,8 @@
font-size: 14px font-size: 14px
@media screen and (max-width: 1024px) @media screen and (max-width: 1024px)
font-size: 12px font-size: 12px
.cast-real-time-button
width: 20%
width: -webkit-calc(40px)
width: calc(40px)

View file

@ -19,11 +19,11 @@
.title .title
position: absolute position: absolute
display: inline-block display: inline-block
margin-left: 50%
right: 0
color: #BEBEBE color: #BEBEBE
line-height: 15px line-height: 15px
left: 0 left: 20%
width: 60%
text-align: center
max-width: 1920px max-width: 1920px
margin: 0 auto margin: 0 auto

View file

@ -13,6 +13,8 @@
#canvas-top-gradient.gradient #canvas-top-gradient.gradient
#goals-view.secret #goals-view.secret
#level-flags-view.secret
#gold-view.secret.expanded #gold-view.secret.expanded
#level-chat-view #level-chat-view

View 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

View file

@ -8,6 +8,10 @@ block modal-body-content
dt(title="Shift+" + enter) dt(title="Shift+" + enter)
code ⇧+#{enter} code ⇧+#{enter}
dd(data-i18n="keyboard_shortcuts.cast_spell") Cast current spell. 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 dl.dl-horizontal
dt(title="Shift+" + space) dt(title="Shift+" + space)
code ⇧+#{space} code ⇧+#{space}

View file

@ -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-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

View file

@ -83,5 +83,5 @@ module.exports = class SearchView extends RootView
newModel: (e) -> newModel: (e) ->
modal = new NewModelModal model: @model, modelLabel: @modelLabel modal = new NewModelModal model: @model, modelLabel: @modelLabel
modal.once 'success', @onNewModelSaved modal.once 'model-created', @onNewModelSaved
@openModalView modal @openModalView modal

View file

@ -55,6 +55,7 @@ module.exports = class SpectateLevelView extends RootView
'surface:world-set-up': 'onSurfaceSetUpNewWorld' 'surface:world-set-up': 'onSurfaceSetUpNewWorld'
'level:set-team': 'setTeam' 'level:set-team': 'setTeam'
'god:new-world-created': 'loadSoundsForWorld' 'god:new-world-created': 'loadSoundsForWorld'
'god:streaming-world-updated': 'loadSoundsForWorld'
'next-game-pressed': 'onNextGamePressed' 'next-game-pressed': 'onNextGamePressed'
'level:started': 'onLevelStarted' 'level:started': 'onLevelStarted'
'level:loading-view-unveiled': 'onLoadingViewUnveiled' 'level:loading-view-unveiled': 'onLoadingViewUnveiled'

View file

@ -197,8 +197,9 @@ module.exports = class LadderTabView extends CocoView
formatCount = d3.format(',.0') 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) data = d3.layout.histogram().bins(x.ticks(20))(histogramData)
y = d3.scale.linear().domain([0, d3.max(data, (d) -> d.y)]).range([height, 10]) y = d3.scale.linear().domain([0, d3.max(data, (d) -> d.y)]).range([height, 10])

View 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

View file

@ -70,6 +70,8 @@ module.exports = class LevelHUDView extends CocoView
@thang = e.world.thangMap[@thang.id] if @thang @thang = e.world.thangMap[@thang.id] if @thang
if hadThang and not @thang if hadThang and not @thang
@setThang null, null @setThang null, null
else if @thang
@createActions() # Make sure it updates its actions.
setThang: (thang, thangType) -> setThang: (thang, thangType) ->
unless @speaker unless @speaker

View file

@ -22,8 +22,10 @@ module.exports = class LevelPlaybackView extends CocoView
'level-toggle-grid': 'onToggleGrid' 'level-toggle-grid': 'onToggleGrid'
'surface:frame-changed': 'onFrameChanged' 'surface:frame-changed': 'onFrameChanged'
'god:new-world-created': 'onNewWorld' 'god:new-world-created': 'onNewWorld'
'god:streaming-world-updated': 'onNewWorld'
'level-set-letterbox': 'onSetLetterbox' 'level-set-letterbox': 'onSetLetterbox'
'tome:cast-spells': 'onCastSpells' 'tome:cast-spells': 'onTomeCast'
'playback:real-time-playback-ended': 'onRealTimePlaybackEnded'
events: events:
'click #debug-toggle': 'onToggleDebug' '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 #zoom-out-button': -> Backbone.Mediator.publish('camera-zoom-out') unless @shouldIgnore()
'click #volume-button': 'onToggleVolume' 'click #volume-button': 'onToggleVolume'
'click #play-button': 'onTogglePlay' 'click #play-button': 'onTogglePlay'
'click': -> Backbone.Mediator.publish 'tome:focus-editor' 'click': -> Backbone.Mediator.publish 'tome:focus-editor' unless @realTime
'mouseenter #timeProgress': 'onProgressEnter' 'mouseenter #timeProgress': 'onProgressEnter'
'mouseleave #timeProgress': 'onProgressLeave' 'mouseleave #timeProgress': 'onProgressLeave'
'mousemove #timeProgress': 'onProgressHover' 'mousemove #timeProgress': 'onProgressHover'
@ -114,6 +116,15 @@ module.exports = class LevelPlaybackView extends CocoView
ua = navigator.userAgent.toLowerCase() ua = navigator.userAgent.toLowerCase()
if /safari/.test(ua) and not /chrome/.test(ua) if /safari/.test(ua) and not /chrome/.test(ua)
@$el.find('.toggle-fullscreen').hide() @$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: -> updatePopupContent: ->
@timePopup?.updateContent "<h2>#{@timeToString @newTime}</h2>#{@formatTime(@current, @currentTime)}<br/>#{@formatTime(@total, @totalTime)}" @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')) @$el.find('#music-button').toggleClass('music-on', me.get('music'))
onSetLetterbox: (e) -> onSetLetterbox: (e) ->
buttons = @$el.find '#play-button, .scrubber-handle' return if @realTime
buttons.css 'visibility', if e.on then 'hidden' else 'visible' @togglePlaybackControls !e.on
@disabled = 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...) => onWindowResize: (s...) =>
@barWidth = $('.progress', @$el).width() @barWidth = $('.progress', @$el).width()
onNewWorld: (e) -> onNewWorld: (e) ->
@totalTime = e.world.totalFrames / e.world.frameRate @updateBarWidth e.world.frames.length, e.world.maxTotalFrames, e.world.dt
pct = parseInt(100 * e.world.totalFrames / e.world.maxTotalFrames) + '%'
updateBarWidth: (loadedFrameCount, maxTotalFrames, dt) ->
@totalTime = loadedFrameCount * dt
pct = parseInt(100 * loadedFrameCount / maxTotalFrames) + '%'
@barWidth = $('.progress', @$el).css('width', pct).show().width() @barWidth = $('.progress', @$el).css('width', pct).show().width()
@casting = false
$('.scrubber .progress', @$el).slider('enable', true) $('.scrubber .progress', @$el).slider('enable', true)
@newTime = 0 @newTime = 0
@currentTime = 0 @currentTime = 0
@lastLoadedFrameCount = loadedFrameCount
@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'
onToggleDebug: -> onToggleDebug: ->
return if @shouldIgnore() return if @shouldIgnore()
@ -188,11 +201,6 @@ module.exports = class LevelPlaybackView extends CocoView
onViewKeyboardShortcuts: -> onViewKeyboardShortcuts: ->
@openModalView new KeyboardShortcutsModal() @openModalView new KeyboardShortcutsModal()
onCastSpells: (e) ->
return if e.preload
@casting = true
@$progressScrubber.slider('disable', true)
onDisableControls: (e) -> onDisableControls: (e) ->
if not e.controls or 'playback' in e.controls if not e.controls or 'playback' in e.controls
@disabled = true @disabled = true
@ -205,6 +213,7 @@ module.exports = class LevelPlaybackView extends CocoView
$('#volume-button', @$el).removeClass('disabled') $('#volume-button', @$el).removeClass('disabled')
onEnableControls: (e) -> onEnableControls: (e) ->
return if @realTime
if not e.controls or 'playback' in e.controls if not e.controls or 'playback' in e.controls
@disabled = false @disabled = false
$('button', @$el).removeClass('disabled') $('button', @$el).removeClass('disabled')
@ -255,7 +264,7 @@ module.exports = class LevelPlaybackView extends CocoView
# @currentTime = @totalTime if Math.abs(@totalTime - @currentTime) < 0.04 # @currentTime = @totalTime if Math.abs(@totalTime - @currentTime) < 0.04
@updatePopupContent() if @timePopup?.shown @updatePopupContent() if @timePopup?.shown
@updateProgress(e.progress) @updateProgress(e.progress, e.world)
@updatePlayButton(e.progress) @updatePlayButton(e.progress)
@lastProgress = 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 if @timePopup and Math.abs(@currentTime - @newTime) < 1 and not @timePopup.shown
@timePopup.show() @timePopup.show()
updateProgress: (progress) -> 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}%") $('.scrubber .progress-bar', @$el).css('width', "#{progress * 100}%")
updatePlayButton: (progress) -> 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') $('#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 if progress < 0.99 and @lastProgress >= 0.99
b = $('#play-button').removeClass('ended') b = $('#play-button').removeClass('ended')
if @playing then b.addClass('playing') else b.addClass('paused') if @playing then b.addClass('playing') else b.addClass('paused')
onRealTimePlaybackEnded: (e) ->
return unless @realTime
@realTime = false
@togglePlaybackControls true
onSetDebug: (e) -> onSetDebug: (e) ->
flag = $('#debug-toggle i.icon-ok') flag = $('#debug-toggle i.icon-ok')
flag.toggleClass 'invisible', not e.debug flag.toggleClass 'invisible', not e.debug
@ -299,20 +320,22 @@ module.exports = class LevelPlaybackView extends CocoView
hookUpScrubber: -> hookUpScrubber: ->
@sliderIncrements = 500 # max slider width before we skip pixels @sliderIncrements = 500 # max slider width before we skip pixels
@clickingSlider = false # whether the mouse has been pressed down without moving
@$progressScrubber.slider( @$progressScrubber.slider(
max: @sliderIncrements max: @sliderIncrements
animate: 'slow' animate: 'slow'
slide: (event, ui) => slide: (event, ui) =>
return if @shouldIgnore()
@scrubTo ui.value / @sliderIncrements @scrubTo ui.value / @sliderIncrements
@slideCount += 1 @slideCount += 1
start: (event, ui) => start: (event, ui) =>
return if @shouldIgnore()
@slideCount = 0 @slideCount = 0
@wasPlaying = @playing @wasPlaying = @playing
Backbone.Mediator.publish 'level-set-playing', {playing: false} Backbone.Mediator.publish 'level-set-playing', {playing: false}
stop: (event, ui) => stop: (event, ui) =>
return if @shouldIgnore()
@actualProgress = ui.value / @sliderIncrements @actualProgress = ui.value / @sliderIncrements
Backbone.Mediator.publish 'playback:manually-scrubbed', ratio: @actualProgress Backbone.Mediator.publish 'playback:manually-scrubbed', ratio: @actualProgress
Backbone.Mediator.publish 'level-set-playing', {playing: @wasPlaying} Backbone.Mediator.publish 'level-set-playing', {playing: @wasPlaying}
@ -329,7 +352,7 @@ module.exports = class LevelPlaybackView extends CocoView
return if @shouldIgnore() return if @shouldIgnore()
Backbone.Mediator.publish 'level-set-time', ratio: ratio, scrubDuration: duration Backbone.Mediator.publish 'level-set-time', ratio: ratio, scrubDuration: duration
shouldIgnore: -> return @disabled or @casting or false shouldIgnore: -> return @disabled or @realTime
onTogglePlay: (e) -> onTogglePlay: (e) ->
e?.preventDefault() e?.preventDefault()

View file

@ -29,6 +29,7 @@ HUDView = require './LevelHUDView'
ControlBarView = require './ControlBarView' ControlBarView = require './ControlBarView'
LevelPlaybackView = require './LevelPlaybackView' LevelPlaybackView = require './LevelPlaybackView'
GoalsView = require './LevelGoalsView' GoalsView = require './LevelGoalsView'
LevelFlagsView = require './LevelFlagsView'
GoldView = require './LevelGoldView' GoldView = require './LevelGoldView'
VictoryModal = require './modal/VictoryModal' VictoryModal = require './modal/VictoryModal'
InfiniteLoopModal = require './modal/InfiniteLoopModal' InfiniteLoopModal = require './modal/InfiniteLoopModal'
@ -53,6 +54,7 @@ module.exports = class PlayLevelView extends RootView
'level-disable-controls': 'onDisableControls' 'level-disable-controls': 'onDisableControls'
'level-enable-controls': 'onEnableControls' 'level-enable-controls': 'onEnableControls'
'god:new-world-created': 'onNewWorld' 'god:new-world-created': 'onNewWorld'
'god:streaming-world-updated': 'onNewWorld'
'god:infinite-loop': 'onInfiniteLoop' 'god:infinite-loop': 'onInfiniteLoop'
'level-reload-from-data': 'onLevelReloadFromData' 'level-reload-from-data': 'onLevelReloadFromData'
'level-reload-thang-type': 'onLevelReloadThangType' 'level-reload-thang-type': 'onLevelReloadThangType'
@ -63,6 +65,8 @@ module.exports = class PlayLevelView extends RootView
'level:set-team': 'setTeam' 'level:set-team': 'setTeam'
'level:started': 'onLevelStarted' 'level:started': 'onLevelStarted'
'level:loading-view-unveiled': 'onLoadingViewUnveiled' 'level:loading-view-unveiled': 'onLoadingViewUnveiled'
'playback:real-time-playback-started': 'onRealTimePlaybackStarted'
'playback:real-time-playback-ended': 'onRealTimePlaybackEnded'
events: events:
'click #level-done-button': 'onDonePressed' '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 @tome = new TomeView levelID: @levelID, session: @session, otherSession: @otherSession, thangs: @world.thangs, supermodel: @supermodel
@insertSubView new LevelPlaybackView session: @session @insertSubView new LevelPlaybackView session: @session
@insertSubView new GoalsView {} @insertSubView new GoalsView {}
@insertSubView new LevelFlagsView world: @world
@insertSubView new GoldView {} @insertSubView new GoldView {}
@insertSubView new HUDView {} @insertSubView new HUDView {}
@insertSubView new ChatView levelID: @levelID, sessionID: @session.id, session: @session @insertSubView new ChatView levelID: @levelID, sessionID: @session.id, session: @session
@ -507,11 +512,25 @@ module.exports = class PlayLevelView extends RootView
@world = e.world @world = e.world
@world.scripts = scripts @world.scripts = scripts
thangTypes = @supermodel.getModels(ThangType) 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 thangType = _.find thangTypes, (m) -> m.get('name') is spriteName
continue unless sound = AudioPlayer.soundForDialogue message, thangType.get('soundTriggers') continue unless sound = AudioPlayer.soundForDialogue message, thangType.get('soundTriggers')
AudioPlayer.preloadSoundReference sound AudioPlayer.preloadSoundReference sound
# Real-time playback
onRealTimePlaybackStarted: (e) ->
@$el.addClass('real-time').focus()
@onWindowResize()
onRealTimePlaybackEnded: (e) ->
@$el.removeClass 'real-time'
@onWindowResize()
destroy: -> destroy: ->
@levelLoader?.destroy() @levelLoader?.destroy()
@surface?.destroy() @surface?.destroy()

View file

@ -70,7 +70,7 @@ module.exports = class ThangAvatarView extends CocoView
@setProblems myProblems.length, worstLevel @setProblems myProblems.length, worstLevel
onNewWorld: (e) -> 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: -> destroy: ->
super() super()

View file

@ -8,7 +8,7 @@ module.exports = class CastButtonView extends CocoView
events: events:
'click .cast-button': 'onCastButtonClick' 'click .cast-button': 'onCastButtonClick'
'click .autocast-delays a': 'onCastOptionsClick' 'click .cast-real-time-button': 'onCastRealTimeButtonClick'
subscriptions: subscriptions:
'tome:spell-changed': 'onSpellChanged' 'tome:spell-changed': 'onSpellChanged'
@ -21,11 +21,15 @@ module.exports = class CastButtonView extends CocoView
@spells = options.spells @spells = options.spells
@levelID = options.levelID @levelID = options.levelID
@castShortcut = '⇧↵' @castShortcut = '⇧↵'
@castShortcutVerbose = 'Shift+Enter'
getRenderData: (context={}) -> getRenderData: (context={}) ->
context = super 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 context
afterRender: -> afterRender: ->
@ -34,9 +38,7 @@ module.exports = class CastButtonView extends CocoView
@castButtonGroup = $('.cast-button-group', @$el) @castButtonGroup = $('.cast-button-group', @$el)
@castOptions = $('.autocast-delays', @$el) @castOptions = $('.autocast-delays', @$el)
delay = me.get('autocastDelay') delay = me.get('autocastDelay')
delay ?= 5000 delay ?= 90019001
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
@setAutocastDelay delay @setAutocastDelay delay
attachTo: (spellView) -> attachTo: (spellView) ->
@ -45,6 +47,9 @@ module.exports = class CastButtonView extends CocoView
onCastButtonClick: (e) -> onCastButtonClick: (e) ->
Backbone.Mediator.publish 'tome:manual-cast', {} Backbone.Mediator.publish 'tome:manual-cast', {}
onCastRealTimeButtonClick: (e) ->
Backbone.Mediator.publish 'tome:manual-cast', {realTime: true}
onCastOptionsClick: (e) => onCastOptionsClick: (e) =>
Backbone.Mediator.publish 'tome:focus-editor' Backbone.Mediator.publish 'tome:focus-editor'
@castButtonGroup.removeClass 'open' @castButtonGroup.removeClass 'open'
@ -98,6 +103,6 @@ module.exports = class CastButtonView extends CocoView
@autocastDelay = delay = parseInt delay @autocastDelay = delay = parseInt delay
me.set('autocastDelay', delay) me.set('autocastDelay', delay)
me.patch() me.patch()
spell.view.setAutocastDelay delay for spellKey, spell of @spells spell.view?.setAutocastDelay delay for spellKey, spell of @spells
@castOptions.find('a').each -> @castOptions.find('a').each ->
$(@).toggleClass('selected', parseInt($(@).attr('data-delay')) is delay) $(@).toggleClass('selected', parseInt($(@).attr('data-delay')) is delay)

View file

@ -40,6 +40,7 @@ module.exports = class Spell
if @permissions.readwrite.length and sessionSource = @session.getSourceFor(@spellKey) if @permissions.readwrite.length and sessionSource = @session.getSourceFor(@spellKey)
@source = sessionSource @source = sessionSource
@thangs = {} @thangs = {}
if @canRead() # We can avoid creating these views if we'll never use them.
@view = new SpellView {spell: @, session: @session, worker: @worker} @view = new SpellView {spell: @, session: @session, worker: @worker}
@view.render() # Get it ready and code loaded in advance @view.render() # Get it ready and code loaded in advance
@tabView = new SpellListTabEntryView spell: @, supermodel: @supermodel, language: @language @tabView = new SpellListTabEntryView spell: @, supermodel: @supermodel, language: @language
@ -48,8 +49,8 @@ module.exports = class Spell
Backbone.Mediator.publish 'tome:spell-created', spell: @ Backbone.Mediator.publish 'tome:spell-created', spell: @
destroy: -> destroy: ->
@view.destroy() @view?.destroy()
@tabView.destroy() @tabView?.destroy()
@thangs = null @thangs = null
@worker = null @worker = null
@ -72,7 +73,7 @@ module.exports = class Spell
(team ? me.team) in @permissions.readwrite (team ? me.team) in @permissions.readwrite
getSource: -> getSource: ->
@view.getSource() @view?.getSource() ? @source
transpile: (source) -> transpile: (source) ->
if source if source

View file

@ -114,6 +114,10 @@ module.exports = class SpellView extends CocoView
name: 'run-code' name: 'run-code'
bindKey: {win: 'Shift-Enter|Ctrl-Enter', mac: 'Shift-Enter|Command-Enter|Ctrl-Enter'} bindKey: {win: 'Shift-Enter|Ctrl-Enter', mac: 'Shift-Enter|Command-Enter|Ctrl-Enter'}
exec: -> Backbone.Mediator.publish 'tome:manual-cast', {} 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 addCommand
name: 'no-op' name: 'no-op'
bindKey: {win: 'Ctrl-S', mac: 'Command-S|Ctrl-S'} bindKey: {win: 'Ctrl-S', mac: 'Command-S|Ctrl-S'}
@ -269,8 +273,8 @@ module.exports = class SpellView extends CocoView
# @addZatannaSnippets() # @addZatannaSnippets()
@highlightCurrentLine() @highlightCurrentLine()
cast: (preload=false) -> cast: (preload=false, realTime=false) ->
Backbone.Mediator.publish 'tome:cast-spell', spell: @spell, thang: @thang, preload: preload Backbone.Mediator.publish 'tome:cast-spell', spell: @spell, thang: @thang, preload: preload, realTime: realTime
notifySpellChanged: => notifySpellChanged: =>
Backbone.Mediator.publish 'tome:spell-changed', spell: @spell Backbone.Mediator.publish 'tome:spell-changed', spell: @spell
@ -285,7 +289,7 @@ module.exports = class SpellView extends CocoView
onManualCast: (e) -> onManualCast: (e) ->
cast = @$el.parent().length cast = @$el.parent().length
@recompile cast @recompile cast, e.realTime
@focus() if cast @focus() if cast
onCodeReload: (e) -> onCodeReload: (e) ->
@ -299,12 +303,15 @@ module.exports = class SpellView extends CocoView
recompileIfNeeded: => recompileIfNeeded: =>
@recompile() if @recompileNeeded @recompile() if @recompileNeeded
recompile: (cast=true) -> recompile: (cast=true, realTime=false) ->
@setRecompileNeeded false @setRecompileNeeded false
return if @spell.source is @getSource() hasChanged = @spell.source isnt @getSource()
if hasChanged
@spell.transpile @getSource() @spell.transpile @getSource()
@updateAether true, false @updateAether true, false
@cast() if cast if cast and (hasChanged or realTime)
@cast(false, realTime)
if hasChanged
@notifySpellChanged() @notifySpellChanged()
updateACEText: (source) -> updateACEText: (source) ->
@ -472,7 +479,7 @@ module.exports = class SpellView extends CocoView
onSessionWillSave: (e) -> onSessionWillSave: (e) ->
return unless @spellHasChanged return unless @spellHasChanged
setTimeout(=> setTimeout(=>
unless @spellHasChanged unless @destroyed or @spellHasChanged
@$el.find('.save-status').finish().show().fadeOut(2000) @$el.find('.save-status').finish().show().fadeOut(2000)
, 1000) , 1000)
@spellHasChanged = false @spellHasChanged = false
@ -510,7 +517,7 @@ module.exports = class SpellView extends CocoView
spellThang.castAether = aether spellThang.castAether = aether
spellThang.aether = @spell.createAether thang 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 #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 @updateAether false, false
# -------------------------------------------------------------------------------------------------- # --------------------------------------------------------------------------------------------------

View file

@ -29,6 +29,8 @@ module.exports = class ThangListView extends CocoView
false false
), @sortScoreForThang ), @sortScoreForThang
@muggleThangs = _.sortBy _.without(@thangs, @readwriteThangs..., @readThangs...), @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) => sortScoreForThang: (t) =>
# Sort by my team, then most spells and fewest shared Thangs per spell, # Sort by my team, then most spells and fewest shared Thangs per spell,
@ -73,6 +75,12 @@ module.exports = class ThangListView extends CocoView
null null
adjustThangs: (spells, thangs) -> 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 @spells = @options.spells = spells
for entry in @entries for entry in @entries
entry.$el.remove() entry.$el.remove()

View file

@ -142,6 +142,9 @@ module.exports = class TomeView extends CocoView
else else
delete @thangSpells[thangID] delete @thangSpells[thangID]
spell.removeThangID thangID for spell in @spells 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 null
onSpellLoaded: (e) -> onSpellLoaded: (e) ->
@ -152,10 +155,10 @@ module.exports = class TomeView extends CocoView
onCastSpell: (e) -> onCastSpell: (e) ->
# A single spell is cast. # A single spell is cast.
# Hmm; do we need to make sure other spells are all cast here? # Hmm; do we need to make sure other spells are all cast here?
@cast e?.preload @cast e?.preload, e?.realTime
cast: (preload=false) -> cast: (preload=false, realTime=false) ->
Backbone.Mediator.publish 'tome:cast-spells', spells: @spells, preload: preload Backbone.Mediator.publish 'tome:cast-spells', spells: @spells, preload: preload, realTime: realTime
onToggleSpellList: (e) -> onToggleSpellList: (e) ->
@spellList.rerenderEntries() @spellList.rerenderEntries()
@ -209,6 +212,7 @@ module.exports = class TomeView extends CocoView
spellFor: (thang, spellName) -> spellFor: (thang, spellName) ->
return null unless thang?.isProgrammable 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]) selectedThangSpells = (@spells[spellKey] for spellKey in @thangSpells[thang.id])
if spellName if spellName
spell = _.find selectedThangSpells, {name: spellName} spell = _.find selectedThangSpells, {name: spellName}