Initial commit of 001code-html Scratch frontend project.
Includes scratch-gui, scratch-vm, scratch-blocks, scratch-render, scratch-l10n, and deployment config. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
206
scratch-vm/src/blocks/scratch3_control.js
Normal file
206
scratch-vm/src/blocks/scratch3_control.js
Normal file
@@ -0,0 +1,206 @@
|
||||
const Cast = require('../util/cast');
|
||||
|
||||
class Scratch3ControlBlocks {
|
||||
constructor (runtime) {
|
||||
/**
|
||||
* The runtime instantiating this block package.
|
||||
* @type {Runtime}
|
||||
*/
|
||||
this.runtime = runtime;
|
||||
|
||||
/**
|
||||
* The "counter" block value. For compatibility with 2.0.
|
||||
* @type {number}
|
||||
*/
|
||||
this._counter = 0; // used by compiler
|
||||
|
||||
this.runtime.on('RUNTIME_DISPOSED', this.clearCounter.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the block primitives implemented by this package.
|
||||
* @return {object.<string, Function>} Mapping of opcode to Function.
|
||||
*/
|
||||
getPrimitives () {
|
||||
return {
|
||||
control_repeat: this.repeat,
|
||||
control_repeat_until: this.repeatUntil,
|
||||
control_while: this.repeatWhile,
|
||||
control_for_each: this.forEach,
|
||||
control_forever: this.forever,
|
||||
control_wait: this.wait,
|
||||
control_wait_until: this.waitUntil,
|
||||
control_if: this.if,
|
||||
control_if_else: this.ifElse,
|
||||
control_stop: this.stop,
|
||||
control_create_clone_of: this.createClone,
|
||||
control_delete_this_clone: this.deleteClone,
|
||||
control_get_counter: this.getCounter,
|
||||
control_incr_counter: this.incrCounter,
|
||||
control_clear_counter: this.clearCounter,
|
||||
control_all_at_once: this.allAtOnce
|
||||
};
|
||||
}
|
||||
|
||||
getHats () {
|
||||
return {
|
||||
control_start_as_clone: {
|
||||
restartExistingThreads: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
repeat (args, util) {
|
||||
const times = Math.round(Cast.toNumber(args.TIMES));
|
||||
// Initialize loop
|
||||
if (typeof util.stackFrame.loopCounter === 'undefined') {
|
||||
util.stackFrame.loopCounter = times;
|
||||
}
|
||||
// Only execute once per frame.
|
||||
// When the branch finishes, `repeat` will be executed again and
|
||||
// the second branch will be taken, yielding for the rest of the frame.
|
||||
// Decrease counter
|
||||
util.stackFrame.loopCounter--;
|
||||
// If we still have some left, start the branch.
|
||||
if (util.stackFrame.loopCounter >= 0) {
|
||||
util.startBranch(1, true);
|
||||
}
|
||||
}
|
||||
|
||||
repeatUntil (args, util) {
|
||||
const condition = Cast.toBoolean(args.CONDITION);
|
||||
// If the condition is false (repeat UNTIL), start the branch.
|
||||
if (!condition) {
|
||||
util.startBranch(1, true);
|
||||
}
|
||||
}
|
||||
|
||||
repeatWhile (args, util) {
|
||||
const condition = Cast.toBoolean(args.CONDITION);
|
||||
// If the condition is true (repeat WHILE), start the branch.
|
||||
if (condition) {
|
||||
util.startBranch(1, true);
|
||||
}
|
||||
}
|
||||
|
||||
forEach (args, util) {
|
||||
const variable = util.target.lookupOrCreateVariable(
|
||||
args.VARIABLE.id, args.VARIABLE.name);
|
||||
|
||||
if (typeof util.stackFrame.index === 'undefined') {
|
||||
util.stackFrame.index = 0;
|
||||
}
|
||||
|
||||
if (util.stackFrame.index < Number(args.VALUE)) {
|
||||
util.stackFrame.index++;
|
||||
variable.value = util.stackFrame.index;
|
||||
util.startBranch(1, true);
|
||||
}
|
||||
}
|
||||
|
||||
waitUntil (args, util) {
|
||||
const condition = Cast.toBoolean(args.CONDITION);
|
||||
if (!condition) {
|
||||
util.yield();
|
||||
}
|
||||
}
|
||||
|
||||
forever (args, util) {
|
||||
util.startBranch(1, true);
|
||||
}
|
||||
|
||||
wait (args, util) {
|
||||
if (util.stackTimerNeedsInit()) {
|
||||
const duration = Math.max(0, 1000 * Cast.toNumber(args.DURATION));
|
||||
|
||||
util.startStackTimer(duration);
|
||||
this.runtime.requestRedraw();
|
||||
util.yield();
|
||||
} else if (!util.stackTimerFinished()) {
|
||||
util.yield();
|
||||
}
|
||||
}
|
||||
|
||||
if (args, util) {
|
||||
const condition = Cast.toBoolean(args.CONDITION);
|
||||
if (condition) {
|
||||
util.startBranch(1, false);
|
||||
}
|
||||
}
|
||||
|
||||
ifElse (args, util) {
|
||||
const condition = Cast.toBoolean(args.CONDITION);
|
||||
if (condition) {
|
||||
util.startBranch(1, false);
|
||||
} else {
|
||||
util.startBranch(2, false);
|
||||
}
|
||||
}
|
||||
|
||||
stop (args, util) {
|
||||
const option = args.STOP_OPTION;
|
||||
if (option === 'all') {
|
||||
util.stopAll();
|
||||
} else if (option === 'other scripts in sprite' ||
|
||||
option === 'other scripts in stage') {
|
||||
util.stopOtherTargetThreads();
|
||||
} else if (option === 'this script') {
|
||||
util.stopThisScript();
|
||||
}
|
||||
}
|
||||
|
||||
createClone (args, util) {
|
||||
this._createClone(Cast.toString(args.CLONE_OPTION), util.target);
|
||||
}
|
||||
_createClone (cloneOption, target) { // used by compiler
|
||||
// Set clone target
|
||||
let cloneTarget;
|
||||
if (cloneOption === '_myself_') {
|
||||
cloneTarget = target;
|
||||
} else {
|
||||
cloneTarget = this.runtime.getSpriteTargetByName(cloneOption);
|
||||
}
|
||||
|
||||
// If clone target is not found, return
|
||||
if (!cloneTarget) return;
|
||||
|
||||
// Create clone
|
||||
const newClone = cloneTarget.makeClone();
|
||||
if (newClone) {
|
||||
this.runtime.addTarget(newClone);
|
||||
|
||||
// Place behind the original target.
|
||||
newClone.goBehindOther(cloneTarget);
|
||||
}
|
||||
}
|
||||
|
||||
deleteClone (args, util) {
|
||||
if (util.target.isOriginal) return;
|
||||
this.runtime.disposeTarget(util.target);
|
||||
this.runtime.stopForTarget(util.target);
|
||||
}
|
||||
|
||||
getCounter () {
|
||||
return this._counter;
|
||||
}
|
||||
|
||||
clearCounter () {
|
||||
this._counter = 0;
|
||||
}
|
||||
|
||||
incrCounter () {
|
||||
this._counter++;
|
||||
}
|
||||
|
||||
allAtOnce (args, util) {
|
||||
// Since the "all at once" block is implemented for compatiblity with
|
||||
// Scratch 2.0 projects, it behaves the same way it did in 2.0, which
|
||||
// is to simply run the contained script (like "if 1 = 1").
|
||||
// (In early versions of Scratch 2.0, it would work the same way as
|
||||
// "run without screen refresh" custom blocks do now, but this was
|
||||
// removed before the release of 2.0.)
|
||||
util.startBranch(1, false);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Scratch3ControlBlocks;
|
||||
69
scratch-vm/src/blocks/scratch3_core_example.js
Normal file
69
scratch-vm/src/blocks/scratch3_core_example.js
Normal file
@@ -0,0 +1,69 @@
|
||||
const BlockType = require('../extension-support/block-type');
|
||||
const ArgumentType = require('../extension-support/argument-type');
|
||||
|
||||
/* eslint-disable-next-line max-len */
|
||||
const blockIconURI = 'data:image/svg+xml,%3Csvg id="rotate-counter-clockwise" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:%233d79cc;%7D.cls-2%7Bfill:%23fff;%7D%3C/style%3E%3C/defs%3E%3Ctitle%3Erotate-counter-clockwise%3C/title%3E%3Cpath class="cls-1" d="M22.68,12.2a1.6,1.6,0,0,1-1.27.63H13.72a1.59,1.59,0,0,1-1.16-2.58l1.12-1.41a4.82,4.82,0,0,0-3.14-.77,4.31,4.31,0,0,0-2,.8,4.25,4.25,0,0,0-1.34,1.73,5.06,5.06,0,0,0,.54,4.62A5.58,5.58,0,0,0,12,17.74h0a2.26,2.26,0,0,1-.16,4.52A10.25,10.25,0,0,1,3.74,18,10.14,10.14,0,0,1,2.25,8.78,9.7,9.7,0,0,1,5.08,4.64,9.92,9.92,0,0,1,9.66,2.5a10.66,10.66,0,0,1,7.72,1.68l1.08-1.35a1.57,1.57,0,0,1,1.24-.6,1.6,1.6,0,0,1,1.54,1.21l1.7,7.37A1.57,1.57,0,0,1,22.68,12.2Z"/%3E%3Cpath class="cls-2" d="M21.38,11.83H13.77a.59.59,0,0,1-.43-1l1.75-2.19a5.9,5.9,0,0,0-4.7-1.58,5.07,5.07,0,0,0-4.11,3.17A6,6,0,0,0,7,15.77a6.51,6.51,0,0,0,5,2.92,1.31,1.31,0,0,1-.08,2.62,9.3,9.3,0,0,1-7.35-3.82A9.16,9.16,0,0,1,3.17,9.12,8.51,8.51,0,0,1,5.71,5.4,8.76,8.76,0,0,1,9.82,3.48a9.71,9.71,0,0,1,7.75,2.07l1.67-2.1a.59.59,0,0,1,1,.21L22,11.08A.59.59,0,0,1,21.38,11.83Z"/%3E%3C/svg%3E';
|
||||
|
||||
/**
|
||||
* An example core block implemented using the extension spec.
|
||||
* This is not loaded as part of the core blocks in the VM but it is provided
|
||||
* and used as part of tests.
|
||||
*/
|
||||
class Scratch3CoreExample {
|
||||
constructor (runtime) {
|
||||
/**
|
||||
* The runtime instantiating this block package.
|
||||
* @type {Runtime}
|
||||
*/
|
||||
this.runtime = runtime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {object} metadata for this extension and its blocks.
|
||||
*/
|
||||
getInfo () {
|
||||
return {
|
||||
id: 'coreExample',
|
||||
name: 'CoreEx', // This string does not need to be translated as this extension is only used as an example.
|
||||
blocks: [
|
||||
{
|
||||
func: 'MAKE_A_VARIABLE',
|
||||
blockType: BlockType.BUTTON,
|
||||
text: 'make a variable (CoreEx)'
|
||||
},
|
||||
{
|
||||
opcode: 'exampleOpcode',
|
||||
blockType: BlockType.REPORTER,
|
||||
text: 'example block'
|
||||
},
|
||||
{
|
||||
opcode: 'exampleWithInlineImage',
|
||||
blockType: BlockType.COMMAND,
|
||||
text: 'block with image [CLOCKWISE] inline',
|
||||
arguments: {
|
||||
CLOCKWISE: {
|
||||
type: ArgumentType.IMAGE,
|
||||
dataURI: blockIconURI
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Example opcode just returns the name of the stage target.
|
||||
* @returns {string} The name of the first target in the project.
|
||||
*/
|
||||
exampleOpcode () {
|
||||
const stage = this.runtime.getTargetForStage();
|
||||
return stage ? stage.getName() : 'no stage yet';
|
||||
}
|
||||
|
||||
exampleWithInlineImage () {
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Scratch3CoreExample;
|
||||
240
scratch-vm/src/blocks/scratch3_data.js
Normal file
240
scratch-vm/src/blocks/scratch3_data.js
Normal file
@@ -0,0 +1,240 @@
|
||||
const Cast = require('../util/cast');
|
||||
|
||||
class Scratch3DataBlocks {
|
||||
constructor (runtime) {
|
||||
/**
|
||||
* The runtime instantiating this block package.
|
||||
* @type {Runtime}
|
||||
*/
|
||||
this.runtime = runtime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the block primitives implemented by this package.
|
||||
* @return {object.<string, Function>} Mapping of opcode to Function.
|
||||
*/
|
||||
getPrimitives () {
|
||||
return {
|
||||
data_variable: this.getVariable,
|
||||
data_setvariableto: this.setVariableTo,
|
||||
data_changevariableby: this.changeVariableBy,
|
||||
data_hidevariable: this.hideVariable,
|
||||
data_showvariable: this.showVariable,
|
||||
data_listcontents: this.getListContents,
|
||||
data_addtolist: this.addToList,
|
||||
data_deleteoflist: this.deleteOfList,
|
||||
data_deletealloflist: this.deleteAllOfList,
|
||||
data_insertatlist: this.insertAtList,
|
||||
data_replaceitemoflist: this.replaceItemOfList,
|
||||
data_itemoflist: this.getItemOfList,
|
||||
data_itemnumoflist: this.getItemNumOfList,
|
||||
data_lengthoflist: this.lengthOfList,
|
||||
data_listcontainsitem: this.listContainsItem,
|
||||
data_hidelist: this.hideList,
|
||||
data_showlist: this.showList
|
||||
};
|
||||
}
|
||||
|
||||
getVariable (args, util) {
|
||||
const variable = util.target.lookupOrCreateVariable(
|
||||
args.VARIABLE.id, args.VARIABLE.name);
|
||||
return variable.value;
|
||||
}
|
||||
|
||||
setVariableTo (args, util) {
|
||||
const variable = util.target.lookupOrCreateVariable(
|
||||
args.VARIABLE.id, args.VARIABLE.name);
|
||||
variable.value = args.VALUE;
|
||||
|
||||
if (variable.isCloud) {
|
||||
util.ioQuery('cloud', 'requestUpdateVariable', [variable.name, args.VALUE]);
|
||||
}
|
||||
}
|
||||
|
||||
changeVariableBy (args, util) {
|
||||
const variable = util.target.lookupOrCreateVariable(
|
||||
args.VARIABLE.id, args.VARIABLE.name);
|
||||
const castedValue = Cast.toNumber(variable.value);
|
||||
const dValue = Cast.toNumber(args.VALUE);
|
||||
const newValue = castedValue + dValue;
|
||||
variable.value = newValue;
|
||||
|
||||
if (variable.isCloud) {
|
||||
util.ioQuery('cloud', 'requestUpdateVariable', [variable.name, newValue]);
|
||||
}
|
||||
}
|
||||
|
||||
changeMonitorVisibility (id, visible) {
|
||||
// Send the monitor blocks an event like the flyout checkbox event.
|
||||
// This both updates the monitor state and changes the isMonitored block flag.
|
||||
this.runtime.monitorBlocks.changeBlock({
|
||||
id: id, // Monitor blocks for variables are the variable ID.
|
||||
element: 'checkbox', // Mimic checkbox event from flyout.
|
||||
value: visible
|
||||
}, this.runtime);
|
||||
}
|
||||
|
||||
showVariable (args) {
|
||||
this.changeMonitorVisibility(args.VARIABLE.id, true);
|
||||
}
|
||||
|
||||
hideVariable (args) {
|
||||
this.changeMonitorVisibility(args.VARIABLE.id, false);
|
||||
}
|
||||
|
||||
showList (args) {
|
||||
this.changeMonitorVisibility(args.LIST.id, true);
|
||||
}
|
||||
|
||||
hideList (args) {
|
||||
this.changeMonitorVisibility(args.LIST.id, false);
|
||||
}
|
||||
|
||||
getListContents (args, util) {
|
||||
const list = util.target.lookupOrCreateList(
|
||||
args.LIST.id, args.LIST.name);
|
||||
|
||||
// If block is running for monitors, return copy of list as an array if changed.
|
||||
if (util.thread.updateMonitor) {
|
||||
// Return original list value if up-to-date, which doesn't trigger monitor update.
|
||||
if (list._monitorUpToDate) return list.value;
|
||||
// If value changed, reset the flag and return a copy to trigger monitor update.
|
||||
// Because monitors use Immutable data structures, only new objects trigger updates.
|
||||
list._monitorUpToDate = true;
|
||||
return list.value.slice();
|
||||
}
|
||||
|
||||
// Determine if the list is all single letters.
|
||||
// If it is, report contents joined together with no separator.
|
||||
// If it's not, report contents joined together with a space.
|
||||
let allSingleLetters = true;
|
||||
for (let i = 0; i < list.value.length; i++) {
|
||||
const listItem = list.value[i];
|
||||
if (!((typeof listItem === 'string') &&
|
||||
(listItem.length === 1))) {
|
||||
allSingleLetters = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (allSingleLetters) {
|
||||
return list.value.join('');
|
||||
}
|
||||
return list.value.join(' ');
|
||||
|
||||
}
|
||||
|
||||
addToList (args, util) {
|
||||
const list = util.target.lookupOrCreateList(
|
||||
args.LIST.id, args.LIST.name);
|
||||
list.value.push(args.ITEM);
|
||||
list._monitorUpToDate = false;
|
||||
}
|
||||
|
||||
deleteOfList (args, util) {
|
||||
const list = util.target.lookupOrCreateList(
|
||||
args.LIST.id, args.LIST.name);
|
||||
const index = Cast.toListIndex(args.INDEX, list.value.length, true);
|
||||
if (index === Cast.LIST_INVALID) {
|
||||
return;
|
||||
} else if (index === Cast.LIST_ALL) {
|
||||
list.value = [];
|
||||
return;
|
||||
}
|
||||
list.value.splice(index - 1, 1);
|
||||
list._monitorUpToDate = false;
|
||||
}
|
||||
|
||||
deleteAllOfList (args, util) {
|
||||
const list = util.target.lookupOrCreateList(
|
||||
args.LIST.id, args.LIST.name);
|
||||
list.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
insertAtList (args, util) {
|
||||
const item = args.ITEM;
|
||||
const list = util.target.lookupOrCreateList(
|
||||
args.LIST.id, args.LIST.name);
|
||||
const index = Cast.toListIndex(args.INDEX, list.value.length + 1, false);
|
||||
if (index === Cast.LIST_INVALID) {
|
||||
return;
|
||||
}
|
||||
list.value.splice(index - 1, 0, item);
|
||||
list._monitorUpToDate = false;
|
||||
}
|
||||
|
||||
replaceItemOfList (args, util) {
|
||||
const item = args.ITEM;
|
||||
const list = util.target.lookupOrCreateList(
|
||||
args.LIST.id, args.LIST.name);
|
||||
const index = Cast.toListIndex(args.INDEX, list.value.length, false);
|
||||
if (index === Cast.LIST_INVALID) {
|
||||
return;
|
||||
}
|
||||
list.value[index - 1] = item;
|
||||
list._monitorUpToDate = false;
|
||||
}
|
||||
|
||||
getItemOfList (args, util) {
|
||||
const list = util.target.lookupOrCreateList(
|
||||
args.LIST.id, args.LIST.name);
|
||||
const index = Cast.toListIndex(args.INDEX, list.value.length, false);
|
||||
if (index === Cast.LIST_INVALID) {
|
||||
return '';
|
||||
}
|
||||
return list.value[index - 1];
|
||||
}
|
||||
|
||||
getItemNumOfList (args, util) {
|
||||
const item = args.ITEM;
|
||||
const list = util.target.lookupOrCreateList(
|
||||
args.LIST.id, args.LIST.name);
|
||||
|
||||
// Go through the list items one-by-one using Cast.compare. This is for
|
||||
// cases like checking if 123 is contained in a list [4, 7, '123'] --
|
||||
// Scratch considers 123 and '123' to be equal.
|
||||
for (let i = 0; i < list.value.length; i++) {
|
||||
if (Cast.compare(list.value[i], item) === 0) {
|
||||
return i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// We don't bother using .indexOf() at all, because it would end up with
|
||||
// edge cases such as the index of '123' in [4, 7, 123, '123', 9].
|
||||
// If we use indexOf(), this block would return 4 instead of 3, because
|
||||
// indexOf() sees the first occurence of the string 123 as the fourth
|
||||
// item in the list. With Scratch, this would be confusing -- after all,
|
||||
// '123' and 123 look the same, so one would expect the block to say
|
||||
// that the first occurrence of '123' (or 123) to be the third item.
|
||||
|
||||
// Default to 0 if there's no match. Since Scratch lists are 1-indexed,
|
||||
// we don't have to worry about this conflicting with the "this item is
|
||||
// the first value" number (in JS that is 0, but in Scratch it's 1).
|
||||
return 0;
|
||||
}
|
||||
|
||||
lengthOfList (args, util) {
|
||||
const list = util.target.lookupOrCreateList(
|
||||
args.LIST.id, args.LIST.name);
|
||||
return list.value.length;
|
||||
}
|
||||
|
||||
listContainsItem (args, util) {
|
||||
const item = args.ITEM;
|
||||
const list = util.target.lookupOrCreateList(
|
||||
args.LIST.id, args.LIST.name);
|
||||
if (list.value.indexOf(item) >= 0) {
|
||||
return true;
|
||||
}
|
||||
// Try using Scratch comparison operator on each item.
|
||||
// (Scratch considers the string '123' equal to the number 123).
|
||||
for (let i = 0; i < list.value.length; i++) {
|
||||
if (Cast.compare(list.value[i], item) === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Scratch3DataBlocks;
|
||||
137
scratch-vm/src/blocks/scratch3_event.js
Normal file
137
scratch-vm/src/blocks/scratch3_event.js
Normal file
@@ -0,0 +1,137 @@
|
||||
const Cast = require('../util/cast');
|
||||
|
||||
class Scratch3EventBlocks {
|
||||
constructor (runtime) {
|
||||
/**
|
||||
* The runtime instantiating this block package.
|
||||
* @type {Runtime}
|
||||
*/
|
||||
this.runtime = runtime;
|
||||
|
||||
this.runtime.on('KEY_PRESSED', key => {
|
||||
this.runtime.startHats('event_whenkeypressed', {
|
||||
KEY_OPTION: key
|
||||
});
|
||||
this.runtime.startHats('event_whenkeypressed', {
|
||||
KEY_OPTION: 'any'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the block primitives implemented by this package.
|
||||
* @return {object.<string, Function>} Mapping of opcode to Function.
|
||||
*/
|
||||
getPrimitives () {
|
||||
return {
|
||||
event_whentouchingobject: this.touchingObject,
|
||||
event_broadcast: this.broadcast,
|
||||
event_broadcastandwait: this.broadcastAndWait,
|
||||
event_whengreaterthan: this.hatGreaterThanPredicate
|
||||
};
|
||||
}
|
||||
|
||||
getHats () {
|
||||
return {
|
||||
event_whenflagclicked: {
|
||||
restartExistingThreads: true
|
||||
},
|
||||
event_whenkeypressed: {
|
||||
restartExistingThreads: false
|
||||
},
|
||||
event_whenthisspriteclicked: {
|
||||
restartExistingThreads: true
|
||||
},
|
||||
event_whentouchingobject: {
|
||||
restartExistingThreads: false,
|
||||
edgeActivated: true
|
||||
},
|
||||
event_whenstageclicked: {
|
||||
restartExistingThreads: true
|
||||
},
|
||||
event_whenbackdropswitchesto: {
|
||||
restartExistingThreads: true
|
||||
},
|
||||
event_whengreaterthan: {
|
||||
restartExistingThreads: false,
|
||||
edgeActivated: true
|
||||
},
|
||||
event_whenbroadcastreceived: {
|
||||
restartExistingThreads: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
touchingObject (args, util) {
|
||||
return util.target.isTouchingObject(args.TOUCHINGOBJECTMENU);
|
||||
}
|
||||
|
||||
hatGreaterThanPredicate (args, util) {
|
||||
const option = Cast.toString(args.WHENGREATERTHANMENU).toLowerCase();
|
||||
const value = Cast.toNumber(args.VALUE);
|
||||
switch (option) {
|
||||
case 'timer':
|
||||
return util.ioQuery('clock', 'projectTimer') > value;
|
||||
case 'loudness':
|
||||
return this.runtime.audioEngine && this.runtime.audioEngine.getLoudness() > value;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
broadcast (args, util) {
|
||||
const broadcastVar = util.runtime.getTargetForStage().lookupBroadcastMsg(
|
||||
args.BROADCAST_OPTION.id, args.BROADCAST_OPTION.name);
|
||||
if (broadcastVar) {
|
||||
const broadcastOption = broadcastVar.name;
|
||||
util.startHats('event_whenbroadcastreceived', {
|
||||
BROADCAST_OPTION: broadcastOption
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
broadcastAndWait (args, util) {
|
||||
if (!util.stackFrame.broadcastVar) {
|
||||
util.stackFrame.broadcastVar = util.runtime.getTargetForStage().lookupBroadcastMsg(
|
||||
args.BROADCAST_OPTION.id, args.BROADCAST_OPTION.name);
|
||||
}
|
||||
if (util.stackFrame.broadcastVar) {
|
||||
const broadcastOption = util.stackFrame.broadcastVar.name;
|
||||
// Have we run before, starting threads?
|
||||
if (!util.stackFrame.startedThreads) {
|
||||
// No - start hats for this broadcast.
|
||||
util.stackFrame.startedThreads = util.startHats(
|
||||
'event_whenbroadcastreceived', {
|
||||
BROADCAST_OPTION: broadcastOption
|
||||
}
|
||||
);
|
||||
if (util.stackFrame.startedThreads.length === 0) {
|
||||
// Nothing was started.
|
||||
return;
|
||||
}
|
||||
}
|
||||
// We've run before; check if the wait is still going on.
|
||||
const instance = this;
|
||||
// Scratch 2 considers threads to be waiting if they are still in
|
||||
// runtime.threads. Threads that have run all their blocks, or are
|
||||
// marked done but still in runtime.threads are still considered to
|
||||
// be waiting.
|
||||
const waiting = util.stackFrame.startedThreads
|
||||
.some(thread => instance.runtime.threads.indexOf(thread) !== -1);
|
||||
if (waiting) {
|
||||
// If all threads are waiting for the next tick or later yield
|
||||
// for a tick as well. Otherwise yield until the next loop of
|
||||
// the threads.
|
||||
if (
|
||||
util.stackFrame.startedThreads
|
||||
.every(thread => instance.runtime.isWaitingThread(thread))
|
||||
) {
|
||||
util.yieldTick();
|
||||
} else {
|
||||
util.yield();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Scratch3EventBlocks;
|
||||
615
scratch-vm/src/blocks/scratch3_looks.js
Normal file
615
scratch-vm/src/blocks/scratch3_looks.js
Normal file
@@ -0,0 +1,615 @@
|
||||
const Cast = require('../util/cast');
|
||||
const Clone = require('../util/clone');
|
||||
const uid = require('../util/uid');
|
||||
const StageLayering = require('../engine/stage-layering');
|
||||
const getMonitorIdForBlockWithArgs = require('../util/get-monitor-id');
|
||||
const MathUtil = require('../util/math-util');
|
||||
|
||||
/**
|
||||
* @typedef {object} BubbleState - the bubble state associated with a particular target.
|
||||
* @property {Boolean} onSpriteRight - tracks whether the bubble is right or left of the sprite.
|
||||
* @property {?int} drawableId - the ID of the associated bubble Drawable, null if none.
|
||||
* @property {string} text - the text of the bubble.
|
||||
* @property {string} type - the type of the bubble, "say" or "think"
|
||||
* @property {?string} usageId - ID indicating the most recent usage of the say/think bubble.
|
||||
* Used for comparison when determining whether to clear a say/think bubble.
|
||||
*/
|
||||
|
||||
class Scratch3LooksBlocks {
|
||||
constructor (runtime) {
|
||||
/**
|
||||
* The runtime instantiating this block package.
|
||||
* @type {Runtime}
|
||||
*/
|
||||
this.runtime = runtime;
|
||||
|
||||
this._onTargetChanged = this._onTargetChanged.bind(this);
|
||||
this._onResetBubbles = this._onResetBubbles.bind(this);
|
||||
this._onTargetWillExit = this._onTargetWillExit.bind(this);
|
||||
this._updateBubble = this._updateBubble.bind(this);
|
||||
|
||||
// Reset all bubbles on start/stop
|
||||
this.runtime.on('PROJECT_STOP_ALL', this._onResetBubbles);
|
||||
this.runtime.on('targetWasRemoved', this._onTargetWillExit);
|
||||
|
||||
// Enable other blocks to use bubbles like ask/answer
|
||||
this.runtime.on(Scratch3LooksBlocks.SAY_OR_THINK, this._updateBubble);
|
||||
}
|
||||
|
||||
/**
|
||||
* The default bubble state, to be used when a target has no existing bubble state.
|
||||
* @type {BubbleState}
|
||||
*/
|
||||
static get DEFAULT_BUBBLE_STATE () {
|
||||
return {
|
||||
drawableId: null,
|
||||
onSpriteRight: true,
|
||||
skinId: null,
|
||||
text: '',
|
||||
type: 'say',
|
||||
usageId: null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The key to load & store a target's bubble-related state.
|
||||
* @type {string}
|
||||
*/
|
||||
static get STATE_KEY () {
|
||||
return 'Scratch.looks';
|
||||
}
|
||||
|
||||
/**
|
||||
* Event name for a text bubble being created or updated.
|
||||
* @const {string}
|
||||
*/
|
||||
static get SAY_OR_THINK () {
|
||||
// There are currently many places in the codebase which explicitly refer to this event by the string 'SAY',
|
||||
// so keep this as the string 'SAY' for now rather than changing it to 'SAY_OR_THINK' and breaking things.
|
||||
return 'SAY';
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit for say bubble string.
|
||||
* @const {string}
|
||||
*/
|
||||
static get SAY_BUBBLE_LIMIT () {
|
||||
return 330;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit for ghost effect
|
||||
* @const {object}
|
||||
*/
|
||||
static get EFFECT_GHOST_LIMIT (){
|
||||
return {min: 0, max: 100};
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit for brightness effect
|
||||
* @const {object}
|
||||
*/
|
||||
static get EFFECT_BRIGHTNESS_LIMIT (){
|
||||
return {min: -100, max: 100};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Target} target - collect bubble state for this target. Probably, but not necessarily, a RenderedTarget.
|
||||
* @returns {BubbleState} the mutable bubble state associated with that target. This will be created if necessary.
|
||||
* @private
|
||||
*/
|
||||
_getBubbleState (target) {
|
||||
let bubbleState = target.getCustomState(Scratch3LooksBlocks.STATE_KEY);
|
||||
if (!bubbleState) {
|
||||
bubbleState = Clone.simple(Scratch3LooksBlocks.DEFAULT_BUBBLE_STATE);
|
||||
target.setCustomState(Scratch3LooksBlocks.STATE_KEY, bubbleState);
|
||||
}
|
||||
return bubbleState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a target which has moved.
|
||||
* @param {RenderedTarget} target - the target which has moved.
|
||||
* @private
|
||||
*/
|
||||
_onTargetChanged (target) {
|
||||
const bubbleState = this._getBubbleState(target);
|
||||
if (bubbleState.drawableId) {
|
||||
this._positionBubble(target);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a target which is exiting.
|
||||
* @param {RenderedTarget} target - the target.
|
||||
* @private
|
||||
*/
|
||||
_onTargetWillExit (target) {
|
||||
const bubbleState = this._getBubbleState(target);
|
||||
if (bubbleState.drawableId && bubbleState.skinId) {
|
||||
this.runtime.renderer.destroyDrawable(bubbleState.drawableId, StageLayering.SPRITE_LAYER);
|
||||
this.runtime.renderer.destroySkin(bubbleState.skinId);
|
||||
bubbleState.drawableId = null;
|
||||
bubbleState.skinId = null;
|
||||
this.runtime.requestRedraw();
|
||||
}
|
||||
target.onTargetVisualChange = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle project start/stop by clearing all visible bubbles.
|
||||
* @private
|
||||
*/
|
||||
_onResetBubbles () {
|
||||
for (let n = 0; n < this.runtime.targets.length; n++) {
|
||||
const bubbleState = this._getBubbleState(this.runtime.targets[n]);
|
||||
bubbleState.text = '';
|
||||
this._onTargetWillExit(this.runtime.targets[n]);
|
||||
}
|
||||
clearTimeout(this._bubbleTimeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Position the bubble of a target. If it doesn't fit on the specified side, flip and rerender.
|
||||
* @param {!Target} target Target whose bubble needs positioning.
|
||||
* @private
|
||||
*/
|
||||
_positionBubble (target) {
|
||||
if (!target.visible) return;
|
||||
const bubbleState = this._getBubbleState(target);
|
||||
const [bubbleWidth, bubbleHeight] = this.runtime.renderer.getCurrentSkinSize(bubbleState.drawableId);
|
||||
let targetBounds;
|
||||
try {
|
||||
targetBounds = target.getBoundsForBubble();
|
||||
} catch (error_) {
|
||||
// Bounds calculation could fail (e.g. on empty costumes), in that case
|
||||
// use the x/y position of the target.
|
||||
targetBounds = {
|
||||
left: target.x,
|
||||
right: target.x,
|
||||
top: target.y,
|
||||
bottom: target.y
|
||||
};
|
||||
}
|
||||
const stageSize = this.runtime.renderer.getNativeSize();
|
||||
const stageBounds = {
|
||||
left: -stageSize[0] / 2,
|
||||
right: stageSize[0] / 2,
|
||||
top: stageSize[1] / 2,
|
||||
bottom: -stageSize[1] / 2
|
||||
};
|
||||
if (bubbleState.onSpriteRight && bubbleWidth + targetBounds.right > stageBounds.right &&
|
||||
(targetBounds.left - bubbleWidth > stageBounds.left)) { // Only flip if it would fit
|
||||
bubbleState.onSpriteRight = false;
|
||||
this._renderBubble(target);
|
||||
} else if (!bubbleState.onSpriteRight && targetBounds.left - bubbleWidth < stageBounds.left &&
|
||||
(bubbleWidth + targetBounds.right < stageBounds.right)) { // Only flip if it would fit
|
||||
bubbleState.onSpriteRight = true;
|
||||
this._renderBubble(target);
|
||||
} else {
|
||||
this.runtime.renderer.updateDrawablePosition(bubbleState.drawableId, [
|
||||
bubbleState.onSpriteRight ? (
|
||||
Math.max(
|
||||
stageBounds.left, // Bubble should not extend past left edge of stage
|
||||
Math.min(stageBounds.right - bubbleWidth, targetBounds.right)
|
||||
)
|
||||
) : (
|
||||
Math.min(
|
||||
stageBounds.right - bubbleWidth, // Bubble should not extend past right edge of stage
|
||||
Math.max(stageBounds.left, targetBounds.left - bubbleWidth)
|
||||
)
|
||||
),
|
||||
// Bubble should not extend past the top of the stage
|
||||
Math.min(stageBounds.top, targetBounds.bottom + bubbleHeight)
|
||||
]);
|
||||
this.runtime.requestRedraw();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a visible bubble for a target. If a bubble exists for the target,
|
||||
* just set it to visible and update the type/text. Otherwise create a new
|
||||
* bubble and update the relevant custom state.
|
||||
* @param {!Target} target Target who needs a bubble.
|
||||
* @return {undefined} Early return if text is empty string.
|
||||
* @private
|
||||
*/
|
||||
_renderBubble (target) { // used by compiler
|
||||
if (!this.runtime.renderer) return;
|
||||
|
||||
const bubbleState = this._getBubbleState(target);
|
||||
const {type, text, onSpriteRight} = bubbleState;
|
||||
|
||||
// Remove the bubble if target is not visible, or text is being set to blank.
|
||||
if (!target.visible || text === '') {
|
||||
this._onTargetWillExit(target);
|
||||
return;
|
||||
}
|
||||
|
||||
if (bubbleState.skinId) {
|
||||
this.runtime.renderer.updateTextSkin(bubbleState.skinId, type, text, onSpriteRight, [0, 0]);
|
||||
} else {
|
||||
target.onTargetVisualChange = this._onTargetChanged;
|
||||
bubbleState.drawableId = this.runtime.renderer.createDrawable(StageLayering.SPRITE_LAYER);
|
||||
bubbleState.skinId = this.runtime.renderer.createTextSkin(type, text, bubbleState.onSpriteRight, [0, 0]);
|
||||
this.runtime.renderer.updateDrawableSkinId(bubbleState.drawableId, bubbleState.skinId);
|
||||
}
|
||||
|
||||
this._positionBubble(target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Properly format text for a text bubble.
|
||||
* @param {string} text The text to be formatted
|
||||
* @return {string} The formatted text
|
||||
* @private
|
||||
*/
|
||||
_formatBubbleText (text) {
|
||||
if (text === '') return text;
|
||||
|
||||
// Non-integers should be rounded to 2 decimal places (no more, no less), unless they're small enough that
|
||||
// rounding would display them as 0.00. This matches 2.0's behavior:
|
||||
// https://github.com/scratchfoundation/scratch-flash/blob/2e4a402ceb205a042887f54b26eebe1c2e6da6c0/src/scratch/ScratchSprite.as#L579-L585
|
||||
if (typeof text === 'number' &&
|
||||
Math.abs(text) >= 0.01 && text % 1 !== 0) {
|
||||
text = text.toFixed(2);
|
||||
}
|
||||
|
||||
// Limit the length of the string.
|
||||
text = String(text).substr(0, Scratch3LooksBlocks.SAY_BUBBLE_LIMIT);
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* The entry point for say/think blocks. Clears existing bubble if the text is empty.
|
||||
* Set the bubble custom state and then call _renderBubble.
|
||||
* @param {!Target} target Target that say/think blocks are being called on.
|
||||
* @param {!string} type Either "say" or "think"
|
||||
* @param {!string} text The text for the bubble, empty string clears the bubble.
|
||||
* @private
|
||||
*/
|
||||
_updateBubble (target, type, text) {
|
||||
const bubbleState = this._getBubbleState(target);
|
||||
bubbleState.type = type;
|
||||
bubbleState.text = this._formatBubbleText(text);
|
||||
bubbleState.usageId = uid();
|
||||
this._renderBubble(target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the block primitives implemented by this package.
|
||||
* @return {object.<string, Function>} Mapping of opcode to Function.
|
||||
*/
|
||||
getPrimitives () {
|
||||
return {
|
||||
looks_say: this.say,
|
||||
looks_sayforsecs: this.sayforsecs,
|
||||
looks_think: this.think,
|
||||
looks_thinkforsecs: this.thinkforsecs,
|
||||
looks_show: this.show,
|
||||
looks_hide: this.hide,
|
||||
looks_hideallsprites: () => {}, // legacy no-op block
|
||||
looks_switchcostumeto: this.switchCostume,
|
||||
looks_switchbackdropto: this.switchBackdrop,
|
||||
looks_switchbackdroptoandwait: this.switchBackdropAndWait,
|
||||
looks_nextcostume: this.nextCostume,
|
||||
looks_nextbackdrop: this.nextBackdrop,
|
||||
looks_changeeffectby: this.changeEffect,
|
||||
looks_seteffectto: this.setEffect,
|
||||
looks_cleargraphiceffects: this.clearEffects,
|
||||
looks_changesizeby: this.changeSize,
|
||||
looks_setsizeto: this.setSize,
|
||||
looks_changestretchby: () => {}, // legacy no-op blocks
|
||||
looks_setstretchto: () => {},
|
||||
looks_gotofrontback: this.goToFrontBack,
|
||||
looks_goforwardbackwardlayers: this.goForwardBackwardLayers,
|
||||
looks_size: this.getSize,
|
||||
looks_costumenumbername: this.getCostumeNumberName,
|
||||
looks_backdropnumbername: this.getBackdropNumberName
|
||||
};
|
||||
}
|
||||
|
||||
getMonitored () {
|
||||
return {
|
||||
looks_size: {
|
||||
isSpriteSpecific: true,
|
||||
getId: targetId => `${targetId}_size`
|
||||
},
|
||||
looks_costumenumbername: {
|
||||
isSpriteSpecific: true,
|
||||
getId: (targetId, fields) => getMonitorIdForBlockWithArgs(`${targetId}_costumenumbername`, fields)
|
||||
},
|
||||
looks_backdropnumbername: {
|
||||
getId: (_, fields) => getMonitorIdForBlockWithArgs('backdropnumbername', fields)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
say (args, util) {
|
||||
// @TODO in 2.0 calling say/think resets the right/left bias of the bubble
|
||||
const message = args.MESSAGE;
|
||||
this._say(message, util.target);
|
||||
}
|
||||
_say (message, target) { // used by compiler
|
||||
this.runtime.emit(Scratch3LooksBlocks.SAY_OR_THINK, target, 'say', message);
|
||||
}
|
||||
|
||||
sayforsecs (args, util) {
|
||||
this.say(args, util);
|
||||
const target = util.target;
|
||||
const usageId = this._getBubbleState(target).usageId;
|
||||
return new Promise(resolve => {
|
||||
this._bubbleTimeout = setTimeout(() => {
|
||||
this._bubbleTimeout = null;
|
||||
// Clear say bubble if it hasn't been changed and proceed.
|
||||
if (this._getBubbleState(target).usageId === usageId) {
|
||||
this._updateBubble(target, 'say', '');
|
||||
}
|
||||
resolve();
|
||||
}, 1000 * args.SECS);
|
||||
});
|
||||
}
|
||||
|
||||
think (args, util) {
|
||||
this.runtime.emit(Scratch3LooksBlocks.SAY_OR_THINK, util.target, 'think', args.MESSAGE);
|
||||
}
|
||||
|
||||
thinkforsecs (args, util) {
|
||||
this.think(args, util);
|
||||
const target = util.target;
|
||||
const usageId = this._getBubbleState(target).usageId;
|
||||
return new Promise(resolve => {
|
||||
this._bubbleTimeout = setTimeout(() => {
|
||||
this._bubbleTimeout = null;
|
||||
// Clear think bubble if it hasn't been changed and proceed.
|
||||
if (this._getBubbleState(target).usageId === usageId) {
|
||||
this._updateBubble(target, 'think', '');
|
||||
}
|
||||
resolve();
|
||||
}, 1000 * args.SECS);
|
||||
});
|
||||
}
|
||||
|
||||
show (args, util) {
|
||||
util.target.setVisible(true);
|
||||
this._renderBubble(util.target);
|
||||
}
|
||||
|
||||
hide (args, util) {
|
||||
util.target.setVisible(false);
|
||||
this._renderBubble(util.target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to set the costume of a target.
|
||||
* Matches the behavior of Scratch 2.0 for different types of arguments.
|
||||
* @param {!Target} target Target to set costume to.
|
||||
* @param {Any} requestedCostume Costume requested, e.g., 0, 'name', etc.
|
||||
* @param {boolean=} optZeroIndex Set to zero-index the requestedCostume.
|
||||
* @return {Array.<!Thread>} Any threads started by this switch.
|
||||
*/
|
||||
_setCostume (target, requestedCostume, optZeroIndex) { // used by compiler
|
||||
if (typeof requestedCostume === 'number') {
|
||||
// Numbers should be treated as costume indices, always
|
||||
target.setCostume(optZeroIndex ? requestedCostume : requestedCostume - 1);
|
||||
} else {
|
||||
// Strings should be treated as costume names, where possible
|
||||
const costumeIndex = target.getCostumeIndexByName(requestedCostume.toString());
|
||||
|
||||
if (costumeIndex !== -1) {
|
||||
target.setCostume(costumeIndex);
|
||||
} else if (requestedCostume === 'next costume') {
|
||||
target.setCostume(target.currentCostume + 1);
|
||||
} else if (requestedCostume === 'previous costume') {
|
||||
target.setCostume(target.currentCostume - 1);
|
||||
// Try to cast the string to a number (and treat it as a costume index)
|
||||
// Pure whitespace should not be treated as a number
|
||||
// Note: isNaN will cast the string to a number before checking if it's NaN
|
||||
} else if (!(isNaN(requestedCostume) || Cast.isWhiteSpace(requestedCostume))) {
|
||||
target.setCostume(optZeroIndex ? Number(requestedCostume) : Number(requestedCostume) - 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Per 2.0, 'switch costume' can't start threads even in the Stage.
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to set the backdrop of a target.
|
||||
* Matches the behavior of Scratch 2.0 for different types of arguments.
|
||||
* @param {!Target} stage Target to set backdrop to.
|
||||
* @param {Any} requestedBackdrop Backdrop requested, e.g., 0, 'name', etc.
|
||||
* @param {boolean=} optZeroIndex Set to zero-index the requestedBackdrop.
|
||||
* @return {Array.<!Thread>} Any threads started by this switch.
|
||||
*/
|
||||
_setBackdrop (stage, requestedBackdrop, optZeroIndex) { // used by compiler
|
||||
if (typeof requestedBackdrop === 'number') {
|
||||
// Numbers should be treated as backdrop indices, always
|
||||
stage.setCostume(optZeroIndex ? requestedBackdrop : requestedBackdrop - 1);
|
||||
} else {
|
||||
// Strings should be treated as backdrop names where possible
|
||||
const costumeIndex = stage.getCostumeIndexByName(requestedBackdrop.toString());
|
||||
|
||||
if (costumeIndex !== -1) {
|
||||
stage.setCostume(costumeIndex);
|
||||
} else if (requestedBackdrop === 'next backdrop') {
|
||||
stage.setCostume(stage.currentCostume + 1);
|
||||
} else if (requestedBackdrop === 'previous backdrop') {
|
||||
stage.setCostume(stage.currentCostume - 1);
|
||||
} else if (requestedBackdrop === 'random backdrop') {
|
||||
const numCostumes = stage.getCostumes().length;
|
||||
if (numCostumes > 1) {
|
||||
// Don't pick the current backdrop, so that the block
|
||||
// will always have an observable effect.
|
||||
const lowerBound = 0;
|
||||
const upperBound = numCostumes - 1;
|
||||
const costumeToExclude = stage.currentCostume;
|
||||
|
||||
const nextCostume = MathUtil.inclusiveRandIntWithout(lowerBound, upperBound, costumeToExclude);
|
||||
|
||||
stage.setCostume(nextCostume);
|
||||
}
|
||||
// Try to cast the string to a number (and treat it as a costume index)
|
||||
// Pure whitespace should not be treated as a number
|
||||
// Note: isNaN will cast the string to a number before checking if it's NaN
|
||||
} else if (!(isNaN(requestedBackdrop) || Cast.isWhiteSpace(requestedBackdrop))) {
|
||||
stage.setCostume(optZeroIndex ? Number(requestedBackdrop) : Number(requestedBackdrop) - 1);
|
||||
}
|
||||
}
|
||||
|
||||
const newName = stage.getCostumes()[stage.currentCostume].name;
|
||||
return this.runtime.startHats('event_whenbackdropswitchesto', {
|
||||
BACKDROP: newName
|
||||
});
|
||||
}
|
||||
|
||||
switchCostume (args, util) {
|
||||
this._setCostume(util.target, args.COSTUME); // used by compiler
|
||||
}
|
||||
|
||||
nextCostume (args, util) {
|
||||
this._setCostume(
|
||||
util.target, util.target.currentCostume + 1, true
|
||||
);
|
||||
}
|
||||
|
||||
switchBackdrop (args) {
|
||||
this._setBackdrop(this.runtime.getTargetForStage(), args.BACKDROP);
|
||||
}
|
||||
|
||||
switchBackdropAndWait (args, util) {
|
||||
// Have we run before, starting threads?
|
||||
if (!util.stackFrame.startedThreads) {
|
||||
// No - switch the backdrop.
|
||||
util.stackFrame.startedThreads = (
|
||||
this._setBackdrop(
|
||||
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.
|
||||
const instance = this;
|
||||
// Scratch 2 considers threads to be waiting if they are still in
|
||||
// runtime.threads. Threads that have run all their blocks, or are
|
||||
// marked done but still in runtime.threads are still considered to
|
||||
// be waiting.
|
||||
const waiting = util.stackFrame.startedThreads
|
||||
.some(thread => instance.runtime.threads.indexOf(thread) !== -1);
|
||||
if (waiting) {
|
||||
// If all threads are waiting for the next tick or later yield
|
||||
// for a tick as well. Otherwise yield until the next loop of
|
||||
// the threads.
|
||||
if (
|
||||
util.stackFrame.startedThreads
|
||||
.every(thread => instance.runtime.isWaitingThread(thread))
|
||||
) {
|
||||
util.yieldTick();
|
||||
} else {
|
||||
util.yield();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nextBackdrop () {
|
||||
const stage = this.runtime.getTargetForStage();
|
||||
this._setBackdrop(
|
||||
stage, stage.currentCostume + 1, true
|
||||
);
|
||||
}
|
||||
|
||||
clampEffect (effect, value) { // used by compiler
|
||||
let clampedValue = value;
|
||||
switch (effect) {
|
||||
case 'ghost':
|
||||
clampedValue = MathUtil.clamp(value,
|
||||
Scratch3LooksBlocks.EFFECT_GHOST_LIMIT.min,
|
||||
Scratch3LooksBlocks.EFFECT_GHOST_LIMIT.max);
|
||||
break;
|
||||
case 'brightness':
|
||||
clampedValue = MathUtil.clamp(value,
|
||||
Scratch3LooksBlocks.EFFECT_BRIGHTNESS_LIMIT.min,
|
||||
Scratch3LooksBlocks.EFFECT_BRIGHTNESS_LIMIT.max);
|
||||
break;
|
||||
}
|
||||
return clampedValue;
|
||||
}
|
||||
|
||||
changeEffect (args, util) {
|
||||
const effect = Cast.toString(args.EFFECT).toLowerCase();
|
||||
const change = Cast.toNumber(args.CHANGE);
|
||||
if (!Object.prototype.hasOwnProperty.call(util.target.effects, effect)) return;
|
||||
let newValue = change + util.target.effects[effect];
|
||||
newValue = this.clampEffect(effect, newValue);
|
||||
util.target.setEffect(effect, newValue);
|
||||
}
|
||||
|
||||
setEffect (args, util) {
|
||||
const effect = Cast.toString(args.EFFECT).toLowerCase();
|
||||
let value = Cast.toNumber(args.VALUE);
|
||||
value = this.clampEffect(effect, value);
|
||||
util.target.setEffect(effect, value);
|
||||
}
|
||||
|
||||
clearEffects (args, util) {
|
||||
util.target.clearEffects();
|
||||
}
|
||||
|
||||
changeSize (args, util) {
|
||||
const change = Cast.toNumber(args.CHANGE);
|
||||
util.target.setSize(util.target.size + change);
|
||||
}
|
||||
|
||||
setSize (args, util) {
|
||||
const size = Cast.toNumber(args.SIZE);
|
||||
util.target.setSize(size);
|
||||
}
|
||||
|
||||
goToFrontBack (args, util) {
|
||||
if (!util.target.isStage) {
|
||||
if (args.FRONT_BACK === 'front') {
|
||||
util.target.goToFront();
|
||||
} else {
|
||||
util.target.goToBack();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
goForwardBackwardLayers (args, util) {
|
||||
if (!util.target.isStage) {
|
||||
if (args.FORWARD_BACKWARD === 'forward') {
|
||||
util.target.goForwardLayers(Cast.toNumber(args.NUM));
|
||||
} else {
|
||||
util.target.goBackwardLayers(Cast.toNumber(args.NUM));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getSize (args, util) {
|
||||
return Math.round(util.target.size);
|
||||
}
|
||||
|
||||
getBackdropNumberName (args) {
|
||||
const stage = this.runtime.getTargetForStage();
|
||||
if (args.NUMBER_NAME === 'number') {
|
||||
return stage.currentCostume + 1;
|
||||
}
|
||||
// Else return name
|
||||
return stage.getCostumes()[stage.currentCostume].name;
|
||||
}
|
||||
|
||||
getCostumeNumberName (args, util) {
|
||||
if (args.NUMBER_NAME === 'number') {
|
||||
return util.target.currentCostume + 1;
|
||||
}
|
||||
// Else return name
|
||||
return util.target.getCostumes()[util.target.currentCostume].name;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Scratch3LooksBlocks;
|
||||
328
scratch-vm/src/blocks/scratch3_motion.js
Normal file
328
scratch-vm/src/blocks/scratch3_motion.js
Normal file
@@ -0,0 +1,328 @@
|
||||
const Cast = require('../util/cast');
|
||||
const { debug } = require('../util/log');
|
||||
const MathUtil = require('../util/math-util');
|
||||
const Timer = require('../util/timer');
|
||||
|
||||
class Scratch3MotionBlocks {
|
||||
constructor (runtime) {
|
||||
/**
|
||||
* The runtime instantiating this block package.
|
||||
* @type {Runtime}
|
||||
*/
|
||||
this.runtime = runtime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the block primitives implemented by this package.
|
||||
* @return {object.<string, Function>} Mapping of opcode to Function.
|
||||
*/
|
||||
getPrimitives () {
|
||||
console.log("Primitives being registered");
|
||||
return {
|
||||
motion_movesteps: this.moveSteps,
|
||||
motion_testblock: this.testblock,
|
||||
motion_move_f: this.MoveFSteps,
|
||||
motion_move_b: this.MoveBSteps,
|
||||
motion_cmd_print: this.CMDprint,
|
||||
motion_gotoxy: this.goToXY,
|
||||
motion_goto: this.goTo,
|
||||
motion_turnright: this.turnRight,
|
||||
motion_turnleft: this.turnLeft,
|
||||
motion_jump_move: this.motion_jump_move,
|
||||
motion_pointindirection: this.pointInDirection,
|
||||
motion_pointtowards: this.pointTowards,
|
||||
motion_glidesecstoxy: this.glide,
|
||||
motion_glideto: this.glideTo,
|
||||
motion_ifonedgebounce: this.ifOnEdgeBounce,
|
||||
motion_setrotationstyle: this.setRotationStyle,
|
||||
motion_changexby: this.changeX,
|
||||
motion_setx: this.setX,
|
||||
motion_changeyby: this.changeY,
|
||||
motion_sety: this.setY,
|
||||
motion_xposition: this.getX,
|
||||
motion_yposition: this.getY,
|
||||
motion_direction: this.getDirection,
|
||||
// Legacy no-op blocks:
|
||||
motion_scroll_right: () => {},
|
||||
motion_scroll_up: () => {},
|
||||
motion_align_scene: () => {},
|
||||
motion_xscroll: () => {},
|
||||
motion_yscroll: () => {}
|
||||
};
|
||||
}
|
||||
|
||||
getMonitored () {
|
||||
return {
|
||||
motion_xposition: {
|
||||
isSpriteSpecific: true,
|
||||
getId: targetId => `${targetId}_xposition`
|
||||
},
|
||||
motion_yposition: {
|
||||
isSpriteSpecific: true,
|
||||
getId: targetId => `${targetId}_yposition`
|
||||
},
|
||||
motion_direction: {
|
||||
isSpriteSpecific: true,
|
||||
getId: targetId => `${targetId}_direction`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
moveSteps (args, util) {
|
||||
const steps = Cast.toNumber(args.STEPS);
|
||||
this._moveSteps(steps, util.target);
|
||||
}
|
||||
|
||||
MoveBSteps (args, util) {
|
||||
// const steps = Cast.toNumber(args.STEPS);
|
||||
// this._moveSteps(steps, util.target);
|
||||
}
|
||||
|
||||
MoveFSteps (args, util) {
|
||||
// const steps = Cast.toNumber(args.STEPS);
|
||||
// this._moveSteps(steps, util.target);
|
||||
}
|
||||
|
||||
CMDprint (args, util) {
|
||||
// const text = Cast.toString(args.TEXT);
|
||||
// console.log(text);
|
||||
}
|
||||
|
||||
_moveSteps (steps, target) { // used by compiler
|
||||
const radians = MathUtil.degToRad(90 - target.direction);
|
||||
const dx = steps * Math.cos(radians);
|
||||
const dy = steps * Math.sin(radians);
|
||||
target.setXY(target.x + dx, target.y + dy);
|
||||
}
|
||||
|
||||
goToXY (args, util) {
|
||||
const x = Cast.toNumber(args.X);
|
||||
const y = Cast.toNumber(args.Y);
|
||||
util.target.setXY(x, y);
|
||||
}
|
||||
|
||||
getTargetXY (targetName, util) {
|
||||
let targetX = 0;
|
||||
let targetY = 0;
|
||||
if (targetName === '_mouse_') {
|
||||
targetX = util.ioQuery('mouse', 'getScratchX');
|
||||
targetY = util.ioQuery('mouse', 'getScratchY');
|
||||
} else if (targetName === '_random_') {
|
||||
const stageWidth = this.runtime.stageWidth;
|
||||
const stageHeight = this.runtime.stageHeight;
|
||||
targetX = Math.round(stageWidth * (Math.random() - 0.5));
|
||||
targetY = Math.round(stageHeight * (Math.random() - 0.5));
|
||||
} else {
|
||||
targetName = Cast.toString(targetName);
|
||||
const goToTarget = this.runtime.getSpriteTargetByName(targetName);
|
||||
if (!goToTarget) return;
|
||||
targetX = goToTarget.x;
|
||||
targetY = goToTarget.y;
|
||||
}
|
||||
return [targetX, targetY];
|
||||
}
|
||||
|
||||
goTo (args, util) {
|
||||
const targetXY = this.getTargetXY(args.TO, util);
|
||||
if (targetXY) {
|
||||
util.target.setXY(targetXY[0], targetXY[1]);
|
||||
}
|
||||
}
|
||||
|
||||
testblock (args, util) {
|
||||
const steps = Cast.toNumber(args.STEPS);
|
||||
this._moveSteps(steps, util.target);
|
||||
}
|
||||
|
||||
|
||||
turnRight (args, util) {
|
||||
// const degrees = Cast.toNumber(args.DEGREES);
|
||||
// util.target.setDirection(util.target.direction + degrees);
|
||||
}
|
||||
|
||||
turnLeft (args, util) {
|
||||
// const degrees = Cast.toNumber(args.DEGREES);
|
||||
// util.target.setDirection(util.target.direction - degrees);
|
||||
}
|
||||
|
||||
motion_jump_move (args, util) {
|
||||
|
||||
}
|
||||
|
||||
pointInDirection (args, util) {
|
||||
const direction = Cast.toNumber(args.DIRECTION);
|
||||
util.target.setDirection(direction);
|
||||
}
|
||||
|
||||
pointTowards (args, util) {
|
||||
let targetX = 0;
|
||||
let targetY = 0;
|
||||
if (args.TOWARDS === '_mouse_') {
|
||||
targetX = util.ioQuery('mouse', 'getScratchX');
|
||||
targetY = util.ioQuery('mouse', 'getScratchY');
|
||||
} else if (args.TOWARDS === '_random_') {
|
||||
util.target.setDirection(Math.round(Math.random() * 360) - 180);
|
||||
return;
|
||||
} else {
|
||||
args.TOWARDS = Cast.toString(args.TOWARDS);
|
||||
const pointTarget = this.runtime.getSpriteTargetByName(args.TOWARDS);
|
||||
if (!pointTarget) return;
|
||||
targetX = pointTarget.x;
|
||||
targetY = pointTarget.y;
|
||||
}
|
||||
|
||||
const dx = targetX - util.target.x;
|
||||
const dy = targetY - util.target.y;
|
||||
const direction = 90 - MathUtil.radToDeg(Math.atan2(dy, dx));
|
||||
util.target.setDirection(direction);
|
||||
}
|
||||
|
||||
glide (args, util) {
|
||||
if (util.stackFrame.timer) {
|
||||
const timeElapsed = util.stackFrame.timer.timeElapsed();
|
||||
if (timeElapsed < util.stackFrame.duration * 1000) {
|
||||
// In progress: move to intermediate position.
|
||||
const frac = timeElapsed / (util.stackFrame.duration * 1000);
|
||||
const dx = frac * (util.stackFrame.endX - util.stackFrame.startX);
|
||||
const dy = frac * (util.stackFrame.endY - util.stackFrame.startY);
|
||||
util.target.setXY(
|
||||
util.stackFrame.startX + dx,
|
||||
util.stackFrame.startY + dy
|
||||
);
|
||||
util.yield();
|
||||
} else {
|
||||
// Finished: move to final position.
|
||||
util.target.setXY(util.stackFrame.endX, util.stackFrame.endY);
|
||||
}
|
||||
} else {
|
||||
// First time: save data for future use.
|
||||
util.stackFrame.timer = new Timer();
|
||||
util.stackFrame.timer.start();
|
||||
util.stackFrame.duration = Cast.toNumber(args.SECS);
|
||||
util.stackFrame.startX = util.target.x;
|
||||
util.stackFrame.startY = util.target.y;
|
||||
util.stackFrame.endX = Cast.toNumber(args.X);
|
||||
util.stackFrame.endY = Cast.toNumber(args.Y);
|
||||
if (util.stackFrame.duration <= 0) {
|
||||
// Duration too short to glide.
|
||||
util.target.setXY(util.stackFrame.endX, util.stackFrame.endY);
|
||||
return;
|
||||
}
|
||||
util.yield();
|
||||
}
|
||||
}
|
||||
|
||||
glideTo (args, util) {
|
||||
const targetXY = this.getTargetXY(args.TO, util);
|
||||
if (targetXY) {
|
||||
this.glide({SECS: args.SECS, X: targetXY[0], Y: targetXY[1]}, util);
|
||||
}
|
||||
}
|
||||
|
||||
ifOnEdgeBounce (args, util) {
|
||||
this._ifOnEdgeBounce(util.target);
|
||||
}
|
||||
_ifOnEdgeBounce (target) { // used by compiler
|
||||
const bounds = target.getBounds();
|
||||
if (!bounds) {
|
||||
return;
|
||||
}
|
||||
// Measure distance to edges.
|
||||
// Values are positive when the sprite is far away,
|
||||
// and clamped to zero when the sprite is beyond.
|
||||
const stageWidth = this.runtime.stageWidth;
|
||||
const stageHeight = this.runtime.stageHeight;
|
||||
const distLeft = Math.max(0, (stageWidth / 2) + bounds.left);
|
||||
const distTop = Math.max(0, (stageHeight / 2) - bounds.top);
|
||||
const distRight = Math.max(0, (stageWidth / 2) - bounds.right);
|
||||
const distBottom = Math.max(0, (stageHeight / 2) + bounds.bottom);
|
||||
// Find the nearest edge.
|
||||
let nearestEdge = '';
|
||||
let minDist = Infinity;
|
||||
if (distLeft < minDist) {
|
||||
minDist = distLeft;
|
||||
nearestEdge = 'left';
|
||||
}
|
||||
if (distTop < minDist) {
|
||||
minDist = distTop;
|
||||
nearestEdge = 'top';
|
||||
}
|
||||
if (distRight < minDist) {
|
||||
minDist = distRight;
|
||||
nearestEdge = 'right';
|
||||
}
|
||||
if (distBottom < minDist) {
|
||||
minDist = distBottom;
|
||||
nearestEdge = 'bottom';
|
||||
}
|
||||
if (minDist > 0) {
|
||||
return; // Not touching any edge.
|
||||
}
|
||||
// Point away from the nearest edge.
|
||||
const radians = MathUtil.degToRad(90 - target.direction);
|
||||
let dx = Math.cos(radians);
|
||||
let dy = -Math.sin(radians);
|
||||
if (nearestEdge === 'left') {
|
||||
dx = Math.max(0.2, Math.abs(dx));
|
||||
} else if (nearestEdge === 'top') {
|
||||
dy = Math.max(0.2, Math.abs(dy));
|
||||
} else if (nearestEdge === 'right') {
|
||||
dx = 0 - Math.max(0.2, Math.abs(dx));
|
||||
} else if (nearestEdge === 'bottom') {
|
||||
dy = 0 - Math.max(0.2, Math.abs(dy));
|
||||
}
|
||||
const newDirection = MathUtil.radToDeg(Math.atan2(dy, dx)) + 90;
|
||||
target.setDirection(newDirection);
|
||||
// Keep within the stage.
|
||||
const fencedPosition = target.keepInFence(target.x, target.y);
|
||||
target.setXY(fencedPosition[0], fencedPosition[1]);
|
||||
}
|
||||
|
||||
setRotationStyle (args, util) {
|
||||
util.target.setRotationStyle(args.STYLE);
|
||||
}
|
||||
|
||||
changeX (args, util) {
|
||||
const dx = Cast.toNumber(args.DX);
|
||||
util.target.setXY(util.target.x + dx, util.target.y);
|
||||
}
|
||||
|
||||
setX (args, util) {
|
||||
const x = Cast.toNumber(args.X);
|
||||
util.target.setXY(x, util.target.y);
|
||||
}
|
||||
|
||||
changeY (args, util) {
|
||||
const dy = Cast.toNumber(args.DY);
|
||||
util.target.setXY(util.target.x, util.target.y + dy);
|
||||
}
|
||||
|
||||
setY (args, util) {
|
||||
debugger
|
||||
const y = Cast.toNumber(args.Y);
|
||||
util.target.setXY(util.target.x, y);
|
||||
}
|
||||
|
||||
getX (args, util) {
|
||||
// return this.limitPrecision(util.target.x);
|
||||
}
|
||||
|
||||
getY (args, util) {
|
||||
// return this.limitPrecision(util.target.y);
|
||||
}
|
||||
|
||||
getDirection (args, util) {
|
||||
// return util.target.direction;
|
||||
}
|
||||
|
||||
// This corresponds to snapToInteger in Scratch 2
|
||||
limitPrecision (coordinate) {
|
||||
const rounded = Math.round(coordinate);
|
||||
const delta = coordinate - rounded;
|
||||
const limitedCoord = (Math.abs(delta) < 1e-9) ? rounded : coordinate;
|
||||
|
||||
return limitedCoord;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Scratch3MotionBlocks;
|
||||
157
scratch-vm/src/blocks/scratch3_operators.js
Normal file
157
scratch-vm/src/blocks/scratch3_operators.js
Normal file
@@ -0,0 +1,157 @@
|
||||
const Cast = require('../util/cast.js');
|
||||
const MathUtil = require('../util/math-util.js');
|
||||
|
||||
class Scratch3OperatorsBlocks {
|
||||
constructor (runtime) {
|
||||
/**
|
||||
* The runtime instantiating this block package.
|
||||
* @type {Runtime}
|
||||
*/
|
||||
this.runtime = runtime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the block primitives implemented by this package.
|
||||
* @return {object.<string, Function>} Mapping of opcode to Function.
|
||||
*/
|
||||
getPrimitives () {
|
||||
return {
|
||||
operator_add: this.add,
|
||||
operator_subtract: this.subtract,
|
||||
operator_multiply: this.multiply,
|
||||
operator_divide: this.divide,
|
||||
operator_lt: this.lt,
|
||||
operator_equals: this.equals,
|
||||
operator_gt: this.gt,
|
||||
operator_and: this.and,
|
||||
operator_or: this.or,
|
||||
operator_not: this.not,
|
||||
operator_random: this.random,
|
||||
operator_join: this.join,
|
||||
operator_letter_of: this.letterOf,
|
||||
operator_length: this.length,
|
||||
operator_contains: this.contains,
|
||||
operator_mod: this.mod,
|
||||
operator_round: this.round,
|
||||
operator_mathop: this.mathop
|
||||
};
|
||||
}
|
||||
|
||||
add (args) {
|
||||
return Cast.toNumber(args.NUM1) + Cast.toNumber(args.NUM2);
|
||||
}
|
||||
|
||||
subtract (args) {
|
||||
return Cast.toNumber(args.NUM1) - Cast.toNumber(args.NUM2);
|
||||
}
|
||||
|
||||
multiply (args) {
|
||||
return Cast.toNumber(args.NUM1) * Cast.toNumber(args.NUM2);
|
||||
}
|
||||
|
||||
divide (args) {
|
||||
return Cast.toNumber(args.NUM1) / Cast.toNumber(args.NUM2);
|
||||
}
|
||||
|
||||
lt (args) {
|
||||
return Cast.compare(args.OPERAND1, args.OPERAND2) < 0;
|
||||
}
|
||||
|
||||
equals (args) {
|
||||
return Cast.compare(args.OPERAND1, args.OPERAND2) === 0;
|
||||
}
|
||||
|
||||
gt (args) {
|
||||
return Cast.compare(args.OPERAND1, args.OPERAND2) > 0;
|
||||
}
|
||||
|
||||
and (args) {
|
||||
return Cast.toBoolean(args.OPERAND1) && Cast.toBoolean(args.OPERAND2);
|
||||
}
|
||||
|
||||
or (args) {
|
||||
return Cast.toBoolean(args.OPERAND1) || Cast.toBoolean(args.OPERAND2);
|
||||
}
|
||||
|
||||
not (args) {
|
||||
return !Cast.toBoolean(args.OPERAND);
|
||||
}
|
||||
|
||||
random (args) {
|
||||
return this._random(args.FROM, args.TO);
|
||||
}
|
||||
_random (from, to) { // used by compiler
|
||||
const nFrom = Cast.toNumber(from);
|
||||
const nTo = Cast.toNumber(to);
|
||||
const low = nFrom <= nTo ? nFrom : nTo;
|
||||
const high = nFrom <= nTo ? nTo : nFrom;
|
||||
if (low === high) return low;
|
||||
// If both arguments are ints, truncate the result to an int.
|
||||
if (Cast.isInt(from) && Cast.isInt(to)) {
|
||||
return low + Math.floor(Math.random() * ((high + 1) - low));
|
||||
}
|
||||
return (Math.random() * (high - low)) + low;
|
||||
}
|
||||
|
||||
join (args) {
|
||||
return Cast.toString(args.STRING1) + Cast.toString(args.STRING2);
|
||||
}
|
||||
|
||||
letterOf (args) {
|
||||
const index = Cast.toNumber(args.LETTER) - 1;
|
||||
const str = Cast.toString(args.STRING);
|
||||
// Out of bounds?
|
||||
if (index < 0 || index >= str.length) {
|
||||
return '';
|
||||
}
|
||||
return str.charAt(index);
|
||||
}
|
||||
|
||||
length (args) {
|
||||
return Cast.toString(args.STRING).length;
|
||||
}
|
||||
|
||||
contains (args) {
|
||||
const format = function (string) {
|
||||
return Cast.toString(string).toLowerCase();
|
||||
};
|
||||
return format(args.STRING1).includes(format(args.STRING2));
|
||||
}
|
||||
|
||||
mod (args) {
|
||||
const n = Cast.toNumber(args.NUM1);
|
||||
const modulus = Cast.toNumber(args.NUM2);
|
||||
let result = n % modulus;
|
||||
// Scratch mod uses floored division instead of truncated division.
|
||||
if (result / modulus < 0) result += modulus;
|
||||
return result;
|
||||
}
|
||||
|
||||
round (args) {
|
||||
return Math.round(Cast.toNumber(args.NUM));
|
||||
}
|
||||
|
||||
mathop (args) {
|
||||
const operator = Cast.toString(args.OPERATOR).toLowerCase();
|
||||
const n = Cast.toNumber(args.NUM);
|
||||
switch (operator) {
|
||||
case 'abs': return Math.abs(n);
|
||||
case 'floor': return Math.floor(n);
|
||||
case 'ceiling': return Math.ceil(n);
|
||||
case 'sqrt': return Math.sqrt(n);
|
||||
case 'sin': return Math.round(Math.sin((Math.PI * n) / 180) * 1e10) / 1e10;
|
||||
case 'cos': return Math.round(Math.cos((Math.PI * n) / 180) * 1e10) / 1e10;
|
||||
case 'tan': return MathUtil.tan(n);
|
||||
case 'asin': return (Math.asin(n) * 180) / Math.PI;
|
||||
case 'acos': return (Math.acos(n) * 180) / Math.PI;
|
||||
case 'atan': return (Math.atan(n) * 180) / Math.PI;
|
||||
case 'ln': return Math.log(n);
|
||||
case 'log': return Math.log(n) / Math.LN10;
|
||||
case 'e ^': return Math.exp(n);
|
||||
case '10 ^': return Math.pow(10, n);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Scratch3OperatorsBlocks;
|
||||
136
scratch-vm/src/blocks/scratch3_procedures.js
Normal file
136
scratch-vm/src/blocks/scratch3_procedures.js
Normal file
@@ -0,0 +1,136 @@
|
||||
class Scratch3ProcedureBlocks {
|
||||
constructor (runtime) {
|
||||
/**
|
||||
* The runtime instantiating this block package.
|
||||
* @type {Runtime}
|
||||
*/
|
||||
this.runtime = runtime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the block primitives implemented by this package.
|
||||
* @return {object.<string, Function>} Mapping of opcode to Function.
|
||||
*/
|
||||
getPrimitives () {
|
||||
return {
|
||||
procedures_definition: this.definition,
|
||||
procedures_call: this.call,
|
||||
procedures_return: this.return,
|
||||
argument_reporter_string_number: this.argumentReporterStringNumber,
|
||||
argument_reporter_boolean: this.argumentReporterBoolean
|
||||
};
|
||||
}
|
||||
|
||||
definition () {
|
||||
// No-op: execute the blocks.
|
||||
}
|
||||
|
||||
call (args, util) {
|
||||
const stackFrame = util.stackFrame;
|
||||
const isReporter = !!args.mutation.return;
|
||||
|
||||
if (stackFrame.executed) {
|
||||
if (isReporter) {
|
||||
const returnValue = stackFrame.returnValue;
|
||||
// This stackframe will be reused for other reporters in this block, so clean it up for them.
|
||||
// Can't use reset() because that will reset too much.
|
||||
const threadStackFrame = util.thread.peekStackFrame();
|
||||
threadStackFrame.params = null;
|
||||
delete stackFrame.returnValue;
|
||||
delete stackFrame.executed;
|
||||
return returnValue;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const procedureCode = args.mutation.proccode;
|
||||
const paramNamesIdsAndDefaults = util.getProcedureParamNamesIdsAndDefaults(procedureCode);
|
||||
|
||||
// If null, procedure could not be found, which can happen if custom
|
||||
// block is dragged between sprites without the definition.
|
||||
// Match Scratch 2.0 behavior and noop.
|
||||
if (paramNamesIdsAndDefaults === null) {
|
||||
if (isReporter) {
|
||||
return '';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const [paramNames, paramIds, paramDefaults] = paramNamesIdsAndDefaults;
|
||||
|
||||
// Initialize params for the current stackFrame to {}, even if the procedure does
|
||||
// not take any arguments. This is so that `getParam` down the line does not look
|
||||
// at earlier stack frames for the values of a given parameter (#1729)
|
||||
util.initParams();
|
||||
for (let i = 0; i < paramIds.length; i++) {
|
||||
if (Object.prototype.hasOwnProperty.call(args, paramIds[i])) {
|
||||
util.pushParam(paramNames[i], args[paramIds[i]]);
|
||||
} else {
|
||||
util.pushParam(paramNames[i], paramDefaults[i]);
|
||||
}
|
||||
}
|
||||
|
||||
const addonBlock = util.runtime.getAddonBlock(procedureCode);
|
||||
if (addonBlock) {
|
||||
const result = addonBlock.callback(util.thread.getAllparams(), util);
|
||||
if (util.thread.status === 1 /* STATUS_PROMISE_WAIT */) {
|
||||
// If the addon block is using STATUS_PROMISE_WAIT to force us to sleep,
|
||||
// make sure to not re-run this block when we resume.
|
||||
stackFrame.executed = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
stackFrame.executed = true;
|
||||
|
||||
if (isReporter) {
|
||||
util.thread.peekStackFrame().waitingReporter = true;
|
||||
// Default return value
|
||||
stackFrame.returnValue = '';
|
||||
}
|
||||
|
||||
util.startProcedure(procedureCode);
|
||||
}
|
||||
|
||||
return (args, util) {
|
||||
util.stopThisScript();
|
||||
// If used outside of a custom block, there may be no stackframe.
|
||||
if (util.thread.peekStackFrame()) {
|
||||
util.stackFrame.returnValue = args.VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
argumentReporterStringNumber (args, util) {
|
||||
const value = util.getParam(args.VALUE);
|
||||
if (value === null) {
|
||||
// tw: support legacy block
|
||||
if (String(args.VALUE).toLowerCase() === 'last key pressed') {
|
||||
return util.ioQuery('keyboard', 'getLastKeyPressed');
|
||||
}
|
||||
// When the parameter is not found in the most recent procedure
|
||||
// call, the default is always 0.
|
||||
return 0;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
argumentReporterBoolean (args, util) {
|
||||
const value = util.getParam(args.VALUE);
|
||||
if (value === null) {
|
||||
// tw: implement is compiled? and is turbowarp?
|
||||
const lowercaseValue = String(args.VALUE).toLowerCase();
|
||||
if (util.target.runtime.compilerOptions.enabled && lowercaseValue === 'is compiled?') {
|
||||
return true;
|
||||
}
|
||||
if (lowercaseValue === 'is turbowarp?') {
|
||||
return true;
|
||||
}
|
||||
// When the parameter is not found in the most recent procedure
|
||||
// call, the default is always 0.
|
||||
return 0;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Scratch3ProcedureBlocks;
|
||||
348
scratch-vm/src/blocks/scratch3_sensing.js
Normal file
348
scratch-vm/src/blocks/scratch3_sensing.js
Normal file
@@ -0,0 +1,348 @@
|
||||
const Cast = require('../util/cast');
|
||||
const Timer = require('../util/timer');
|
||||
const getMonitorIdForBlockWithArgs = require('../util/get-monitor-id');
|
||||
|
||||
class Scratch3SensingBlocks {
|
||||
constructor (runtime) {
|
||||
/**
|
||||
* The runtime instantiating this block package.
|
||||
* @type {Runtime}
|
||||
*/
|
||||
this.runtime = runtime;
|
||||
|
||||
/**
|
||||
* The "answer" block value.
|
||||
* @type {string}
|
||||
*/
|
||||
this._answer = ''; // used by compiler
|
||||
|
||||
/**
|
||||
* The timer utility.
|
||||
* @type {Timer}
|
||||
*/
|
||||
this._timer = new Timer();
|
||||
|
||||
/**
|
||||
* The stored microphone loudness measurement.
|
||||
* @type {number}
|
||||
*/
|
||||
this._cachedLoudness = -1;
|
||||
|
||||
/**
|
||||
* The time of the most recent microphone loudness measurement.
|
||||
* @type {number}
|
||||
*/
|
||||
this._cachedLoudnessTimestamp = 0;
|
||||
|
||||
/**
|
||||
* The list of queued questions and respective `resolve` callbacks.
|
||||
* @type {!Array}
|
||||
*/
|
||||
this._questionList = [];
|
||||
|
||||
this.runtime.on('ANSWER', this._onAnswer.bind(this));
|
||||
this.runtime.on('PROJECT_START', this._resetAnswer.bind(this));
|
||||
this.runtime.on('PROJECT_STOP_ALL', this._clearAllQuestions.bind(this));
|
||||
this.runtime.on('STOP_FOR_TARGET', this._clearTargetQuestions.bind(this));
|
||||
this.runtime.on('RUNTIME_DISPOSED', this._resetAnswer.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the block primitives implemented by this package.
|
||||
* @return {object.<string, Function>} Mapping of opcode to Function.
|
||||
*/
|
||||
getPrimitives () {
|
||||
return {
|
||||
sensing_touchingobject: this.touchingObject,
|
||||
sensing_touchingcolor: this.touchingColor,
|
||||
sensing_coloristouchingcolor: this.colorTouchingColor,
|
||||
sensing_distanceto: this.distanceTo,
|
||||
sensing_timer: this.getTimer,
|
||||
sensing_resettimer: this.resetTimer,
|
||||
sensing_of: this.getAttributeOf,
|
||||
sensing_mousex: this.getMouseX,
|
||||
sensing_mousey: this.getMouseY,
|
||||
sensing_setdragmode: this.setDragMode,
|
||||
sensing_mousedown: this.getMouseDown,
|
||||
sensing_keypressed: this.getKeyPressed,
|
||||
sensing_current: this.current,
|
||||
sensing_dayssince2000: this.daysSince2000,
|
||||
sensing_loudness: this.getLoudness,
|
||||
sensing_loud: this.isLoud,
|
||||
sensing_askandwait: this.askAndWait,
|
||||
sensing_answer: this.getAnswer,
|
||||
sensing_username: this.getUsername,
|
||||
sensing_userid: () => {} // legacy no-op block
|
||||
};
|
||||
}
|
||||
|
||||
getMonitored () {
|
||||
return {
|
||||
sensing_answer: {
|
||||
getId: () => 'answer'
|
||||
},
|
||||
sensing_mousedown: {
|
||||
getId: () => 'mousedown'
|
||||
},
|
||||
sensing_mousex: {
|
||||
getId: () => 'mousex'
|
||||
},
|
||||
sensing_mousey: {
|
||||
getId: () => 'mousey'
|
||||
},
|
||||
sensing_loudness: {
|
||||
getId: () => 'loudness'
|
||||
},
|
||||
sensing_timer: {
|
||||
getId: () => 'timer'
|
||||
},
|
||||
sensing_dayssince2000: {
|
||||
getId: () => 'dayssince2000'
|
||||
},
|
||||
sensing_current: {
|
||||
// This is different from the default toolbox xml id in order to support
|
||||
// importing multiple monitors from the same opcode from sb2 files,
|
||||
// something that is not currently supported in scratch 3.
|
||||
getId: (_, fields) => getMonitorIdForBlockWithArgs('current', fields) // _${param}`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
_onAnswer (answer) {
|
||||
this._answer = answer;
|
||||
const questionObj = this._questionList.shift();
|
||||
if (questionObj) {
|
||||
const [_question, resolve, target, wasVisible, wasStage] = questionObj;
|
||||
// If the target was visible when asked, hide the say bubble unless the target was the stage.
|
||||
if (wasVisible && !wasStage) {
|
||||
this.runtime.emit('SAY', target, 'say', '');
|
||||
}
|
||||
resolve();
|
||||
this._askNextQuestion();
|
||||
}
|
||||
}
|
||||
|
||||
_resetAnswer () {
|
||||
this._answer = '';
|
||||
}
|
||||
|
||||
_enqueueAsk (question, resolve, target, wasVisible, wasStage) {
|
||||
this._questionList.push([question, resolve, target, wasVisible, wasStage]);
|
||||
}
|
||||
|
||||
_askNextQuestion () {
|
||||
if (this._questionList.length > 0) {
|
||||
const [question, _resolve, target, wasVisible, wasStage] = this._questionList[0];
|
||||
// If the target is visible, emit a blank question and use the
|
||||
// say event to trigger a bubble unless the target was the stage.
|
||||
if (wasVisible && !wasStage) {
|
||||
this.runtime.emit('SAY', target, 'say', question);
|
||||
this.runtime.emit('QUESTION', '');
|
||||
} else {
|
||||
this.runtime.emit('QUESTION', question);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_clearAllQuestions () {
|
||||
this._questionList = [];
|
||||
this.runtime.emit('QUESTION', null);
|
||||
}
|
||||
|
||||
_clearTargetQuestions (stopTarget) {
|
||||
const currentlyAsking = this._questionList.length > 0 && this._questionList[0][2] === stopTarget;
|
||||
this._questionList = this._questionList.filter(question => (
|
||||
question[2] !== stopTarget
|
||||
));
|
||||
|
||||
if (currentlyAsking) {
|
||||
this.runtime.emit('SAY', stopTarget, 'say', '');
|
||||
if (this._questionList.length > 0) {
|
||||
this._askNextQuestion();
|
||||
} else {
|
||||
this.runtime.emit('QUESTION', null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
askAndWait (args, util) {
|
||||
const _target = util.target;
|
||||
return new Promise(resolve => {
|
||||
const isQuestionAsked = this._questionList.length > 0;
|
||||
this._enqueueAsk(String(args.QUESTION), resolve, _target, _target.visible, _target.isStage);
|
||||
if (!isQuestionAsked) {
|
||||
this._askNextQuestion();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getAnswer () {
|
||||
return this._answer;
|
||||
}
|
||||
|
||||
touchingObject (args, util) {
|
||||
return util.target.isTouchingObject(args.TOUCHINGOBJECTMENU);
|
||||
}
|
||||
|
||||
touchingColor (args, util) {
|
||||
const color = Cast.toRgbColorList(args.COLOR);
|
||||
return util.target.isTouchingColor(color);
|
||||
}
|
||||
|
||||
colorTouchingColor (args, util) {
|
||||
const maskColor = Cast.toRgbColorList(args.COLOR);
|
||||
const targetColor = Cast.toRgbColorList(args.COLOR2);
|
||||
return util.target.colorIsTouchingColor(targetColor, maskColor);
|
||||
}
|
||||
|
||||
distanceTo (args, util) {
|
||||
if (util.target.isStage) return 10000;
|
||||
|
||||
let targetX = 0;
|
||||
let targetY = 0;
|
||||
if (args.DISTANCETOMENU === '_mouse_') {
|
||||
targetX = util.ioQuery('mouse', 'getScratchX');
|
||||
targetY = util.ioQuery('mouse', 'getScratchY');
|
||||
} else {
|
||||
args.DISTANCETOMENU = Cast.toString(args.DISTANCETOMENU);
|
||||
const distTarget = this.runtime.getSpriteTargetByName(
|
||||
args.DISTANCETOMENU
|
||||
);
|
||||
if (!distTarget) return 10000;
|
||||
targetX = distTarget.x;
|
||||
targetY = distTarget.y;
|
||||
}
|
||||
|
||||
const dx = util.target.x - targetX;
|
||||
const dy = util.target.y - targetY;
|
||||
return Math.sqrt((dx * dx) + (dy * dy));
|
||||
}
|
||||
|
||||
setDragMode (args, util) {
|
||||
util.target.setDraggable(args.DRAG_MODE === 'draggable');
|
||||
}
|
||||
|
||||
getTimer (args, util) {
|
||||
return util.ioQuery('clock', 'projectTimer');
|
||||
}
|
||||
|
||||
resetTimer (args, util) {
|
||||
util.ioQuery('clock', 'resetProjectTimer');
|
||||
}
|
||||
|
||||
getMouseX (args, util) {
|
||||
return util.ioQuery('mouse', 'getScratchX');
|
||||
}
|
||||
|
||||
getMouseY (args, util) {
|
||||
return util.ioQuery('mouse', 'getScratchY');
|
||||
}
|
||||
|
||||
getMouseDown (args, util) {
|
||||
return util.ioQuery('mouse', 'getIsDown');
|
||||
}
|
||||
|
||||
current (args) {
|
||||
const menuOption = Cast.toString(args.CURRENTMENU).toLowerCase();
|
||||
const date = new Date();
|
||||
switch (menuOption) {
|
||||
case 'year': return date.getFullYear();
|
||||
case 'month': return date.getMonth() + 1; // getMonth is zero-based
|
||||
case 'date': return date.getDate();
|
||||
case 'dayofweek': return date.getDay() + 1; // getDay is zero-based, Sun=0
|
||||
case 'hour': return date.getHours();
|
||||
case 'minute': return date.getMinutes();
|
||||
case 'second': return date.getSeconds();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
getKeyPressed (args, util) {
|
||||
return util.ioQuery('keyboard', 'getKeyIsDown', [args.KEY_OPTION]);
|
||||
}
|
||||
|
||||
daysSince2000 () {
|
||||
const msPerDay = 24 * 60 * 60 * 1000;
|
||||
const start = new Date(2000, 0, 1); // Months are 0-indexed.
|
||||
const today = new Date();
|
||||
const dstAdjust = today.getTimezoneOffset() - start.getTimezoneOffset();
|
||||
let mSecsSinceStart = today.valueOf() - start.valueOf();
|
||||
mSecsSinceStart += ((today.getTimezoneOffset() - dstAdjust) * 60 * 1000);
|
||||
return mSecsSinceStart / msPerDay;
|
||||
}
|
||||
|
||||
getLoudness () {
|
||||
if (typeof this.runtime.audioEngine === 'undefined') return -1;
|
||||
if (this.runtime.currentStepTime === null) return -1;
|
||||
|
||||
// Only measure loudness once per step
|
||||
const timeSinceLoudness = this._timer.time() - this._cachedLoudnessTimestamp;
|
||||
if (timeSinceLoudness < this.runtime.currentStepTime) {
|
||||
return this._cachedLoudness;
|
||||
}
|
||||
|
||||
this._cachedLoudnessTimestamp = this._timer.time();
|
||||
this._cachedLoudness = this.runtime.audioEngine.getLoudness();
|
||||
return this._cachedLoudness;
|
||||
}
|
||||
|
||||
isLoud () {
|
||||
return this.getLoudness() > 10;
|
||||
}
|
||||
|
||||
getAttributeOf (args) {
|
||||
let attrTarget;
|
||||
|
||||
if (args.OBJECT === '_stage_') {
|
||||
attrTarget = this.runtime.getTargetForStage();
|
||||
} else {
|
||||
args.OBJECT = Cast.toString(args.OBJECT);
|
||||
attrTarget = this.runtime.getSpriteTargetByName(args.OBJECT);
|
||||
}
|
||||
|
||||
// attrTarget can be undefined if the target does not exist
|
||||
// (e.g. single sprite uploaded from larger project referencing
|
||||
// another sprite that wasn't uploaded)
|
||||
if (!attrTarget) return 0;
|
||||
|
||||
// Generic attributes
|
||||
if (attrTarget.isStage) {
|
||||
switch (args.PROPERTY) {
|
||||
// Scratch 1.4 support
|
||||
case 'background #': return attrTarget.currentCostume + 1;
|
||||
|
||||
case 'backdrop #': return attrTarget.currentCostume + 1;
|
||||
case 'backdrop name':
|
||||
return attrTarget.getCostumes()[attrTarget.currentCostume].name;
|
||||
case 'volume': return attrTarget.volume;
|
||||
}
|
||||
} else {
|
||||
switch (args.PROPERTY) {
|
||||
case 'x position': return attrTarget.x;
|
||||
case 'y position': return attrTarget.y;
|
||||
case 'direction': return attrTarget.direction;
|
||||
case 'costume #': return attrTarget.currentCostume + 1;
|
||||
case 'costume name':
|
||||
return attrTarget.getCostumes()[attrTarget.currentCostume].name;
|
||||
case 'size': return attrTarget.size;
|
||||
case 'volume': return attrTarget.volume;
|
||||
}
|
||||
}
|
||||
|
||||
// Target variables.
|
||||
const varName = args.PROPERTY;
|
||||
const variable = attrTarget.lookupVariableByNameAndType(varName, '', true);
|
||||
if (variable) {
|
||||
return variable.value;
|
||||
}
|
||||
|
||||
// Otherwise, 0
|
||||
return 0;
|
||||
}
|
||||
|
||||
getUsername (args, util) {
|
||||
return util.ioQuery('userData', 'getUsername');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Scratch3SensingBlocks;
|
||||
373
scratch-vm/src/blocks/scratch3_sound.js
Normal file
373
scratch-vm/src/blocks/scratch3_sound.js
Normal file
@@ -0,0 +1,373 @@
|
||||
const MathUtil = require('../util/math-util');
|
||||
const Cast = require('../util/cast');
|
||||
const Clone = require('../util/clone');
|
||||
|
||||
/**
|
||||
* Occluded boolean value to make its use more understandable.
|
||||
* @const {boolean}
|
||||
*/
|
||||
const STORE_WAITING = true;
|
||||
|
||||
class Scratch3SoundBlocks {
|
||||
constructor (runtime) {
|
||||
/**
|
||||
* The runtime instantiating this block package.
|
||||
* @type {Runtime}
|
||||
*/
|
||||
this.runtime = runtime;
|
||||
|
||||
this.waitingSounds = {};
|
||||
|
||||
// Clear sound effects on green flag and stop button events.
|
||||
this.stopAllSounds = this.stopAllSounds.bind(this);
|
||||
this._stopWaitingSoundsForTarget = this._stopWaitingSoundsForTarget.bind(this);
|
||||
this._clearEffectsForAllTargets = this._clearEffectsForAllTargets.bind(this);
|
||||
if (this.runtime) {
|
||||
this.runtime.on('PROJECT_STOP_ALL', this.stopAllSounds);
|
||||
this.runtime.on('PROJECT_STOP_ALL', this._clearEffectsForAllTargets);
|
||||
this.runtime.on('STOP_FOR_TARGET', this._stopWaitingSoundsForTarget);
|
||||
this.runtime.on('PROJECT_START', this._clearEffectsForAllTargets);
|
||||
}
|
||||
|
||||
this._onTargetCreated = this._onTargetCreated.bind(this);
|
||||
if (this.runtime) {
|
||||
runtime.on('targetWasCreated', this._onTargetCreated);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The key to load & store a target's sound-related state.
|
||||
* @type {string}
|
||||
*/
|
||||
static get STATE_KEY () {
|
||||
return 'Scratch.sound';
|
||||
}
|
||||
|
||||
/**
|
||||
* The default sound-related state, to be used when a target has no existing sound state.
|
||||
* @type {SoundState}
|
||||
*/
|
||||
static get DEFAULT_SOUND_STATE () {
|
||||
return {
|
||||
effects: {
|
||||
pitch: 0,
|
||||
pan: 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The minimum and maximum MIDI note numbers, for clamping the input to play note.
|
||||
* @type {{min: number, max: number}}
|
||||
*/
|
||||
static get MIDI_NOTE_RANGE () {
|
||||
return {min: 36, max: 96}; // C2 to C7
|
||||
}
|
||||
|
||||
/**
|
||||
* The minimum and maximum beat values, for clamping the duration of play note, play drum and rest.
|
||||
* 100 beats at the default tempo of 60bpm is 100 seconds.
|
||||
* @type {{min: number, max: number}}
|
||||
*/
|
||||
static get BEAT_RANGE () {
|
||||
return {min: 0, max: 100};
|
||||
}
|
||||
|
||||
/** The minimum and maximum tempo values, in bpm.
|
||||
* @type {{min: number, max: number}}
|
||||
*/
|
||||
static get TEMPO_RANGE () {
|
||||
return {min: 20, max: 500};
|
||||
}
|
||||
|
||||
/** The minimum and maximum values for each sound effect.
|
||||
* @type {{effect:{min: number, max: number}}}
|
||||
*/
|
||||
static get EFFECT_RANGE () {
|
||||
return {
|
||||
pitch: {min: -360, max: 360}, // -3 to 3 octaves
|
||||
pan: {min: -100, max: 100} // 100% left to 100% right
|
||||
};
|
||||
}
|
||||
|
||||
/** The minimum and maximum values for sound effects when miscellaneous limits are removed. */
|
||||
static get LARGER_EFFECT_RANGE () {
|
||||
return {
|
||||
// scratch-audio throws if pitch is too big because some math results in Infinity
|
||||
pitch: {min: -1000, max: 1000},
|
||||
|
||||
// No reason for these to go beyond 100
|
||||
pan: {min: -100, max: 100}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Target} target - collect sound state for this target.
|
||||
* @returns {SoundState} the mutable sound state associated with that target. This will be created if necessary.
|
||||
* @private
|
||||
*/
|
||||
_getSoundState (target) {
|
||||
let soundState = target.getCustomState(Scratch3SoundBlocks.STATE_KEY);
|
||||
if (!soundState) {
|
||||
soundState = Clone.simple(Scratch3SoundBlocks.DEFAULT_SOUND_STATE);
|
||||
target.setCustomState(Scratch3SoundBlocks.STATE_KEY, soundState);
|
||||
target.soundEffects = soundState.effects;
|
||||
}
|
||||
return soundState;
|
||||
}
|
||||
|
||||
/**
|
||||
* When a Target is cloned, clone the sound state.
|
||||
* @param {Target} newTarget - the newly created target.
|
||||
* @param {Target} [sourceTarget] - the target used as a source for the new clone, if any.
|
||||
* @listens Runtime#event:targetWasCreated
|
||||
* @private
|
||||
*/
|
||||
_onTargetCreated (newTarget, sourceTarget) {
|
||||
if (sourceTarget) {
|
||||
const soundState = sourceTarget.getCustomState(Scratch3SoundBlocks.STATE_KEY);
|
||||
if (soundState && newTarget) {
|
||||
newTarget.setCustomState(Scratch3SoundBlocks.STATE_KEY, Clone.simple(soundState));
|
||||
this._syncEffectsForTarget(newTarget);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the block primitives implemented by this package.
|
||||
* @return {object.<string, Function>} Mapping of opcode to Function.
|
||||
*/
|
||||
getPrimitives () {
|
||||
return {
|
||||
sound_play: this.playSound,
|
||||
sound_playuntildone: this.playSoundAndWait,
|
||||
sound_stopallsounds: this.stopAllSounds,
|
||||
sound_seteffectto: this.setEffect,
|
||||
sound_changeeffectby: this.changeEffect,
|
||||
sound_cleareffects: this.clearEffects,
|
||||
sound_sounds_menu: this.soundsMenu,
|
||||
sound_beats_menu: this.beatsMenu,
|
||||
sound_effects_menu: this.effectsMenu,
|
||||
sound_setvolumeto: this.setVolume,
|
||||
sound_changevolumeby: this.changeVolume,
|
||||
sound_volume: this.getVolume
|
||||
};
|
||||
}
|
||||
|
||||
getMonitored () {
|
||||
return {
|
||||
sound_volume: {
|
||||
isSpriteSpecific: true,
|
||||
getId: targetId => `${targetId}_volume`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
playSound (args, util) {
|
||||
// Don't return the promise, it's the only difference for AndWait
|
||||
this._playSound(args, util);
|
||||
}
|
||||
|
||||
playSoundAndWait (args, util) {
|
||||
return this._playSound(args, util, STORE_WAITING);
|
||||
}
|
||||
|
||||
_playSound (args, util, storeWaiting) {
|
||||
const index = this._getSoundIndex(args.SOUND_MENU, util);
|
||||
if (index >= 0) {
|
||||
const {target} = util;
|
||||
const {sprite} = target;
|
||||
const {soundId} = sprite.sounds[index];
|
||||
if (sprite.soundBank) {
|
||||
if (storeWaiting === STORE_WAITING) {
|
||||
this._addWaitingSound(target.id, soundId);
|
||||
} else {
|
||||
this._removeWaitingSound(target.id, soundId);
|
||||
}
|
||||
return sprite.soundBank.playSound(target, soundId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_addWaitingSound (targetId, soundId) {
|
||||
if (!this.waitingSounds[targetId]) {
|
||||
this.waitingSounds[targetId] = new Set();
|
||||
}
|
||||
this.waitingSounds[targetId].add(soundId);
|
||||
}
|
||||
|
||||
_removeWaitingSound (targetId, soundId) {
|
||||
if (!this.waitingSounds[targetId]) {
|
||||
return;
|
||||
}
|
||||
this.waitingSounds[targetId].delete(soundId);
|
||||
}
|
||||
|
||||
_getSoundIndex (soundName, util) {
|
||||
// if the sprite has no sounds, return -1
|
||||
const len = util.target.sprite.sounds.length;
|
||||
if (len === 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// look up by name first
|
||||
const index = this.getSoundIndexByName(soundName, util);
|
||||
if (index !== -1) {
|
||||
return index;
|
||||
}
|
||||
|
||||
// then try using the sound name as a 1-indexed index
|
||||
const oneIndexedIndex = parseInt(soundName, 10);
|
||||
if (!isNaN(oneIndexedIndex)) {
|
||||
return MathUtil.wrapClamp(oneIndexedIndex - 1, 0, len - 1);
|
||||
}
|
||||
|
||||
// could not be found as a name or converted to index, return -1
|
||||
return -1;
|
||||
}
|
||||
|
||||
getSoundIndexByName (soundName, util) {
|
||||
const sounds = util.target.sprite.sounds;
|
||||
for (let i = 0; i < sounds.length; i++) {
|
||||
if (sounds[i].name === soundName) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
// if there is no sound by that name, return -1
|
||||
return -1;
|
||||
}
|
||||
|
||||
stopAllSounds () {
|
||||
if (this.runtime.targets === null) return;
|
||||
const allTargets = this.runtime.targets;
|
||||
for (let i = 0; i < allTargets.length; i++) {
|
||||
this._stopAllSoundsForTarget(allTargets[i]);
|
||||
}
|
||||
}
|
||||
|
||||
_stopAllSoundsForTarget (target) {
|
||||
if (target.sprite.soundBank) {
|
||||
target.sprite.soundBank.stopAllSounds(target);
|
||||
if (this.waitingSounds[target.id]) {
|
||||
this.waitingSounds[target.id].clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_stopWaitingSoundsForTarget (target) {
|
||||
if (target.sprite.soundBank) {
|
||||
if (this.waitingSounds[target.id]) {
|
||||
for (const soundId of this.waitingSounds[target.id].values()) {
|
||||
target.sprite.soundBank.stop(target, soundId);
|
||||
}
|
||||
this.waitingSounds[target.id].clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setEffect (args, util) {
|
||||
return this._updateEffect(args, util, false);
|
||||
}
|
||||
|
||||
changeEffect (args, util) {
|
||||
return this._updateEffect(args, util, true);
|
||||
}
|
||||
|
||||
_updateEffect (args, util, change) {
|
||||
const effect = Cast.toString(args.EFFECT).toLowerCase();
|
||||
const value = Cast.toNumber(args.VALUE);
|
||||
|
||||
const soundState = this._getSoundState(util.target);
|
||||
if (!Object.prototype.hasOwnProperty.call(soundState.effects, effect)) return;
|
||||
|
||||
if (change) {
|
||||
soundState.effects[effect] += value;
|
||||
} else {
|
||||
soundState.effects[effect] = value;
|
||||
}
|
||||
|
||||
const miscLimits = this.runtime.runtimeOptions.miscLimits;
|
||||
const {min, max} = miscLimits ?
|
||||
Scratch3SoundBlocks.EFFECT_RANGE[effect] :
|
||||
Scratch3SoundBlocks.LARGER_EFFECT_RANGE[effect];
|
||||
soundState.effects[effect] = MathUtil.clamp(soundState.effects[effect], min, max);
|
||||
|
||||
this._syncEffectsForTarget(util.target);
|
||||
if (miscLimits) {
|
||||
// Yield until the next tick.
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// Requesting a redraw makes sure that "forever: change pitch by 1" still work but without
|
||||
// yielding unnecessarily in other cases
|
||||
this.runtime.requestRedraw();
|
||||
}
|
||||
|
||||
_syncEffectsForTarget (target) {
|
||||
if (!target || !target.sprite.soundBank) return;
|
||||
target.soundEffects = this._getSoundState(target).effects;
|
||||
|
||||
target.sprite.soundBank.setEffects(target);
|
||||
}
|
||||
|
||||
clearEffects (args, util) {
|
||||
this._clearEffectsForTarget(util.target);
|
||||
}
|
||||
|
||||
_clearEffectsForTarget (target) {
|
||||
const soundState = this._getSoundState(target);
|
||||
for (const effect in soundState.effects) {
|
||||
if (!Object.prototype.hasOwnProperty.call(soundState.effects, effect)) continue;
|
||||
soundState.effects[effect] = 0;
|
||||
}
|
||||
this._syncEffectsForTarget(target);
|
||||
}
|
||||
|
||||
_clearEffectsForAllTargets () {
|
||||
if (this.runtime.targets === null) return;
|
||||
const allTargets = this.runtime.targets;
|
||||
for (let i = 0; i < allTargets.length; i++) {
|
||||
this._clearEffectsForTarget(allTargets[i]);
|
||||
}
|
||||
}
|
||||
|
||||
setVolume (args, util) {
|
||||
const volume = Cast.toNumber(args.VOLUME);
|
||||
return this._updateVolume(volume, util);
|
||||
}
|
||||
|
||||
changeVolume (args, util) {
|
||||
const volume = Cast.toNumber(args.VOLUME) + util.target.volume;
|
||||
return this._updateVolume(volume, util);
|
||||
}
|
||||
|
||||
_updateVolume (volume, util) {
|
||||
volume = MathUtil.clamp(volume, 0, 100);
|
||||
util.target.volume = volume;
|
||||
this._syncEffectsForTarget(util.target);
|
||||
|
||||
if (this.runtime.runtimeOptions.miscLimits) {
|
||||
// Yield until the next tick.
|
||||
return Promise.resolve();
|
||||
}
|
||||
this.runtime.requestRedraw();
|
||||
}
|
||||
|
||||
getVolume (args, util) {
|
||||
return util.target.volume;
|
||||
}
|
||||
|
||||
soundsMenu (args) {
|
||||
return args.SOUND_MENU;
|
||||
}
|
||||
|
||||
beatsMenu (args) {
|
||||
return args.BEATS;
|
||||
}
|
||||
|
||||
effectsMenu (args) {
|
||||
return args.EFFECT;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Scratch3SoundBlocks;
|
||||
Reference in New Issue
Block a user