mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2025-06-09 03:54:44 -04:00
Variables and lists (#187)
* Import lists and variables from SB2 * Switch to Variable and List objects * Add Clone.lookupOrCreateVariable, Clone.getVariable, Clone.setVariable * Add (get, set, change) variable blocks. * Copy variables and lists on clone instantiation * Move variable options closer to blocks * Add list primitives * Move variable and lists storage to `Target` instead of `Clone` * Move _computeIndex to a Cast function * Rename `getList` -> `getListAsString` * Renames renames * Remove extra check in Cast.isNaN
This commit is contained in:
parent
a687184c3c
commit
a118d50056
8 changed files with 302 additions and 3 deletions
src
blocks
engine
import
sprites
util
136
src/blocks/scratch3_data.js
Normal file
136
src/blocks/scratch3_data.js
Normal file
|
@ -0,0 +1,136 @@
|
|||
var Cast = require('../util/cast');
|
||||
|
||||
function Scratch3DataBlocks(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.
|
||||
*/
|
||||
Scratch3DataBlocks.prototype.getPrimitives = function () {
|
||||
return {
|
||||
'data_variable': this.getVariable,
|
||||
'data_setvariableto': this.setVariableTo,
|
||||
'data_changevariableby': this.changeVariableBy,
|
||||
'data_list': this.getListContents,
|
||||
'data_addtolist': this.addToList,
|
||||
'data_deleteoflist': this.deleteOfList,
|
||||
'data_insertatlist': this.insertAtList,
|
||||
'data_replaceitemoflist': this.replaceItemOfList,
|
||||
'data_itemoflist': this.getItemOfList,
|
||||
'data_lengthoflist': this.lengthOfList,
|
||||
'data_listcontainsitem': this.listContainsItem
|
||||
};
|
||||
};
|
||||
|
||||
Scratch3DataBlocks.prototype.getVariable = function (args, util) {
|
||||
var variable = util.target.lookupOrCreateVariable(args.VARIABLE);
|
||||
return variable.value;
|
||||
};
|
||||
|
||||
Scratch3DataBlocks.prototype.setVariableTo = function (args, util) {
|
||||
var variable = util.target.lookupOrCreateVariable(args.VARIABLE);
|
||||
variable.value = args.VALUE;
|
||||
};
|
||||
|
||||
Scratch3DataBlocks.prototype.changeVariableBy = function (args, util) {
|
||||
var variable = util.target.lookupOrCreateVariable(args.VARIABLE);
|
||||
var castedValue = Cast.toNumber(variable.value);
|
||||
var dValue = Cast.toNumber(args.VALUE);
|
||||
variable.value = castedValue + dValue;
|
||||
};
|
||||
|
||||
Scratch3DataBlocks.prototype.getListContents = function (args, util) {
|
||||
var list = util.target.lookupOrCreateList(args.LIST);
|
||||
// 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.
|
||||
var allSingleLetters = true;
|
||||
for (var i = 0; i < list.contents.length; i++) {
|
||||
var listItem = list.contents[i];
|
||||
if (!((typeof listItem === 'string') &&
|
||||
(listItem.length == 1))) {
|
||||
allSingleLetters = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (allSingleLetters) {
|
||||
return list.contents.join('');
|
||||
} else {
|
||||
return list.contents.join(' ');
|
||||
}
|
||||
};
|
||||
|
||||
Scratch3DataBlocks.prototype.addToList = function (args, util) {
|
||||
var list = util.target.lookupOrCreateList(args.LIST);
|
||||
list.contents.push(args.ITEM);
|
||||
};
|
||||
|
||||
Scratch3DataBlocks.prototype.deleteOfList = function (args, util) {
|
||||
var list = util.target.lookupOrCreateList(args.LIST);
|
||||
var index = Cast.toListIndex(args.INDEX, list.contents.length, true);
|
||||
if (index === Cast.LIST_INVALID) {
|
||||
return;
|
||||
} else if (index === Cast.LIST_ALL) {
|
||||
list.contents = [];
|
||||
return;
|
||||
}
|
||||
list.contents.splice(index - 1, 1);
|
||||
};
|
||||
|
||||
Scratch3DataBlocks.prototype.insertAtList = function (args, util) {
|
||||
var item = args.ITEM;
|
||||
var list = util.target.lookupOrCreateList(args.LIST);
|
||||
var index = Cast.toListIndex(args.INDEX, list.contents.length + 1);
|
||||
if (index === Cast.LIST_INVALID) {
|
||||
return;
|
||||
}
|
||||
list.contents.splice(index - 1, 0, item);
|
||||
};
|
||||
|
||||
Scratch3DataBlocks.prototype.replaceItemOfList = function (args, util) {
|
||||
var item = args.ITEM;
|
||||
var list = util.target.lookupOrCreateList(args.LIST);
|
||||
var index = Cast.toListIndex(args.INDEX, list.contents.length);
|
||||
if (index === Cast.LIST_INVALID) {
|
||||
return;
|
||||
}
|
||||
list.contents.splice(index - 1, 1, item);
|
||||
};
|
||||
|
||||
Scratch3DataBlocks.prototype.getItemOfList = function (args, util) {
|
||||
var list = util.target.lookupOrCreateList(args.LIST);
|
||||
var index = Cast.toListIndex(args.INDEX, list.contents.length);
|
||||
if (index === Cast.LIST_INVALID) {
|
||||
return '';
|
||||
}
|
||||
return list.contents[index - 1];
|
||||
};
|
||||
|
||||
Scratch3DataBlocks.prototype.lengthOfList = function (args, util) {
|
||||
var list = util.target.lookupOrCreateList(args.LIST);
|
||||
return list.contents.length;
|
||||
};
|
||||
|
||||
Scratch3DataBlocks.prototype.listContainsItem = function (args, util) {
|
||||
var item = args.ITEM;
|
||||
var list = util.target.lookupOrCreateList(args.LIST);
|
||||
if (list.contents.indexOf(item) >= 0) {
|
||||
return true;
|
||||
}
|
||||
// Try using Scratch comparison operator on each item.
|
||||
// (Scratch considers the string '123' equal to the number 123).
|
||||
for (var i = 0; i < list.contents.length; i++) {
|
||||
if (Cast.compare(list.contents[i], item) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
module.exports = Scratch3DataBlocks;
|
16
src/engine/list.js
Normal file
16
src/engine/list.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
/**
|
||||
* @fileoverview
|
||||
* Object representing a Scratch list.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {!string} name Name of the list.
|
||||
* @param {Array} contents Contents of the list, as an array.
|
||||
* @constructor
|
||||
*/
|
||||
function List (name, contents) {
|
||||
this.name = name;
|
||||
this.contents = contents;
|
||||
}
|
||||
|
||||
module.exports = List;
|
|
@ -14,7 +14,8 @@ var defaultBlockPackages = {
|
|||
'scratch3_looks': require('../blocks/scratch3_looks'),
|
||||
'scratch3_motion': require('../blocks/scratch3_motion'),
|
||||
'scratch3_operators': require('../blocks/scratch3_operators'),
|
||||
'scratch3_sensing': require('../blocks/scratch3_sensing')
|
||||
'scratch3_sensing': require('../blocks/scratch3_sensing'),
|
||||
'scratch3_data': require('../blocks/scratch3_data')
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
var Blocks = require('./blocks');
|
||||
var Variable = require('../engine/variable');
|
||||
var List = require('../engine/list');
|
||||
var uid = require('../util/uid');
|
||||
|
||||
/**
|
||||
|
@ -25,6 +27,18 @@ function Target (blocks) {
|
|||
* @type {!Blocks}
|
||||
*/
|
||||
this.blocks = blocks;
|
||||
/**
|
||||
* Dictionary of variables and their values for this target.
|
||||
* Key is the variable name.
|
||||
* @type {Object.<string,*>}
|
||||
*/
|
||||
this.variables = {};
|
||||
/**
|
||||
* Dictionary of lists and their contents for this target.
|
||||
* Key is the list name.
|
||||
* @type {Object.<string,*>}
|
||||
*/
|
||||
this.lists = {};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -43,6 +57,54 @@ Target.prototype.getName = function () {
|
|||
return this.id;
|
||||
};
|
||||
|
||||
/**
|
||||
* Look up a variable object, and create it if one doesn't exist.
|
||||
* Search begins for local variables; then look for globals.
|
||||
* @param {!string} name Name of the variable.
|
||||
* @return {!Variable} Variable object.
|
||||
*/
|
||||
Target.prototype.lookupOrCreateVariable = function (name) {
|
||||
// If we have a local copy, return it.
|
||||
if (this.variables.hasOwnProperty(name)) {
|
||||
return this.variables[name];
|
||||
}
|
||||
// If the stage has a global copy, return it.
|
||||
if (this.runtime && !this.isStage) {
|
||||
var stage = this.runtime.getTargetForStage();
|
||||
if (stage.variables.hasOwnProperty(name)) {
|
||||
return stage.variables[name];
|
||||
}
|
||||
}
|
||||
// No variable with this name exists - create it locally.
|
||||
var newVariable = new Variable(name, 0, false);
|
||||
this.variables[name] = newVariable;
|
||||
return newVariable;
|
||||
};
|
||||
|
||||
/**
|
||||
* Look up a list object for this target, and create it if one doesn't exist.
|
||||
* Search begins for local lists; then look for globals.
|
||||
* @param {!string} name Name of the list.
|
||||
* @return {!List} List object.
|
||||
*/
|
||||
Target.prototype.lookupOrCreateList = function (name) {
|
||||
// If we have a local copy, return it.
|
||||
if (this.lists.hasOwnProperty(name)) {
|
||||
return this.lists[name];
|
||||
}
|
||||
// If the stage has a global copy, return it.
|
||||
if (this.runtime && !this.isStage) {
|
||||
var stage = this.runtime.getTargetForStage();
|
||||
if (stage.lists.hasOwnProperty(name)) {
|
||||
return stage.lists[name];
|
||||
}
|
||||
}
|
||||
// No list with this name exists - create it locally.
|
||||
var newList = new List(name, []);
|
||||
this.lists[name] = newList;
|
||||
return newList;
|
||||
};
|
||||
|
||||
/**
|
||||
* Call to destroy a target.
|
||||
* @abstract
|
||||
|
|
18
src/engine/variable.js
Normal file
18
src/engine/variable.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* @fileoverview
|
||||
* Object representing a Scratch variable.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {!string} name Name of the variable.
|
||||
* @param {(string|Number)} value Value of the variable.
|
||||
* @param {boolean} isCloud Whether the variable is stored in the cloud.
|
||||
* @constructor
|
||||
*/
|
||||
function Variable (name, value, isCloud) {
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
this.isCloud = isCloud;
|
||||
}
|
||||
|
||||
module.exports = Variable;
|
|
@ -10,6 +10,8 @@ var Sprite = require('../sprites/sprite');
|
|||
var Color = require('../util/color.js');
|
||||
var uid = require('../util/uid');
|
||||
var specMap = require('./sb2specmap');
|
||||
var Variable = require('../engine/variable');
|
||||
var List = require('../engine/list');
|
||||
|
||||
/**
|
||||
* Top-level handler. Parse provided JSON,
|
||||
|
@ -68,6 +70,27 @@ function parseScratchObject (object, runtime, topLevel) {
|
|||
var target = sprite.createClone();
|
||||
// Add it to the runtime's list of targets.
|
||||
runtime.targets.push(target);
|
||||
// Load target properties from JSON.
|
||||
if (object.hasOwnProperty('variables')) {
|
||||
for (var j = 0; j < object.variables.length; j++) {
|
||||
var variable = object.variables[j];
|
||||
target.variables[variable.name] = new Variable(
|
||||
variable.name,
|
||||
variable.value,
|
||||
variable.isPersistent
|
||||
);
|
||||
}
|
||||
}
|
||||
if (object.hasOwnProperty('lists')) {
|
||||
for (var k = 0; k < object.lists.length; k++) {
|
||||
var list = object.lists[k];
|
||||
// @todo: monitor properties.
|
||||
target.lists[list.listName] = new List(
|
||||
list.listName,
|
||||
list.contents
|
||||
);
|
||||
}
|
||||
}
|
||||
if (object.hasOwnProperty('scratchX')) {
|
||||
target.x = object.scratchX;
|
||||
}
|
||||
|
@ -91,8 +114,8 @@ function parseScratchObject (object, runtime, topLevel) {
|
|||
target.updateAllDrawableProperties();
|
||||
// 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, false);
|
||||
for (var m = 0; m < object.children.length; m++) {
|
||||
parseScratchObject(object.children[m], runtime, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -327,6 +327,8 @@ Clone.prototype.makeClone = function () {
|
|||
newClone.size = this.size;
|
||||
newClone.currentCostume = this.currentCostume;
|
||||
newClone.effects = JSON.parse(JSON.stringify(this.effects));
|
||||
newClone.variables = JSON.parse(JSON.stringify(this.variables));
|
||||
newClone.lists = JSON.parse(JSON.stringify(this.lists));
|
||||
newClone.initDrawable();
|
||||
newClone.updateAllDrawableProperties();
|
||||
return newClone;
|
||||
|
|
|
@ -125,4 +125,45 @@ Cast.isInt = function (val) {
|
|||
return false;
|
||||
};
|
||||
|
||||
Cast.LIST_INVALID = 'INVALID';
|
||||
Cast.LIST_ALL = 'ALL';
|
||||
/**
|
||||
* Compute a 1-based index into a list, based on a Scratch argument.
|
||||
* Two special cases may be returned:
|
||||
* LIST_ALL: if the block is referring to all of the items in the list.
|
||||
* LIST_INVALID: if the index was invalid in any way.
|
||||
* @param {*} index Scratch arg, including 1-based numbers or special cases.
|
||||
* @param {number} length Length of the list.
|
||||
* @param {boolean} useRound If set, Math.round (not Math.floor for 2.0 compat).
|
||||
* @return {(number|string)} 1-based index for list, LIST_ALL, or LIST_INVALID.
|
||||
*/
|
||||
Cast.toListIndex = function (
|
||||
index, length, useRound) {
|
||||
if (typeof index !== 'number') {
|
||||
if (index == 'all') {
|
||||
return Cast.LIST_ALL;
|
||||
}
|
||||
if (index == 'last') {
|
||||
if (length > 0) {
|
||||
return length;
|
||||
}
|
||||
return Cast.LIST_INVALID;
|
||||
} else if (index == 'random' || index == 'any') {
|
||||
if (length > 0) {
|
||||
return 1 + Math.floor(Math.random() * length);
|
||||
}
|
||||
return Cast.LIST_INVALID;
|
||||
}
|
||||
}
|
||||
if (useRound) {
|
||||
index = Math.round(Cast.toNumber(index));
|
||||
} else {
|
||||
index = Math.floor(Cast.toNumber(index));
|
||||
}
|
||||
if (index < 1 || index > length) {
|
||||
return Cast.LIST_INVALID;
|
||||
}
|
||||
return index;
|
||||
};
|
||||
|
||||
module.exports = Cast;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue