Variables and lists ()

* 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:
Tim Mickel 2016-09-21 16:38:33 -04:00 committed by GitHub
parent a687184c3c
commit a118d50056
8 changed files with 302 additions and 3 deletions

136
src/blocks/scratch3_data.js Normal file
View 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
View 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;

View file

@ -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')
};
/**

View file

@ -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
View 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;

View file

@ -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);
}
}
}

View file

@ -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;

View file

@ -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;