mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2024-12-24 06:52:40 -05:00
Merge branch 'develop' into timerBlock
This commit is contained in:
commit
2aef75bc11
14 changed files with 432 additions and 61 deletions
53
.travis.yml
53
.travis.yml
|
@ -2,6 +2,9 @@ language: node_js
|
||||||
node_js:
|
node_js:
|
||||||
- 6
|
- 6
|
||||||
- node
|
- node
|
||||||
|
env:
|
||||||
|
- NPM_SCRIPT="tap:unit -- --jobs=4"
|
||||||
|
- NPM_SCRIPT="tap:integration -- --jobs=4"
|
||||||
sudo: false
|
sudo: false
|
||||||
cache:
|
cache:
|
||||||
directories:
|
directories:
|
||||||
|
@ -9,27 +12,29 @@ cache:
|
||||||
install:
|
install:
|
||||||
- npm install
|
- npm install
|
||||||
- npm update
|
- npm update
|
||||||
after_script:
|
script: npm run $NPM_SCRIPT
|
||||||
- |
|
jobs:
|
||||||
# RELEASE_BRANCHES and NPM_TOKEN defined in Travis settings panel
|
include:
|
||||||
declare exitCode
|
- env: NPM_SCRIPT=lint
|
||||||
$(npm bin)/travis-after-all
|
node_js: 6
|
||||||
exitCode=$?
|
- stage: release
|
||||||
if [[
|
node_js: 6
|
||||||
# Execute after all jobs finish successfully
|
env: NPM_SCRIPT=build
|
||||||
$exitCode = 0 &&
|
before_deploy:
|
||||||
# Only release on release branches
|
- npm --no-git-tag-version version $($(npm bin)/json -f package.json version)-prerelease.$(date +%s)
|
||||||
$RELEASE_BRANCHES =~ $TRAVIS_BRANCH &&
|
- git config --global user.email $(git log --pretty=format:"%ae" -n1)
|
||||||
# Don't release on PR builds
|
- git config --global user.name $(git log --pretty=format:"%an" -n1)
|
||||||
$TRAVIS_PULL_REQUEST = "false"
|
deploy:
|
||||||
]]; then
|
- provider: script
|
||||||
# Authenticate NPM
|
"on":
|
||||||
echo "//registry.npmjs.org/:_authToken=\${NPM_TOKEN}" > .npmrc
|
all_branches: true
|
||||||
# Set version to timestamp
|
condition: $RELEASE_BRANCHES =~ $TRAVIS_BRANCH
|
||||||
npm --no-git-tag-version version $($(npm bin)/json -f package.json version)-prerelease.$(date +%s)
|
skip_cleanup: true
|
||||||
npm publish
|
script: npm run --silent deploy -- -x -r $GH_PAGES_REPO
|
||||||
# Publish to gh-pages as most recent committer
|
- provider: npm
|
||||||
git config --global user.email $(git log --pretty=format:"%ae" -n1)
|
"on":
|
||||||
git config --global user.name $(git log --pretty=format:"%an" -n1)
|
all_branches: true
|
||||||
npm run --silent deploy -- -x -r $GH_PAGES_REPO
|
condition: $RELEASE_BRANCHES =~ $TRAVIS_BRANCH
|
||||||
fi
|
skip_cleanup: true
|
||||||
|
email: $NPM_EMAIL
|
||||||
|
api_key: $NPM_TOKEN
|
||||||
|
|
|
@ -15,10 +15,11 @@
|
||||||
"coverage": "./node_modules/.bin/tap ./test/{unit,integration}/*.js --coverage --coverage-report=lcov",
|
"coverage": "./node_modules/.bin/tap ./test/{unit,integration}/*.js --coverage --coverage-report=lcov",
|
||||||
"deploy": "touch playground/.nojekyll && ./node_modules/.bin/gh-pages -t -d playground -m \"Build for $(git log --pretty=format:%H -n1)\"",
|
"deploy": "touch playground/.nojekyll && ./node_modules/.bin/gh-pages -t -d playground -m \"Build for $(git log --pretty=format:%H -n1)\"",
|
||||||
"lint": "./node_modules/.bin/eslint .",
|
"lint": "./node_modules/.bin/eslint .",
|
||||||
"prepublish": "npm run build",
|
"prepublish": "in-publish && npm run build || not-in-publish",
|
||||||
"prepublish-watch": "npm run watch",
|
|
||||||
"start": "./node_modules/.bin/webpack-dev-server",
|
"start": "./node_modules/.bin/webpack-dev-server",
|
||||||
"tap": "./node_modules/.bin/tap ./test/{unit,integration}/*.js",
|
"tap": "./node_modules/.bin/tap ./test/{unit,integration}/*.js",
|
||||||
|
"tap:unit": "./node_modules/.bin/tap ./test/unit/*.js",
|
||||||
|
"tap:integration": "./node_modules/.bin/tap ./test/integration/*.js",
|
||||||
"test": "npm run lint && npm run tap",
|
"test": "npm run lint && npm run tap",
|
||||||
"watch": "./node_modules/.bin/webpack --progress --colors --watch",
|
"watch": "./node_modules/.bin/webpack --progress --colors --watch",
|
||||||
"version": "./node_modules/.bin/json -f package.json -I -e \"this.repository.sha = '$(git log -n1 --pretty=format:%H)'\""
|
"version": "./node_modules/.bin/json -f package.json -I -e \"this.repository.sha = '$(git log -n1 --pretty=format:%H)'\""
|
||||||
|
@ -38,6 +39,7 @@
|
||||||
"highlightjs": "^9.8.0",
|
"highlightjs": "^9.8.0",
|
||||||
"htmlparser2": "3.9.2",
|
"htmlparser2": "3.9.2",
|
||||||
"immutable": "3.8.1",
|
"immutable": "3.8.1",
|
||||||
|
"in-publish": "^2.0.0",
|
||||||
"json": "^9.0.4",
|
"json": "^9.0.4",
|
||||||
"lodash.defaultsdeep": "4.6.0",
|
"lodash.defaultsdeep": "4.6.0",
|
||||||
"minilog": "3.1.0",
|
"minilog": "3.1.0",
|
||||||
|
@ -50,7 +52,6 @@
|
||||||
"socket.io-client": "1.7.3",
|
"socket.io-client": "1.7.3",
|
||||||
"stats.js": "^0.17.0",
|
"stats.js": "^0.17.0",
|
||||||
"tap": "^10.2.0",
|
"tap": "^10.2.0",
|
||||||
"travis-after-all": "^1.4.4",
|
|
||||||
"webpack": "^2.4.1",
|
"webpack": "^2.4.1",
|
||||||
"webpack-dev-server": "^2.4.1"
|
"webpack-dev-server": "^2.4.1"
|
||||||
}
|
}
|
||||||
|
|
|
@ -113,18 +113,18 @@ class Scratch3SoundBlocks {
|
||||||
playSound (args, util) {
|
playSound (args, util) {
|
||||||
const index = this._getSoundIndex(args.SOUND_MENU, util);
|
const index = this._getSoundIndex(args.SOUND_MENU, util);
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
const md5 = util.target.sprite.sounds[index].md5;
|
const soundId = util.target.sprite.sounds[index].soundId;
|
||||||
if (util.target.audioPlayer === null) return;
|
if (util.target.audioPlayer === null) return;
|
||||||
util.target.audioPlayer.playSound(md5);
|
util.target.audioPlayer.playSound(soundId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
playSoundAndWait (args, util) {
|
playSoundAndWait (args, util) {
|
||||||
const index = this._getSoundIndex(args.SOUND_MENU, util);
|
const index = this._getSoundIndex(args.SOUND_MENU, util);
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
const md5 = util.target.sprite.sounds[index].md5;
|
const soundId = util.target.sprite.sounds[index].soundId;
|
||||||
if (util.target.audioPlayer === null) return;
|
if (util.target.audioPlayer === null) return;
|
||||||
return util.target.audioPlayer.playSound(md5);
|
return util.target.audioPlayer.playSound(soundId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,18 +135,20 @@ class Scratch3SoundBlocks {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
let index;
|
// look up by name first
|
||||||
|
const index = this.getSoundIndexByName(soundName, util);
|
||||||
// try to convert to a number and use that as an index
|
if (index !== -1) {
|
||||||
const num = parseInt(soundName, 10);
|
|
||||||
if (!isNaN(num)) {
|
|
||||||
index = MathUtil.wrapClamp(num, 0, len - 1);
|
|
||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
|
|
||||||
// return the index for the sound of that name
|
// then try using the sound name as a 1-indexed index
|
||||||
index = this.getSoundIndexByName(soundName, util);
|
const oneIndexedIndex = parseInt(soundName, 10);
|
||||||
return index;
|
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) {
|
getSoundIndexByName (soundName, util) {
|
||||||
|
@ -181,7 +183,7 @@ class Scratch3SoundBlocks {
|
||||||
let drum = Cast.toNumber(args.DRUM);
|
let drum = Cast.toNumber(args.DRUM);
|
||||||
drum -= 1; // drums are one-indexed
|
drum -= 1; // drums are one-indexed
|
||||||
if (typeof this.runtime.audioEngine === 'undefined') return;
|
if (typeof this.runtime.audioEngine === 'undefined') return;
|
||||||
drum = MathUtil.wrapClamp(drum, 0, this.runtime.audioEngine.numDrums);
|
drum = MathUtil.wrapClamp(drum, 0, this.runtime.audioEngine.numDrums - 1);
|
||||||
let beats = Cast.toNumber(args.BEATS);
|
let beats = Cast.toNumber(args.BEATS);
|
||||||
beats = this._clampBeats(beats);
|
beats = this._clampBeats(beats);
|
||||||
if (util.target.audioPlayer === null) return;
|
if (util.target.audioPlayer === null) return;
|
||||||
|
@ -204,7 +206,7 @@ class Scratch3SoundBlocks {
|
||||||
let instNum = Cast.toNumber(args.INSTRUMENT);
|
let instNum = Cast.toNumber(args.INSTRUMENT);
|
||||||
instNum -= 1; // instruments are one-indexed
|
instNum -= 1; // instruments are one-indexed
|
||||||
if (typeof this.runtime.audioEngine === 'undefined') return;
|
if (typeof this.runtime.audioEngine === 'undefined') return;
|
||||||
instNum = MathUtil.wrapClamp(instNum, 0, this.runtime.audioEngine.numInstruments);
|
instNum = MathUtil.wrapClamp(instNum, 0, this.runtime.audioEngine.numInstruments - 1);
|
||||||
soundState.currentInstrument = instNum;
|
soundState.currentInstrument = instNum;
|
||||||
return this.runtime.audioEngine.instrumentPlayer.loadInstrument(soundState.currentInstrument);
|
return this.runtime.audioEngine.instrumentPlayer.loadInstrument(soundState.currentInstrument);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ const Blocks = require('./blocks');
|
||||||
const Variable = require('../engine/variable');
|
const Variable = require('../engine/variable');
|
||||||
const List = require('../engine/list');
|
const List = require('../engine/list');
|
||||||
const uid = require('../util/uid');
|
const uid = require('../util/uid');
|
||||||
|
const {Map} = require('immutable');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @fileoverview
|
* @fileoverview
|
||||||
|
@ -161,6 +162,21 @@ class Target extends EventEmitter {
|
||||||
const variable = this.variables[id];
|
const variable = this.variables[id];
|
||||||
if (variable.id === id) {
|
if (variable.id === id) {
|
||||||
variable.name = newName;
|
variable.name = newName;
|
||||||
|
|
||||||
|
if (this.runtime) {
|
||||||
|
const blocks = this.runtime.monitorBlocks;
|
||||||
|
blocks.changeBlock({
|
||||||
|
id: id,
|
||||||
|
element: 'field',
|
||||||
|
name: 'VARIABLE',
|
||||||
|
value: id
|
||||||
|
}, this.runtime);
|
||||||
|
this.runtime.requestUpdateMonitor(Map({
|
||||||
|
id: id,
|
||||||
|
params: blocks._getBlockParams(blocks.getBlock(variable.id))
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -172,6 +188,9 @@ class Target extends EventEmitter {
|
||||||
deleteVariable (id) {
|
deleteVariable (id) {
|
||||||
if (this.variables.hasOwnProperty(id)) {
|
if (this.variables.hasOwnProperty(id)) {
|
||||||
delete this.variables[id];
|
delete this.variables[id];
|
||||||
|
if (this.runtime) {
|
||||||
|
this.runtime.requestRemoveMonitor(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,10 @@ const loadSound = function (sound, runtime) {
|
||||||
{data: soundAsset.data}
|
{data: soundAsset.data}
|
||||||
));
|
));
|
||||||
})
|
})
|
||||||
.then(() => sound);
|
.then(soundId => {
|
||||||
|
sound.soundId = soundId;
|
||||||
|
return sound;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = loadSound;
|
module.exports = loadSound;
|
||||||
|
|
|
@ -77,15 +77,16 @@ const flatten = function (blocks) {
|
||||||
* a list of blocks in a branch (e.g., in forever),
|
* a list of blocks in a branch (e.g., in forever),
|
||||||
* or a list of blocks in an argument (e.g., move [pick random...]).
|
* or a list of blocks in an argument (e.g., move [pick random...]).
|
||||||
* @param {Array.<object>} blockList SB2 JSON-format block list.
|
* @param {Array.<object>} blockList SB2 JSON-format block list.
|
||||||
|
* @param {Function} getVariableId function to retreive a variable's ID based on name
|
||||||
* @return {Array.<object>} Scratch VM-format block list.
|
* @return {Array.<object>} Scratch VM-format block list.
|
||||||
*/
|
*/
|
||||||
const parseBlockList = function (blockList) {
|
const parseBlockList = function (blockList, getVariableId) {
|
||||||
const resultingList = [];
|
const resultingList = [];
|
||||||
let previousBlock = null; // For setting next.
|
let previousBlock = null; // For setting next.
|
||||||
for (let i = 0; i < blockList.length; i++) {
|
for (let i = 0; i < blockList.length; i++) {
|
||||||
const block = blockList[i];
|
const block = blockList[i];
|
||||||
// eslint-disable-next-line no-use-before-define
|
// eslint-disable-next-line no-use-before-define
|
||||||
const parsedBlock = parseBlock(block);
|
const parsedBlock = parseBlock(block, getVariableId);
|
||||||
if (typeof parsedBlock === 'undefined') continue;
|
if (typeof parsedBlock === 'undefined') continue;
|
||||||
if (previousBlock) {
|
if (previousBlock) {
|
||||||
parsedBlock.parent = previousBlock.id;
|
parsedBlock.parent = previousBlock.id;
|
||||||
|
@ -102,14 +103,15 @@ const parseBlockList = function (blockList) {
|
||||||
* This should only handle top-level scripts that include X, Y coordinates.
|
* This should only handle top-level scripts that include X, Y coordinates.
|
||||||
* @param {!object} scripts Scripts object from SB2 JSON.
|
* @param {!object} scripts Scripts object from SB2 JSON.
|
||||||
* @param {!Blocks} blocks Blocks object to load parsed blocks into.
|
* @param {!Blocks} blocks Blocks object to load parsed blocks into.
|
||||||
|
* @param {Function} getVariableId function to retreive a variable's ID based on name
|
||||||
*/
|
*/
|
||||||
const parseScripts = function (scripts, blocks) {
|
const parseScripts = function (scripts, blocks, getVariableId) {
|
||||||
for (let i = 0; i < scripts.length; i++) {
|
for (let i = 0; i < scripts.length; i++) {
|
||||||
const script = scripts[i];
|
const script = scripts[i];
|
||||||
const scriptX = script[0];
|
const scriptX = script[0];
|
||||||
const scriptY = script[1];
|
const scriptY = script[1];
|
||||||
const blockList = script[2];
|
const blockList = script[2];
|
||||||
const parsedBlockList = parseBlockList(blockList);
|
const parsedBlockList = parseBlockList(blockList, getVariableId);
|
||||||
if (parsedBlockList[0]) {
|
if (parsedBlockList[0]) {
|
||||||
// Adjust script coordinates to account for
|
// Adjust script coordinates to account for
|
||||||
// larger block size in scratch-blocks.
|
// larger block size in scratch-blocks.
|
||||||
|
@ -127,6 +129,30 @@ const parseScripts = function (scripts, blocks) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a callback for assigning fixed IDs to imported variables
|
||||||
|
* Generator stores the global variable mapping in a closure
|
||||||
|
* @param {!string} targetId the id of the target to scope the variable to
|
||||||
|
* @return {string} variable ID
|
||||||
|
*/
|
||||||
|
const generateVariableIdGetter = (function () {
|
||||||
|
let globalVariableNameMap = {};
|
||||||
|
const namer = (targetId, name) => `${targetId}-${name}`;
|
||||||
|
return function (targetId, topLevel) {
|
||||||
|
// Reset the global variable map if topLevel
|
||||||
|
if (topLevel) globalVariableNameMap = {};
|
||||||
|
return function (name) {
|
||||||
|
if (topLevel) { // Store the name/id pair in the globalVariableNameMap
|
||||||
|
globalVariableNameMap[name] = namer(targetId, name);
|
||||||
|
return globalVariableNameMap[name];
|
||||||
|
}
|
||||||
|
// Not top-level, so first check the global name map
|
||||||
|
if (globalVariableNameMap[name]) return globalVariableNameMap[name];
|
||||||
|
return namer(targetId, name);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}());
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a single "Scratch object" and create all its in-memory VM objects.
|
* Parse a single "Scratch object" and create all its in-memory VM objects.
|
||||||
* @param {!object} object From-JSON "Scratch object:" sprite, stage, watcher.
|
* @param {!object} object From-JSON "Scratch object:" sprite, stage, watcher.
|
||||||
|
@ -180,19 +206,18 @@ const parseScratchObject = function (object, runtime, topLevel) {
|
||||||
soundPromises.push(loadSound(sound, runtime));
|
soundPromises.push(loadSound(sound, runtime));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If included, parse any and all scripts/blocks on the object.
|
|
||||||
if (object.hasOwnProperty('scripts')) {
|
|
||||||
parseScripts(object.scripts, blocks);
|
|
||||||
}
|
|
||||||
// Create the first clone, and load its run-state from JSON.
|
// Create the first clone, and load its run-state from JSON.
|
||||||
const target = sprite.createClone();
|
const target = sprite.createClone();
|
||||||
|
|
||||||
|
const getVariableId = generateVariableIdGetter(target.id, topLevel);
|
||||||
|
|
||||||
// Load target properties from JSON.
|
// Load target properties from JSON.
|
||||||
if (object.hasOwnProperty('variables')) {
|
if (object.hasOwnProperty('variables')) {
|
||||||
for (let j = 0; j < object.variables.length; j++) {
|
for (let j = 0; j < object.variables.length; j++) {
|
||||||
const variable = object.variables[j];
|
const variable = object.variables[j];
|
||||||
const newVariable = new Variable(
|
const newVariable = new Variable(
|
||||||
null,
|
getVariableId(variable.name),
|
||||||
variable.name,
|
variable.name,
|
||||||
variable.value,
|
variable.value,
|
||||||
variable.isPersistent
|
variable.isPersistent
|
||||||
|
@ -200,6 +225,12 @@ const parseScratchObject = function (object, runtime, topLevel) {
|
||||||
target.variables[newVariable.id] = newVariable;
|
target.variables[newVariable.id] = newVariable;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If included, parse any and all scripts/blocks on the object.
|
||||||
|
if (object.hasOwnProperty('scripts')) {
|
||||||
|
parseScripts(object.scripts, blocks, getVariableId);
|
||||||
|
}
|
||||||
|
|
||||||
if (object.hasOwnProperty('lists')) {
|
if (object.hasOwnProperty('lists')) {
|
||||||
for (let k = 0; k < object.lists.length; k++) {
|
for (let k = 0; k < object.lists.length; k++) {
|
||||||
const list = object.lists[k];
|
const list = object.lists[k];
|
||||||
|
@ -294,9 +325,10 @@ const sb2import = function (json, runtime, optForceSprite) {
|
||||||
/**
|
/**
|
||||||
* Parse a single SB2 JSON-formatted block and its children.
|
* Parse a single SB2 JSON-formatted block and its children.
|
||||||
* @param {!object} sb2block SB2 JSON-formatted block.
|
* @param {!object} sb2block SB2 JSON-formatted block.
|
||||||
|
* @param {Function} getVariableId function to retreive a variable's ID based on name
|
||||||
* @return {object} Scratch VM format block.
|
* @return {object} Scratch VM format block.
|
||||||
*/
|
*/
|
||||||
const parseBlock = function (sb2block) {
|
const parseBlock = function (sb2block, getVariableId) {
|
||||||
// First item in block object is the old opcode (e.g., 'forward:').
|
// First item in block object is the old opcode (e.g., 'forward:').
|
||||||
const oldOpcode = sb2block[0];
|
const oldOpcode = sb2block[0];
|
||||||
// Convert the block using the specMap. See sb2specmap.js.
|
// Convert the block using the specMap. See sb2specmap.js.
|
||||||
|
@ -341,10 +373,10 @@ const parseBlock = function (sb2block) {
|
||||||
let innerBlocks;
|
let innerBlocks;
|
||||||
if (typeof providedArg[0] === 'object' && providedArg[0]) {
|
if (typeof providedArg[0] === 'object' && providedArg[0]) {
|
||||||
// Block list occupies the input.
|
// Block list occupies the input.
|
||||||
innerBlocks = parseBlockList(providedArg);
|
innerBlocks = parseBlockList(providedArg, getVariableId);
|
||||||
} else {
|
} else {
|
||||||
// Single block occupies the input.
|
// Single block occupies the input.
|
||||||
innerBlocks = [parseBlock(providedArg)];
|
innerBlocks = [parseBlock(providedArg, getVariableId)];
|
||||||
}
|
}
|
||||||
let previousBlock = null;
|
let previousBlock = null;
|
||||||
for (let j = 0; j < innerBlocks.length; j++) {
|
for (let j = 0; j < innerBlocks.length; j++) {
|
||||||
|
@ -426,6 +458,11 @@ const parseBlock = function (sb2block) {
|
||||||
name: expectedArg.fieldName,
|
name: expectedArg.fieldName,
|
||||||
value: providedArg
|
value: providedArg
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (expectedArg.fieldName === 'VARIABLE') {
|
||||||
|
// Add `id` property to variable fields
|
||||||
|
activeBlock.fields[expectedArg.fieldName].id = getVariableId(providedArg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Special cases to generate mutations.
|
// Special cases to generate mutations.
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
const log = require('../util/log');
|
const log = require('../util/log');
|
||||||
const MathUtil = require('../util/math-util');
|
const MathUtil = require('../util/math-util');
|
||||||
|
const StringUtil = require('../util/string-util');
|
||||||
const Target = require('../engine/target');
|
const Target = require('../engine/target');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -230,6 +231,9 @@ class RenderedTarget extends Target {
|
||||||
if (this.isStage) {
|
if (this.isStage) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!isFinite(direction)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Keep direction between -179 and +180.
|
// Keep direction between -179 and +180.
|
||||||
this.direction = MathUtil.wrapClamp(direction, -179, 180);
|
this.direction = MathUtil.wrapClamp(direction, -179, 180);
|
||||||
if (this.renderer) {
|
if (this.renderer) {
|
||||||
|
@ -391,6 +395,28 @@ class RenderedTarget extends Target {
|
||||||
this.runtime.requestTargetsUpdate(this);
|
this.runtime.requestTargetsUpdate(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a costume, taking care to avoid duplicate names.
|
||||||
|
* @param {!object} costumeObject Object representing the costume.
|
||||||
|
*/
|
||||||
|
addCostume (costumeObject) {
|
||||||
|
const usedNames = this.sprite.costumes.map(costume => costume.name);
|
||||||
|
costumeObject.name = StringUtil.unusedName(costumeObject.name, usedNames);
|
||||||
|
this.sprite.costumes.push(costumeObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rename a costume, taking care to avoid duplicate names.
|
||||||
|
* @param {int} costumeIndex - the index of the costume to be renamed.
|
||||||
|
* @param {string} newName - the desired new name of the costume (will be modified if already in use).
|
||||||
|
*/
|
||||||
|
renameCostume (costumeIndex, newName) {
|
||||||
|
const usedNames = this.sprite.costumes
|
||||||
|
.filter((costume, index) => costumeIndex !== index)
|
||||||
|
.map(costume => costume.name);
|
||||||
|
this.sprite.costumes[costumeIndex].name = StringUtil.unusedName(newName, usedNames);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a costume by index.
|
* Delete a costume by index.
|
||||||
* @param {number} index Costume index to be deleted
|
* @param {number} index Costume index to be deleted
|
||||||
|
@ -414,6 +440,28 @@ class RenderedTarget extends Target {
|
||||||
this.runtime.requestTargetsUpdate(this);
|
this.runtime.requestTargetsUpdate(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a sound, taking care to avoid duplicate names.
|
||||||
|
* @param {!object} soundObject Object representing the sound.
|
||||||
|
*/
|
||||||
|
addSound (soundObject) {
|
||||||
|
const usedNames = this.sprite.sounds.map(sound => sound.name);
|
||||||
|
soundObject.name = StringUtil.unusedName(soundObject.name, usedNames);
|
||||||
|
this.sprite.sounds.push(soundObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rename a sound, taking care to avoid duplicate names.
|
||||||
|
* @param {int} soundIndex - the index of the sound to be renamed.
|
||||||
|
* @param {string} newName - the desired new name of the sound (will be modified if already in use).
|
||||||
|
*/
|
||||||
|
renameSound (soundIndex, newName) {
|
||||||
|
const usedNames = this.sprite.sounds
|
||||||
|
.filter((sound, index) => soundIndex !== index)
|
||||||
|
.map(sound => sound.name);
|
||||||
|
this.sprite.sounds[soundIndex].name = StringUtil.unusedName(newName, usedNames);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a sound by index.
|
* Delete a sound by index.
|
||||||
* @param {number} index Sound index to be deleted
|
* @param {number} index Sound index to be deleted
|
||||||
|
|
|
@ -280,13 +280,23 @@ class VirtualMachine extends EventEmitter {
|
||||||
*/
|
*/
|
||||||
addCostume (md5ext, costumeObject) {
|
addCostume (md5ext, costumeObject) {
|
||||||
loadCostume(md5ext, costumeObject, this.runtime).then(() => {
|
loadCostume(md5ext, costumeObject, this.runtime).then(() => {
|
||||||
this.editingTarget.sprite.costumes.push(costumeObject);
|
this.editingTarget.addCostume(costumeObject);
|
||||||
this.editingTarget.setCostume(
|
this.editingTarget.setCostume(
|
||||||
this.editingTarget.sprite.costumes.length - 1
|
this.editingTarget.sprite.costumes.length - 1
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rename a costume on the current editing target.
|
||||||
|
* @param {int} costumeIndex - the index of the costume to be renamed.
|
||||||
|
* @param {string} newName - the desired new name of the costume (will be modified if already in use).
|
||||||
|
*/
|
||||||
|
renameCostume (costumeIndex, newName) {
|
||||||
|
this.editingTarget.renameCostume(costumeIndex, newName);
|
||||||
|
this.emitTargetsUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a costume from the current editing target.
|
* Delete a costume from the current editing target.
|
||||||
* @param {int} costumeIndex - the index of the costume to be removed.
|
* @param {int} costumeIndex - the index of the costume to be removed.
|
||||||
|
@ -302,11 +312,47 @@ class VirtualMachine extends EventEmitter {
|
||||||
*/
|
*/
|
||||||
addSound (soundObject) {
|
addSound (soundObject) {
|
||||||
return loadSound(soundObject, this.runtime).then(() => {
|
return loadSound(soundObject, this.runtime).then(() => {
|
||||||
this.editingTarget.sprite.sounds.push(soundObject);
|
this.editingTarget.addSound(soundObject);
|
||||||
this.emitTargetsUpdate();
|
this.emitTargetsUpdate();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rename a sound on the current editing target.
|
||||||
|
* @param {int} soundIndex - the index of the sound to be renamed.
|
||||||
|
* @param {string} newName - the desired new name of the sound (will be modified if already in use).
|
||||||
|
*/
|
||||||
|
renameSound (soundIndex, newName) {
|
||||||
|
this.editingTarget.renameSound(soundIndex, newName);
|
||||||
|
this.emitTargetsUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a sound buffer from the audio engine.
|
||||||
|
* @param {int} soundIndex - the index of the sound to be got.
|
||||||
|
* @return {AudioBuffer} the sound's audio buffer.
|
||||||
|
*/
|
||||||
|
getSoundBuffer (soundIndex) {
|
||||||
|
const id = this.editingTarget.sprite.sounds[soundIndex].soundId;
|
||||||
|
if (id && this.runtime && this.runtime.audioEngine) {
|
||||||
|
return this.runtime.audioEngine.getSoundBuffer(id);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a sound buffer.
|
||||||
|
* @param {int} soundIndex - the index of the sound to be updated.
|
||||||
|
* @param {AudioBuffer} newBuffer - new audio buffer for the audio engine.
|
||||||
|
*/
|
||||||
|
updateSoundBuffer (soundIndex, newBuffer) {
|
||||||
|
const id = this.editingTarget.sprite.sounds[soundIndex].soundId;
|
||||||
|
if (id && this.runtime && this.runtime.audioEngine) {
|
||||||
|
this.runtime.audioEngine.updateSoundBuffer(id, newBuffer);
|
||||||
|
}
|
||||||
|
this.emitTargetsUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a sound from the current editing target.
|
* Delete a sound from the current editing target.
|
||||||
* @param {int} soundIndex - the index of the sound to be removed.
|
* @param {int} soundIndex - the index of the sound to be removed.
|
||||||
|
@ -519,8 +565,11 @@ class VirtualMachine extends EventEmitter {
|
||||||
* of the current editing target's blocks.
|
* of the current editing target's blocks.
|
||||||
*/
|
*/
|
||||||
emitWorkspaceUpdate () {
|
emitWorkspaceUpdate () {
|
||||||
// @todo Include variables scoped to editing target also.
|
const variableMap = Object.assign({},
|
||||||
const variableMap = this.runtime.getTargetForStage().variables;
|
this.runtime.getTargetForStage().variables,
|
||||||
|
this.editingTarget.variables
|
||||||
|
);
|
||||||
|
|
||||||
const variables = Object.keys(variableMap).map(k => variableMap[k]);
|
const variables = Object.keys(variableMap).map(k => variableMap[k]);
|
||||||
|
|
||||||
const xmlString = `<xml xmlns="http://www.w3.org/1999/xhtml">
|
const xmlString = `<xml xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
|
BIN
test/fixtures/data.sb2
vendored
BIN
test/fixtures/data.sb2
vendored
Binary file not shown.
|
@ -43,7 +43,7 @@ test('complex', t => {
|
||||||
vm.addCostume(
|
vm.addCostume(
|
||||||
'f9a1c175dbe2e5dee472858dd30d16bb.svg',
|
'f9a1c175dbe2e5dee472858dd30d16bb.svg',
|
||||||
{
|
{
|
||||||
costumeName: 'costume1',
|
name: 'costume1',
|
||||||
baseLayerID: 0,
|
baseLayerID: 0,
|
||||||
baseLayerMD5: 'f9a1c175dbe2e5dee472858dd30d16bb.svg',
|
baseLayerMD5: 'f9a1c175dbe2e5dee472858dd30d16bb.svg',
|
||||||
bitmapResolution: 1,
|
bitmapResolution: 1,
|
||||||
|
@ -79,7 +79,7 @@ test('complex', t => {
|
||||||
vm.addBackdrop(
|
vm.addBackdrop(
|
||||||
'6b3d87ba2a7f89be703163b6c1d4c964.png',
|
'6b3d87ba2a7f89be703163b6c1d4c964.png',
|
||||||
{
|
{
|
||||||
costumeName: 'baseball-field',
|
name: 'baseball-field',
|
||||||
baseLayerID: 26,
|
baseLayerID: 26,
|
||||||
baseLayerMD5: '6b3d87ba2a7f89be703163b6c1d4c964.png',
|
baseLayerMD5: '6b3d87ba2a7f89be703163b6c1d4c964.png',
|
||||||
bitmapResolution: 2,
|
bitmapResolution: 2,
|
||||||
|
|
111
test/unit/blocks_sounds.js
Normal file
111
test/unit/blocks_sounds.js
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
const test = require('tap').test;
|
||||||
|
const Sound = require('../../src/blocks/scratch3_sound');
|
||||||
|
let playedSound;
|
||||||
|
let playedDrum;
|
||||||
|
let playedInstrument;
|
||||||
|
const runtime = {
|
||||||
|
audioEngine: {
|
||||||
|
numDrums: 3,
|
||||||
|
numInstruments: 3,
|
||||||
|
instrumentPlayer: {
|
||||||
|
loadInstrument: instrument => (playedInstrument = instrument)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const blocks = new Sound(runtime);
|
||||||
|
const util = {
|
||||||
|
target: {
|
||||||
|
sprite: {
|
||||||
|
sounds: [
|
||||||
|
{name: 'first name', soundId: 'first soundId'},
|
||||||
|
{name: 'second name', soundId: 'second soundId'},
|
||||||
|
{name: 'third name', soundId: 'third soundId'},
|
||||||
|
{name: '6', soundId: 'fourth soundId'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
audioPlayer: {
|
||||||
|
playSound: soundId => (playedSound = soundId),
|
||||||
|
playDrumForBeats: drum => (playedDrum = drum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
test('playSound with a name string works', t => {
|
||||||
|
const args = {SOUND_MENU: 'second name'};
|
||||||
|
blocks.playSound(args, util);
|
||||||
|
t.strictEqual(playedSound, 'second soundId');
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('playSound with a number string works 1-indexed', t => {
|
||||||
|
let args = {SOUND_MENU: '5'};
|
||||||
|
blocks.playSound(args, util);
|
||||||
|
t.strictEqual(playedSound, 'first soundId');
|
||||||
|
|
||||||
|
args = {SOUND_MENU: '1'};
|
||||||
|
blocks.playSound(args, util);
|
||||||
|
t.strictEqual(playedSound, 'first soundId');
|
||||||
|
|
||||||
|
args = {SOUND_MENU: '0'};
|
||||||
|
blocks.playSound(args, util);
|
||||||
|
t.strictEqual(playedSound, 'fourth soundId');
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('playSound with a number works 1-indexed', t => {
|
||||||
|
let args = {SOUND_MENU: 5};
|
||||||
|
blocks.playSound(args, util);
|
||||||
|
t.strictEqual(playedSound, 'first soundId');
|
||||||
|
|
||||||
|
args = {SOUND_MENU: 1};
|
||||||
|
blocks.playSound(args, util);
|
||||||
|
t.strictEqual(playedSound, 'first soundId');
|
||||||
|
|
||||||
|
args = {SOUND_MENU: 0};
|
||||||
|
blocks.playSound(args, util);
|
||||||
|
t.strictEqual(playedSound, 'fourth soundId');
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('playSound prioritizes sound index if given a number', t => {
|
||||||
|
const args = {SOUND_MENU: 6};
|
||||||
|
blocks.playSound(args, util);
|
||||||
|
// Ignore the sound named '6', wrapClamp to the second instead
|
||||||
|
t.strictEqual(playedSound, 'second soundId');
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('playSound prioritizes sound name if given a string', t => {
|
||||||
|
const args = {SOUND_MENU: '6'};
|
||||||
|
blocks.playSound(args, util);
|
||||||
|
// Use the sound named '6', which is the fourth
|
||||||
|
t.strictEqual(playedSound, 'fourth soundId');
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('playDrum uses 1-indexing and wrap clamps', t => {
|
||||||
|
let args = {DRUM: 1};
|
||||||
|
blocks.playDrumForBeats(args, util);
|
||||||
|
t.strictEqual(playedDrum, 0);
|
||||||
|
|
||||||
|
args = {DRUM: runtime.audioEngine.numDrums + 1};
|
||||||
|
blocks.playDrumForBeats(args, util);
|
||||||
|
t.strictEqual(playedDrum, 0);
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('setInstrument uses 1-indexing and wrap clamps', t => {
|
||||||
|
// Stub getSoundState
|
||||||
|
blocks._getSoundState = () => ({});
|
||||||
|
|
||||||
|
let args = {INSTRUMENT: 1};
|
||||||
|
blocks.setInstrument(args, util);
|
||||||
|
t.strictEqual(playedInstrument, 0);
|
||||||
|
|
||||||
|
args = {INSTRUMENT: runtime.audioEngine.numInstruments + 1};
|
||||||
|
blocks.setInstrument(args, util);
|
||||||
|
t.strictEqual(playedInstrument, 0);
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
|
@ -51,3 +51,20 @@ test('default', t => {
|
||||||
t.end();
|
t.end();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('data scoping', t => {
|
||||||
|
// Get SB2 JSON (string)
|
||||||
|
const uri = path.resolve(__dirname, '../fixtures/data.sb2');
|
||||||
|
const file = extract(uri);
|
||||||
|
const json = JSON.parse(file);
|
||||||
|
|
||||||
|
// Create runtime instance & load SB2 into it
|
||||||
|
const rt = new Runtime();
|
||||||
|
sb2.deserialize(json, rt).then(targets => {
|
||||||
|
const globalVariableIds = Object.keys(targets[0].variables);
|
||||||
|
const localVariableIds = Object.keys(targets[1].variables);
|
||||||
|
t.equal(targets[0].variables[globalVariableIds[0]].name, 'foo');
|
||||||
|
t.equal(targets[1].variables[localVariableIds[0]].name, 'local');
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -355,3 +355,43 @@ test('#toJSON returns the sounds and costumes', t => {
|
||||||
t.same(a.toJSON().costumes, costumes);
|
t.same(a.toJSON().costumes, costumes);
|
||||||
t.end();
|
t.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('#addSound does not duplicate names', t => {
|
||||||
|
const spr = new Sprite();
|
||||||
|
const a = new RenderedTarget(spr, null);
|
||||||
|
a.sprite.sounds = [{name: 'first'}];
|
||||||
|
a.addSound({name: 'first'});
|
||||||
|
t.deepEqual(a.sprite.sounds, [{name: 'first'}, {name: 'first2'}]);
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('#addCostume does not duplicate names', t => {
|
||||||
|
const spr = new Sprite();
|
||||||
|
const a = new RenderedTarget(spr, null);
|
||||||
|
a.sprite.costumes = [{name: 'first'}];
|
||||||
|
a.addCostume({name: 'first'});
|
||||||
|
t.deepEqual(a.sprite.costumes, [{name: 'first'}, {name: 'first2'}]);
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('#renameSound does not duplicate names', t => {
|
||||||
|
const spr = new Sprite();
|
||||||
|
const a = new RenderedTarget(spr, null);
|
||||||
|
a.sprite.sounds = [{name: 'first'}, {name: 'second'}];
|
||||||
|
a.renameSound(0, 'first'); // Shouldn't increment the name, noop
|
||||||
|
t.deepEqual(a.sprite.sounds, [{name: 'first'}, {name: 'second'}]);
|
||||||
|
a.renameSound(1, 'first');
|
||||||
|
t.deepEqual(a.sprite.sounds, [{name: 'first'}, {name: 'first2'}]);
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('#renameCostume does not duplicate names', t => {
|
||||||
|
const spr = new Sprite();
|
||||||
|
const a = new RenderedTarget(spr, null);
|
||||||
|
a.sprite.costumes = [{name: 'first'}, {name: 'second'}];
|
||||||
|
a.renameCostume(0, 'first'); // Shouldn't increment the name, noop
|
||||||
|
t.deepEqual(a.sprite.costumes, [{name: 'first'}, {name: 'second'}]);
|
||||||
|
a.renameCostume(1, 'first');
|
||||||
|
t.deepEqual(a.sprite.costumes, [{name: 'first'}, {name: 'first2'}]);
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
|
@ -109,3 +109,42 @@ test('renameSprite does not increment when renaming to the same name', t => {
|
||||||
t.equal(vm.runtime.targets[0].sprite.name, 'this name');
|
t.equal(vm.runtime.targets[0].sprite.name, 'this name');
|
||||||
t.end();
|
t.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('emitWorkspaceUpdate', t => {
|
||||||
|
const vm = new VirtualMachine();
|
||||||
|
vm.runtime.targets = [
|
||||||
|
{
|
||||||
|
isStage: true,
|
||||||
|
variables: {
|
||||||
|
global: {
|
||||||
|
toXML: () => 'global'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
variables: {
|
||||||
|
unused: {
|
||||||
|
toXML: () => 'unused'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
variables: {
|
||||||
|
local: {
|
||||||
|
toXML: () => 'local'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
blocks: {
|
||||||
|
toXML: () => 'blocks'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
vm.editingTarget = vm.runtime.targets[2];
|
||||||
|
|
||||||
|
let xml = null;
|
||||||
|
vm.emit = (event, data) => (xml = data.xml);
|
||||||
|
vm.emitWorkspaceUpdate();
|
||||||
|
t.notEqual(xml.indexOf('global'), -1);
|
||||||
|
t.notEqual(xml.indexOf('local'), -1);
|
||||||
|
t.equal(xml.indexOf('unused'), -1);
|
||||||
|
t.notEqual(xml.indexOf('blocks'), -1);
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
Loading…
Reference in a new issue