* Provide property to Clone to distinguish "original" clones

* Provide method to clone a clone's properties

* Don't report clones in the UI target list

* Add target info to Thread

* Allow hats to skip clones (for green flag)

* Green flag skips clones

* Implement "create clone" and hat

* Pass the runtime to sprites and clones (for start hats)

* Clone disposal; trigger hats after drawable initializes.

* Separate stop threads for target; fix handling of stop button

* Remove extraneous `skipClones` property

* Add global clone limit

* Don't allow a non-clone to delete itself.

* Rename `cloneClone` -> `makeClone`

* Variable updates in runtime.js

* Synchronous drawable initialization (until we put it back to promises)
This commit is contained in:
Tim Mickel 2016-09-15 19:37:12 -04:00 committed by GitHub
parent 542899949e
commit 9744bcbb70
10 changed files with 229 additions and 37 deletions

View file

@ -21,7 +21,18 @@ Scratch3ControlBlocks.prototype.getPrimitives = function() {
'control_wait': this.wait,
'control_if': this.if,
'control_if_else': this.ifElse,
'control_stop': this.stop
'control_stop': this.stop,
'control_create_clone_of_menu': this.createCloneMenu,
'control_create_clone_of': this.createClone,
'control_delete_this_clone': this.deleteClone
};
};
Scratch3ControlBlocks.prototype.getHats = function () {
return {
'control_start_as_clone': {
restartExistingThreads: false
}
};
};
@ -118,4 +129,30 @@ Scratch3ControlBlocks.prototype.stop = function() {
this.runtime.stopAll();
};
// @todo (GH-146): remove.
Scratch3ControlBlocks.prototype.createCloneMenu = function (args) {
return args.CLONE_OPTION;
};
Scratch3ControlBlocks.prototype.createClone = function (args, util) {
var cloneTarget;
if (args.CLONE_OPTION == '_myself_') {
cloneTarget = util.target;
} else {
cloneTarget = this.runtime.getSpriteTargetByName(args.CLONE_OPTION);
}
if (!cloneTarget) {
return;
}
var newClone = cloneTarget.makeClone();
if (newClone) {
this.runtime.targets.push(newClone);
}
};
Scratch3ControlBlocks.prototype.deleteClone = function (args, util) {
this.runtime.disposeTarget(util.target);
this.runtime.stopForTarget(util.target);
};
module.exports = Scratch3ControlBlocks;

View file

@ -16,7 +16,7 @@ var isPromise = function (value) {
*/
var execute = function (sequencer, thread) {
var runtime = sequencer.runtime;
var target = runtime.targetForThread(thread);
var target = thread.target;
// Current block to execute is the one on the top of the stack.
var currentBlockId = thread.peekStack();

View file

@ -60,6 +60,11 @@ function Runtime () {
this._scriptGlowsPreviousFrame = [];
this._editingTarget = null;
/**
* Currently known number of clones.
* @type {number}
*/
this._cloneCounter = 0;
}
/**
@ -102,6 +107,11 @@ util.inherits(Runtime, EventEmitter);
*/
Runtime.THREAD_STEP_INTERVAL = 1000 / 60;
/**
* How many clones can be created at a time.
* @const {number}
*/
Runtime.MAX_CLONES = 300;
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
@ -194,11 +204,13 @@ Runtime.prototype.clearEdgeActivatedValues = function () {
/**
* Create a thread and push it to the list of threads.
* @param {!string} id ID of block that starts the stack
* @param {!string} id ID of block that starts the stack.
* @param {!Target} target Target to run thread on.
* @return {!Thread} The newly created thread.
*/
Runtime.prototype._pushThread = function (id) {
Runtime.prototype._pushThread = function (id, target) {
var thread = new Thread(id);
thread.setTarget(target);
thread.pushStack(id);
this.threads.push(thread);
return thread;
@ -237,7 +249,7 @@ Runtime.prototype.toggleScript = function (topBlockId) {
}
}
// Otherwise add it.
this._pushThread(topBlockId);
this._pushThread(topBlockId, this._editingTarget);
};
/**
@ -306,7 +318,8 @@ Runtime.prototype.startHats = function (requestedHatOpcode,
// If `restartExistingThreads` is true, we should stop
// any existing threads starting with the top block.
for (var i = 0; i < instance.threads.length; i++) {
if (instance.threads[i].topBlock === topBlockId) {
if (instance.threads[i].topBlock === topBlockId &&
(!opt_target || instance.threads[i].target == opt_target)) {
instance._removeThread(instance.threads[i]);
}
}
@ -314,31 +327,72 @@ Runtime.prototype.startHats = function (requestedHatOpcode,
// If `restartExistingThreads` is false, we should
// give up if any threads with the top block are running.
for (var j = 0; j < instance.threads.length; j++) {
if (instance.threads[j].topBlock === topBlockId) {
if (instance.threads[j].topBlock === topBlockId &&
(!opt_target || instance.threads[j].target == opt_target)) {
// Some thread is already running.
return;
}
}
}
// Start the thread with this top block.
newThreads.push(instance._pushThread(topBlockId));
newThreads.push(instance._pushThread(topBlockId, target));
}, opt_target);
return newThreads;
};
/**
* Dispose of a target.
* @param {!Target} target Target to dispose of.
*/
Runtime.prototype.disposeTarget = function (target) {
// Allow target to do dispose actions.
target.dispose();
// Remove from list of targets.
var index = this.targets.indexOf(target);
if (index > -1) {
this.targets.splice(index, 1);
}
};
/**
* Stop any threads acting on the target.
* @param {!Target} target Target to stop threads for.
*/
Runtime.prototype.stopForTarget = function (target) {
// Stop any threads on the target.
for (var i = 0; i < this.threads.length; i++) {
if (this.threads[i].target == target) {
this._removeThread(this.threads[i]);
}
}
};
/**
* Start all threads that start with the green flag.
*/
Runtime.prototype.greenFlag = function () {
this.stopAll();
this.ioDevices.clock.resetProjectTimer();
this.clearEdgeActivatedValues();
this.startHats('event_whenflagclicked');
};
/**
* Stop "everything"
* Stop "everything."
*/
Runtime.prototype.stopAll = function () {
// Dispose all clones.
var newTargets = [];
for (var i = 0; i < this.targets.length; i++) {
if (this.targets[i].hasOwnProperty('isOriginal') &&
!this.targets[i].isOriginal) {
this.targets[i].dispose();
} else {
newTargets.push(this.targets[i]);
}
}
this.targets = newTargets;
// Dispose all threads.
var threadsCopy = this.threads.slice();
while (threadsCopy.length > 0) {
var poppedThread = threadsCopy.pop();
@ -379,7 +433,7 @@ Runtime.prototype._updateScriptGlows = function () {
// Find all scripts that should be glowing.
for (var i = 0; i < this.threads.length; i++) {
var thread = this.threads[i];
var target = this.targetForThread(thread);
var target = thread.target;
if (thread.requestScriptGlowInFrame && target == this._editingTarget) {
var blockForThread = thread.peekStack() || thread.topBlock;
var script = target.blocks.getTopLevelScript(blockForThread);
@ -458,23 +512,6 @@ Runtime.prototype.visualReport = function (blockId, value) {
this.emit(Runtime.VISUAL_REPORT, blockId, String(value));
};
/**
* Return the Target for a particular thread.
* @param {!Thread} thread Thread to determine target for.
* @return {?Target} Target object, if one exists.
*/
Runtime.prototype.targetForThread = function (thread) {
// @todo This is a messy solution,
// but prevents having circular data references.
// Have a map or some other way to associate target with threads.
for (var t = 0; t < this.targets.length; t++) {
var target = this.targets[t];
if (target.blocks.getBlock(thread.topBlock)) {
return target;
}
}
};
/**
* Get a target by its id.
* @param {string} targetId Id of target to find.
@ -489,6 +526,36 @@ Runtime.prototype.getTargetById = function (targetId) {
}
};
/**
* Get the first original (non-clone-block-created) sprite given a name.
* @param {string} spriteName Name of sprite to look for.
* @return {?Target} Target representing a sprite of the given name.
*/
Runtime.prototype.getSpriteTargetByName = function (spriteName) {
for (var i = 0; i < this.targets.length; i++) {
var target = this.targets[i];
if (target.sprite && target.sprite.name == spriteName) {
return target;
}
}
};
/**
* Update the clone counter to track how many clones are created.
* @param {number} changeAmount How many clones have been created/destroyed.
*/
Runtime.prototype.changeCloneCounter = function (changeAmount) {
this._cloneCounter += changeAmount;
};
/**
* Return whether there are clones available.
* @return {boolean} True until the number of clones hits Runtime.MAX_CLONES.
*/
Runtime.prototype.clonesAvailable = function () {
return this._cloneCounter < Runtime.MAX_CLONES;
};
/**
* Get a target representing the Scratch stage, if one exists.
* @return {?Target} The target, if found.

View file

@ -111,7 +111,7 @@ Sequencer.prototype.stepToBranch = function (thread, branchNum) {
branchNum = 1;
}
var currentBlockId = thread.peekStack();
var branchId = this.runtime.targetForThread(thread).blocks.getBranch(
var branchId = thread.target.blocks.getBranch(
currentBlockId,
branchNum
);
@ -155,8 +155,7 @@ Sequencer.prototype.proceedThread = function (thread) {
// Pop from the stack - finished this level of execution.
thread.popStack();
// Push next connected block, if there is one.
var nextBlockId = (this.runtime.targetForThread(thread).
blocks.getNextBlock(currentBlockId));
var nextBlockId = thread.target.blocks.getNextBlock(currentBlockId);
if (nextBlockId) {
thread.pushStack(nextBlockId);
}

View file

@ -37,4 +37,12 @@ Target.prototype.getName = function () {
return this.id;
};
/**
* Call to destroy a target.
* @abstract
*/
Target.prototype.dispose = function () {
};
module.exports = Target;

View file

@ -29,6 +29,12 @@ function Thread (firstBlock) {
*/
this.status = 0; /* Thread.STATUS_RUNNING */
/**
* Target of this thread.
* @type {?Target}
*/
this.target = null;
/**
* Whether the thread requests its script to glow during this frame.
* @type {boolean}
@ -145,4 +151,20 @@ Thread.prototype.setStatus = function (status) {
this.status = status;
};
/**
* Set thread target.
* @param {?Target} target Target for this thread.
*/
Thread.prototype.setTarget = function (target) {
this.target = target;
};
/**
* Get thread target.
* @return {?Target} Target for this thread, if available.
*/
Thread.prototype.getTarget = function () {
return this.target;
};
module.exports = Thread;

View file

@ -40,7 +40,7 @@ function parseScratchObject (object, runtime, topLevel) {
// Blocks container for this object.
var blocks = new Blocks();
// @todo: For now, load all Scratch objects (stage/sprites) as a Sprite.
var sprite = new Sprite(blocks);
var sprite = new Sprite(blocks, runtime);
// Sprite/stage name from JSON.
if (object.hasOwnProperty('objName')) {
sprite.name = object.objName;

View file

@ -207,7 +207,10 @@ VirtualMachine.prototype.setEditingTarget = function (targetId) {
VirtualMachine.prototype.emitTargetsUpdate = function () {
this.emit('targetsUpdate', {
// [[target id, human readable target name], ...].
targetList: this.runtime.targets.map(function(target) {
targetList: this.runtime.targets.filter(function (target) {
// Don't report clones.
return !target.hasOwnProperty('isOriginal') || target.isOriginal;
}).map(function(target) {
return [target.id, target.getName()];
}),
// Currently editing target id.

View file

@ -5,10 +5,12 @@ var Target = require('../engine/target');
/**
* Clone (instance) of a sprite.
* @param {!Sprite} sprite Reference to the sprite.
* @param {Runtime} runtime Reference to the runtime.
* @constructor
*/
function Clone(sprite) {
function Clone(sprite, runtime) {
Target.call(this, sprite.blocks);
this.runtime = runtime;
/**
* Reference to the sprite that this is a clone of.
* @type {!Sprite}
@ -29,8 +31,6 @@ function Clone(sprite) {
* @type {?Number}
*/
this.drawableID = null;
this.initDrawable();
}
util.inherits(Clone, Target);
@ -42,9 +42,22 @@ Clone.prototype.initDrawable = function () {
this.drawableID = this.renderer.createDrawable();
this.updateAllDrawableProperties();
}
// If we're a clone, start the hats.
if (!this.isOriginal) {
this.runtime.startHats(
'control_start_as_clone', null, this
);
}
};
// Clone-level properties.
/**
* Whether this represents an "original" clone, i.e., created by the editor
* and not clone blocks. In interface terms, this true for a "sprite."
* @type {boolean}
*/
Clone.prototype.isOriginal = true;
/**
* Whether this clone represents the Scratch stage.
* @type {boolean}
@ -298,4 +311,40 @@ Clone.prototype.colorIsTouchingColor = function (targetRgb, maskRgb) {
return false;
};
/**
* Make a clone of this clone, copying any run-time properties.
* If we've hit the global clone limit, returns null.
* @return {!Clone} New clone object.
*/
Clone.prototype.makeClone = function () {
if (!this.runtime.clonesAvailable()) {
return; // Hit max clone limit.
}
this.runtime.changeCloneCounter(1);
var newClone = this.sprite.createClone();
newClone.x = this.x;
newClone.y = this.y;
newClone.direction = this.direction;
newClone.visible = this.visible;
newClone.size = this.size;
newClone.currentCostume = this.currentCostume;
newClone.effects = JSON.parse(JSON.stringify(this.effects));
newClone.initDrawable();
newClone.updateAllDrawableProperties();
return newClone;
};
/**
* Dispose of this clone, destroying any run-time properties.
*/
Clone.prototype.dispose = function () {
if (this.isOriginal) { // Don't allow a non-clone to delete itself.
return;
}
this.runtime.changeCloneCounter(-1);
if (this.renderer && this.drawableID !== null) {
this.renderer.destroyDrawable(this.drawableID);
}
};
module.exports = Clone;

View file

@ -5,9 +5,11 @@ var Blocks = require('../engine/blocks');
* Sprite to be used on the Scratch stage.
* All clones of a sprite have shared blocks, shared costumes, shared variables.
* @param {?Blocks} blocks Shared blocks object for all clones of sprite.
* @param {Runtime} runtime Reference to the runtime.
* @constructor
*/
function Sprite (blocks) {
function Sprite (blocks, runtime) {
this.runtime = runtime;
if (!blocks) {
// Shared set of blocks for all clones.
blocks = new Blocks();
@ -43,8 +45,13 @@ function Sprite (blocks) {
* @returns {!Clone} Newly created clone.
*/
Sprite.prototype.createClone = function () {
var newClone = new Clone(this);
var newClone = new Clone(this, this.runtime);
newClone.isOriginal = this.clones.length == 0;
this.clones.push(newClone);
if (newClone.isOriginal) {
newClone.initDrawable();
newClone.updateAllDrawableProperties();
}
return newClone;
};