Stage, costumes, backdrops (#149)

* Add `Clone.prototype.getCostumeIndexByName`, keep in range

* Add basic costume primitives from Scratch 2.0

* Add costume getter block

* Add properties and methods for distinguishing stage and sprites-vs-clones

* Add backdrop-related looks blocks

* Fix up "switch to backdrop" to be working

* Costume/backdrop reporters are 1-indexed

* Fire "when backdrop switched" hats

* Cut cloning helpers for a separate PR

* Disable many blocks on the stage

* Refactor into _setCostumeOrBackdrop; implement switch backdrop and wait

* Fire hats even when backdrop unchanged
This commit is contained in:
Tim Mickel 2016-09-08 09:40:27 -04:00 committed by GitHub
parent 14feb64005
commit 797f844de3
5 changed files with 183 additions and 7 deletions

View file

@ -29,10 +29,9 @@ Scratch3EventBlocks.prototype.getHats = function () {
'event_whenthisspriteclicked': { 'event_whenthisspriteclicked': {
restartExistingThreads: true restartExistingThreads: true
}, },
/*
'event_whenbackdropswitchesto': { 'event_whenbackdropswitchesto': {
restartExistingThreads: true restartExistingThreads: true
},*/ },
'event_whengreaterthan': { 'event_whengreaterthan': {
restartExistingThreads: false, restartExistingThreads: false,
edgeActivated: true edgeActivated: true

View file

@ -1,3 +1,5 @@
var Cast = require('../util/cast');
function Scratch3LooksBlocks(runtime) { function Scratch3LooksBlocks(runtime) {
/** /**
* The runtime instantiating this block package. * The runtime instantiating this block package.
@ -18,13 +20,23 @@ Scratch3LooksBlocks.prototype.getPrimitives = function() {
'looks_thinkforsecs': this.sayforsecs, 'looks_thinkforsecs': this.sayforsecs,
'looks_show': this.show, 'looks_show': this.show,
'looks_hide': this.hide, 'looks_hide': this.hide,
'looks_backdrops': this.backdropMenu,
'looks_costume': this.costumeMenu,
'looks_switchcostumeto': this.switchCostume,
'looks_switchbackdropto': this.switchBackdrop,
'looks_switchbackdroptoandwait': this.switchBackdropAndWait,
'looks_nextcostume': this.nextCostume,
'looks_nextbackdrop': this.nextBackdrop,
'looks_effectmenu': this.effectMenu, 'looks_effectmenu': this.effectMenu,
'looks_changeeffectby': this.changeEffect, 'looks_changeeffectby': this.changeEffect,
'looks_seteffectto': this.setEffect, 'looks_seteffectto': this.setEffect,
'looks_cleargraphiceffects': this.clearEffects, 'looks_cleargraphiceffects': this.clearEffects,
'looks_changesizeby': this.changeSize, 'looks_changesizeby': this.changeSize,
'looks_setsizeto': this.setSize, 'looks_setsizeto': this.setSize,
'looks_size': this.getSize 'looks_size': this.getSize,
'looks_costumeorder': this.getCostumeIndex,
'looks_backdroporder': this.getBackdropIndex,
'looks_backdropname': this.getBackdropName
}; };
}; };
@ -66,6 +78,103 @@ Scratch3LooksBlocks.prototype.hide = function (args, util) {
util.target.setVisible(false); util.target.setVisible(false);
}; };
/**
* Utility function to set the costume or backdrop of a target.
* Matches the behavior of Scratch 2.0 for different types of arguments.
* @param {!Target} target Target to set costume/backdrop to.
* @param {Any} requestedCostume Costume requested, e.g., 0, 'name', etc.
* @param {boolean=} opt_zeroIndex Set to zero-index the requestedCostume.
* @return {Array.<!Thread>} Any threads started by this switch.
*/
Scratch3LooksBlocks.prototype._setCostumeOrBackdrop = function (target,
requestedCostume, opt_zeroIndex) {
if (typeof requestedCostume === 'number') {
target.setCostume(opt_zeroIndex ?
requestedCostume : requestedCostume - 1);
} else {
var costumeIndex = target.getCostumeIndexByName(requestedCostume);
if (costumeIndex > -1) {
target.setCostume(costumeIndex);
} else if (costumeIndex == 'previous costume' ||
costumeIndex == 'previous backdrop') {
target.setCostume(target.currentCostume - 1);
} else if (costumeIndex == 'next costume' ||
costumeIndex == 'next backdrop') {
target.setCostume(target.currentCostume + 1);
} else {
var forcedNumber = Cast.toNumber(requestedCostume);
if (!isNaN(forcedNumber)) {
target.setCostume(opt_zeroIndex ?
forcedNumber : forcedNumber - 1);
}
}
}
if (target == this.runtime.getTargetForStage()) {
// Target is the stage - start hats.
var newName = target.sprite.costumes[target.currentCostume].name;
return this.runtime.startHats('event_whenbackdropswitchesto', {
'BACKDROP': newName
});
}
return [];
};
// @todo(GH-146): Remove.
Scratch3LooksBlocks.prototype.costumeMenu = function (args) {
return args.COSTUME;
};
Scratch3LooksBlocks.prototype.switchCostume = function (args, util) {
this._setCostumeOrBackdrop(util.target, args.COSTUME);
};
Scratch3LooksBlocks.prototype.nextCostume = function (args, util) {
this._setCostumeOrBackdrop(
util.target, util.target.currentCostume + 1, true
);
};
// @todo(GH-146): Remove.
Scratch3LooksBlocks.prototype.backdropMenu = function (args) {
return args.BACKDROP;
};
Scratch3LooksBlocks.prototype.switchBackdrop = function (args) {
this._setCostumeOrBackdrop(this.runtime.getTargetForStage(), args.BACKDROP);
};
Scratch3LooksBlocks.prototype.switchBackdropAndWait = function (args, util) {
// Have we run before, starting threads?
if (!util.stackFrame.startedThreads) {
// No - switch the backdrop.
util.stackFrame.startedThreads = (
this._setCostumeOrBackdrop(
this.runtime.getTargetForStage(),
args.BACKDROP
)
);
if (util.stackFrame.startedThreads.length == 0) {
// Nothing was started.
return;
}
}
// We've run before; check if the wait is still going on.
var instance = this;
var waiting = util.stackFrame.startedThreads.some(function(thread) {
return instance.runtime.isActiveThread(thread);
});
if (waiting) {
util.yieldFrame();
}
};
Scratch3LooksBlocks.prototype.nextBackdrop = function () {
var stage = this.runtime.getTargetForStage();
this._setCostumeOrBackdrop(
stage, stage.currentCostume + 1, true
);
};
Scratch3LooksBlocks.prototype.effectMenu = function (args) { Scratch3LooksBlocks.prototype.effectMenu = function (args) {
return args.EFFECT.toLowerCase(); return args.EFFECT.toLowerCase();
}; };
@ -95,4 +204,18 @@ Scratch3LooksBlocks.prototype.getSize = function (args, util) {
return util.target.size; return util.target.size;
}; };
Scratch3LooksBlocks.prototype.getBackdropIndex = function () {
var stage = this.runtime.getTargetForStage();
return stage.currentCostume + 1;
};
Scratch3LooksBlocks.prototype.getBackdropName = function () {
var stage = this.runtime.getTargetForStage();
return stage.sprite.costumes[stage.currentCostume].name;
};
Scratch3LooksBlocks.prototype.getCostumeIndex = function (args, util) {
return util.target.currentCostume + 1;
};
module.exports = Scratch3LooksBlocks; module.exports = Scratch3LooksBlocks;

View file

@ -434,6 +434,19 @@ Runtime.prototype.getTargetById = function (targetId) {
} }
}; };
/**
* Get a target representing the Scratch stage, if one exists.
* @return {?Target} The target, if found.
*/
Runtime.prototype.getTargetForStage = function () {
for (var i = 0; i < this.targets.length; i++) {
var target = this.targets[i];
if (target.isStage) {
return target;
}
}
};
/** /**
* Handle an animation frame from the main thread. * Handle an animation frame from the main thread.
*/ */

View file

@ -20,7 +20,8 @@ var specMap = require('./sb2specmap');
function sb2import (json, runtime) { function sb2import (json, runtime) {
parseScratchObject( parseScratchObject(
JSON.parse(json), JSON.parse(json),
runtime runtime,
true
); );
} }
@ -28,8 +29,9 @@ function sb2import (json, runtime) {
* Parse a single "Scratch object" and create all its in-memory VM objects. * Parse a single "Scratch object" and create all its in-memory VM objects.
* @param {!Object} object From-JSON "Scratch object:" sprite, stage, watcher. * @param {!Object} object From-JSON "Scratch object:" sprite, stage, watcher.
* @param {!Runtime} runtime Runtime object to load all structures into. * @param {!Runtime} runtime Runtime object to load all structures into.
* @param {boolean} topLevel Whether this is the top-level object (stage).
*/ */
function parseScratchObject (object, runtime) { function parseScratchObject (object, runtime, topLevel) {
if (!object.hasOwnProperty('objName')) { if (!object.hasOwnProperty('objName')) {
// Watcher/monitor - skip this object until those are implemented in VM. // Watcher/monitor - skip this object until those are implemented in VM.
// @todo // @todo
@ -84,10 +86,11 @@ function parseScratchObject (object, runtime) {
if (object.currentCostumeIndex) { if (object.currentCostumeIndex) {
target.currentCostume = object.currentCostumeIndex; target.currentCostume = object.currentCostumeIndex;
} }
target.isStage = topLevel;
// The stage will have child objects; recursively process them. // The stage will have child objects; recursively process them.
if (object.children) { if (object.children) {
for (var j = 0; j < object.children.length; j++) { for (var j = 0; j < object.children.length; j++) {
parseScratchObject(object.children[j], runtime); parseScratchObject(object.children[j], runtime, false);
} }
} }
} }

View file

@ -50,6 +50,12 @@ Clone.prototype.initDrawable = function () {
}; };
// Clone-level properties. // Clone-level properties.
/**
* Whether this clone represents the Scratch stage.
* @type {boolean}
*/
Clone.prototype.isStage = false;
/** /**
* Scratch X coordinate. Currently should range from -240 to 240. * Scratch X coordinate. Currently should range from -240 to 240.
* @type {Number} * @type {Number}
@ -107,6 +113,9 @@ Clone.prototype.effects = {
* @param {!number} y New Y coordinate of clone, in Scratch coordinates. * @param {!number} y New Y coordinate of clone, in Scratch coordinates.
*/ */
Clone.prototype.setXY = function (x, y) { Clone.prototype.setXY = function (x, y) {
if (this.isStage) {
return;
}
this.x = x; this.x = x;
this.y = y; this.y = y;
if (this.renderer) { if (this.renderer) {
@ -121,6 +130,9 @@ Clone.prototype.setXY = function (x, y) {
* @param {!number} direction New direction of clone. * @param {!number} direction New direction of clone.
*/ */
Clone.prototype.setDirection = function (direction) { Clone.prototype.setDirection = function (direction) {
if (this.isStage) {
return;
}
// Keep direction between -179 and +180. // Keep direction between -179 and +180.
this.direction = MathUtil.wrapClamp(direction, -179, 180); this.direction = MathUtil.wrapClamp(direction, -179, 180);
if (this.renderer) { if (this.renderer) {
@ -136,6 +148,9 @@ Clone.prototype.setDirection = function (direction) {
* @param {?string} message Message to put in say bubble. * @param {?string} message Message to put in say bubble.
*/ */
Clone.prototype.setSay = function (type, message) { Clone.prototype.setSay = function (type, message) {
if (this.isStage) {
return;
}
// @todo: Render to stage. // @todo: Render to stage.
if (!type || !message) { if (!type || !message) {
console.log('Clearing say bubble'); console.log('Clearing say bubble');
@ -149,6 +164,9 @@ Clone.prototype.setSay = function (type, message) {
* @param {!boolean} visible True if the sprite should be shown. * @param {!boolean} visible True if the sprite should be shown.
*/ */
Clone.prototype.setVisible = function (visible) { Clone.prototype.setVisible = function (visible) {
if (this.isStage) {
return;
}
this.visible = visible; this.visible = visible;
if (this.renderer) { if (this.renderer) {
this.renderer.updateDrawableProperties(this.drawableID, { this.renderer.updateDrawableProperties(this.drawableID, {
@ -162,6 +180,9 @@ Clone.prototype.setVisible = function (visible) {
* @param {!number} size Size of clone, from 5 to 535. * @param {!number} size Size of clone, from 5 to 535.
*/ */
Clone.prototype.setSize = function (size) { Clone.prototype.setSize = function (size) {
if (this.isStage) {
return;
}
// Keep size between 5% and 535%. // Keep size between 5% and 535%.
this.size = MathUtil.clamp(size, 5, 535); this.size = MathUtil.clamp(size, 5, 535);
if (this.renderer) { if (this.renderer) {
@ -202,7 +223,10 @@ Clone.prototype.clearEffects = function () {
* @param {number} index New index of costume. * @param {number} index New index of costume.
*/ */
Clone.prototype.setCostume = function (index) { Clone.prototype.setCostume = function (index) {
this.currentCostume = index; // Keep the costume index within possible values.
this.currentCostume = MathUtil.wrapClamp(
index, 0, this.sprite.costumes.length - 1
);
if (this.renderer) { if (this.renderer) {
this.renderer.updateDrawableProperties(this.drawableID, { this.renderer.updateDrawableProperties(this.drawableID, {
skin: this.sprite.costumes[this.currentCostume].skin skin: this.sprite.costumes[this.currentCostume].skin
@ -210,6 +234,20 @@ Clone.prototype.setCostume = function (index) {
} }
}; };
/**
* Get a costume index of this clone, by name of the costume.
* @param {?string} costumeName Name of a costume.
* @return {number} Index of the named costume, or -1 if not present.
*/
Clone.prototype.getCostumeIndexByName = function (costumeName) {
for (var i = 0; i < this.sprite.costumes.length; i++) {
if (this.sprite.costumes[i].name == costumeName) {
return i;
}
}
return -1;
};
/** /**
* Update all drawable properties for this clone. * Update all drawable properties for this clone.
* Use when a batch has changed, e.g., when the drawable is first created. * Use when a batch has changed, e.g., when the drawable is first created.