From 797f844de30df394068026bed2e7c09b7508d2a9 Mon Sep 17 00:00:00 2001 From: Tim Mickel Date: Thu, 8 Sep 2016 09:40:27 -0400 Subject: [PATCH] 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 --- src/blocks/scratch3_event.js | 3 +- src/blocks/scratch3_looks.js | 125 ++++++++++++++++++++++++++++++++++- src/engine/runtime.js | 13 ++++ src/import/sb2import.js | 9 ++- src/sprites/clone.js | 40 ++++++++++- 5 files changed, 183 insertions(+), 7 deletions(-) diff --git a/src/blocks/scratch3_event.js b/src/blocks/scratch3_event.js index 900fd1f2a..330ab1d0e 100644 --- a/src/blocks/scratch3_event.js +++ b/src/blocks/scratch3_event.js @@ -29,10 +29,9 @@ Scratch3EventBlocks.prototype.getHats = function () { 'event_whenthisspriteclicked': { restartExistingThreads: true }, - /* 'event_whenbackdropswitchesto': { restartExistingThreads: true - },*/ + }, 'event_whengreaterthan': { restartExistingThreads: false, edgeActivated: true diff --git a/src/blocks/scratch3_looks.js b/src/blocks/scratch3_looks.js index 33e1ef3bf..a2060e6c1 100644 --- a/src/blocks/scratch3_looks.js +++ b/src/blocks/scratch3_looks.js @@ -1,3 +1,5 @@ +var Cast = require('../util/cast'); + function Scratch3LooksBlocks(runtime) { /** * The runtime instantiating this block package. @@ -18,13 +20,23 @@ Scratch3LooksBlocks.prototype.getPrimitives = function() { 'looks_thinkforsecs': this.sayforsecs, 'looks_show': this.show, '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_changeeffectby': this.changeEffect, 'looks_seteffectto': this.setEffect, 'looks_cleargraphiceffects': this.clearEffects, 'looks_changesizeby': this.changeSize, '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); }; +/** + * 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.} 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) { return args.EFFECT.toLowerCase(); }; @@ -95,4 +204,18 @@ Scratch3LooksBlocks.prototype.getSize = function (args, util) { 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; diff --git a/src/engine/runtime.js b/src/engine/runtime.js index db157783b..143f30aa7 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -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. */ diff --git a/src/import/sb2import.js b/src/import/sb2import.js index 07eb831b9..391ed06e3 100644 --- a/src/import/sb2import.js +++ b/src/import/sb2import.js @@ -20,7 +20,8 @@ var specMap = require('./sb2specmap'); function sb2import (json, runtime) { parseScratchObject( 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. * @param {!Object} object From-JSON "Scratch object:" sprite, stage, watcher. * @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')) { // Watcher/monitor - skip this object until those are implemented in VM. // @todo @@ -84,10 +86,11 @@ function parseScratchObject (object, runtime) { if (object.currentCostumeIndex) { target.currentCostume = object.currentCostumeIndex; } + target.isStage = topLevel; // The stage will have child objects; recursively process them. if (object.children) { for (var j = 0; j < object.children.length; j++) { - parseScratchObject(object.children[j], runtime); + parseScratchObject(object.children[j], runtime, false); } } } diff --git a/src/sprites/clone.js b/src/sprites/clone.js index 408667ab8..9e6d23c8d 100644 --- a/src/sprites/clone.js +++ b/src/sprites/clone.js @@ -50,6 +50,12 @@ Clone.prototype.initDrawable = function () { }; // 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. * @type {Number} @@ -107,6 +113,9 @@ Clone.prototype.effects = { * @param {!number} y New Y coordinate of clone, in Scratch coordinates. */ Clone.prototype.setXY = function (x, y) { + if (this.isStage) { + return; + } this.x = x; this.y = y; if (this.renderer) { @@ -121,6 +130,9 @@ Clone.prototype.setXY = function (x, y) { * @param {!number} direction New direction of clone. */ Clone.prototype.setDirection = function (direction) { + if (this.isStage) { + return; + } // Keep direction between -179 and +180. this.direction = MathUtil.wrapClamp(direction, -179, 180); if (this.renderer) { @@ -136,6 +148,9 @@ Clone.prototype.setDirection = function (direction) { * @param {?string} message Message to put in say bubble. */ Clone.prototype.setSay = function (type, message) { + if (this.isStage) { + return; + } // @todo: Render to stage. if (!type || !message) { 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. */ Clone.prototype.setVisible = function (visible) { + if (this.isStage) { + return; + } this.visible = visible; if (this.renderer) { this.renderer.updateDrawableProperties(this.drawableID, { @@ -162,6 +180,9 @@ Clone.prototype.setVisible = function (visible) { * @param {!number} size Size of clone, from 5 to 535. */ Clone.prototype.setSize = function (size) { + if (this.isStage) { + return; + } // Keep size between 5% and 535%. this.size = MathUtil.clamp(size, 5, 535); if (this.renderer) { @@ -202,7 +223,10 @@ Clone.prototype.clearEffects = function () { * @param {number} index New index of costume. */ 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) { this.renderer.updateDrawableProperties(this.drawableID, { 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. * Use when a batch has changed, e.g., when the drawable is first created.