Merge branch 'develop' of https://github.com/LLK/scratch-vm into sound

# Conflicts:
#	package.json
#	src/engine/runtime.js
#	src/sprites/clone.js
This commit is contained in:
Eric Rosenbaum 2016-12-21 15:18:38 -05:00
commit f2793a1d59
59 changed files with 3405 additions and 2341 deletions

7
.eslintignore Normal file
View file

@ -0,0 +1,7 @@
build/*
dist.js
node_modules/*
playground/*
vm.js
vm.min.js
coverage/*

View file

@ -1,20 +0,0 @@
{
"rules": {
"curly": [2, "multi-line"],
"eol-last": [2],
"indent": [2, 4],
"quotes": [2, "single"],
"linebreak-style": [2, "unix"],
"max-len": [2, 80, 4],
"semi": [2, "always"],
"strict": [2, "never"],
"no-console": [2, {"allow": ["log", "warn", "error", "groupCollapsed", "groupEnd", "time", "timeEnd"]}],
"valid-jsdoc": ["error", {"requireReturn": false}]
},
"env": {
"node": true,
"browser": true,
"worker": true
},
"extends": "eslint:recommended"
}

3
.eslintrc.js Normal file
View file

@ -0,0 +1,3 @@
module.exports = {
extends: ['scratch', 'scratch/node']
};

32
.gitattributes vendored Normal file
View file

@ -0,0 +1,32 @@
# Set the default behavior, in case people don't have core.autocrlf set.
* text=auto
# Explicitly specify line endings for as many files as possible.
# People who (for example) rsync between Windows and Linux need this.
# File types which we know are binary
# Prefer LF for most file types
*.frag text eol=lf
*.htm text eol=lf
*.html text eol=lf
*.iml text eol=lf
*.js text eol=lf
*.js.map text eol=lf
*.json text eol=lf
*.md text eol=lf
*.vert text eol=lf
*.xml text eol=lf
# Prefer LF for these files
.editorconfig text eol=lf
.eslintrc text eol=lf
.gitattributes text eol=lf
.gitignore text eol=lf
.gitmodules text eol=lf
LICENSE text eol=lf
Makefile text eol=lf
README text eol=lf
TRADEMARK text eol=lf
# Use CRLF for Windows-specific file types

View file

@ -1,7 +1,7 @@
language: node_js
node_js:
- '4'
- stable
- "4"
- "node"
sudo: false
cache:
directories:
@ -29,7 +29,7 @@ after_script:
npm --no-git-tag-version version $($(npm bin)/json -f package.json version)-prerelease.$(date +%s)
npm publish
# Publish to gh-pages as most recent committer
git config --global user.email $(git log --pretty=format:"%ce" -n1)
git config --global user.name $(git log --pretty=format:"%cn" -n1)
./node_modules/.bin/gh-pages -x -r https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git -d playground -m "Build for $(git log --pretty=format:%H)"
git config --global user.email $(git log --pretty=format:"%ae" -n1)
git config --global user.name $(git log --pretty=format:"%an" -n1)
npm run --silent deploy -- -x -r $GH_PAGES_REPO
fi

View file

@ -1,34 +0,0 @@
ESLINT=./node_modules/.bin/eslint
NODE=node
TAP=./node_modules/.bin/tap
WEBPACK=./node_modules/.bin/webpack --progress --colors
WEBPACK_DEV_SERVER=./node_modules/.bin/webpack-dev-server
# ------------------------------------------------------------------------------
build:
$(WEBPACK)
watch:
$(WEBPACK) --watch
serve:
$(WEBPACK_DEV_SERVER)
# ------------------------------------------------------------------------------
lint:
$(ESLINT) ./src/*.js
$(ESLINT) ./src/**/*.js
$(ESLINT) ./test/**/*.js
test:
@make lint
$(TAP) ./test/{unit,integration}/*.js
coverage:
$(TAP) ./test/{unit,integration}/*.js --coverage --coverage-report=lcov
# ------------------------------------------------------------------------------
.PHONY: build lint test coverage benchmark serve

View file

@ -36,14 +36,14 @@ StartServerWindows.bat
```
## Playground
To run the Playground, make sure the dev server's running and go to [http://localhost:8080/](http://localhost:8080/) - you will be directed to the playground, which demonstrates various tools and internal state.
To run the Playground, make sure the dev server's running and go to [http://localhost:8073/](http://localhost:8073/) - you will be directed to the playground, which demonstrates various tools and internal state.
![VM Playground Screenshot](https://i.imgur.com/nOCNqEc.gif)
## Standalone Build
```bash
make build
npm run build
```
```html
@ -104,11 +104,23 @@ The VM's block representation contains all the important information for executi
## Testing
```bash
make test
npm test
```
```bash
make coverage
npm run coverage
```
## Publishing to GitHub Pages
```bash
npm run deploy
```
This will push the currently built playground to the gh-pages branch of the
currently tracked remote. If you would like to change where to push to, add
a repo url argument:
```bash
npm run deploy -- -r <your repo url>
```
## Donate

View file

@ -1,2 +0,0 @@
@echo off
node_modules\.bin\webpack-dev-server --host 0.0.0.0 --content-base .\playground

View file

@ -1,42 +0,0 @@
<svg version="1.1" id="cat" x="0px" y="0px" width="95px" height="111px" viewBox="0 0 95 111" enable-background="new 0 0 95 111" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g>
<g id="Layer_3">
<path fill="#FAA51D" stroke="#000000" d="M22.462,79.039c-2.415-0.451-5.304-1.309-7.742-3.503&#xD;&#xA;&#x9;&#x9;C9.268,70.629,7.526,62.535,3.672,64.622c-3.856,2.088-3.782,15.165,8.353,19.194c4.182,1.391,7.998,1.396,11.091,1.312&#xD;&#xA;&#x9;&#x9;c0.811-0.025,7.717-0.654,10.079-4.074c2.361-3.42,0.719-4.272-0.09-4.744C32.295,75.838,25.878,79.677,22.462,79.039z"/>
<path fill="#FFFFFF" d="M4.236,64.877c-1.989,0.613-3.075,4.998-2.076,8.484c0.998,3.49,2.634,5.022,3.863,6.398&#xD;&#xA;&#x9;&#x9;c1.528,1.038-0.72-2.402,1.361-4.15c2.075-1.744,5.733-0.914,5.733-0.914s-2.909-3.987-4.57-6.396&#xD;&#xA;&#x9;&#x9;C6.975,65.988,6.359,64.375,4.236,64.877z"/>
</g>
<g>
<path fill="#FAA51D" d="M38.217,86.756c0,0-8.832,6.2-17.071,8.412l0.086,0.215c1.247,1.824,5.87,7.497-0.334,9.496&#xD;&#xA;&#x9;&#x9;c-5.333,1.717-15.12-13.104-10.821-15.902c2.626-1.713,4.892-0.252,4.892-0.252s3.474-1.07,6.001-2.345&#xD;&#xA;&#x9;&#x9;c4.303-2.161,5.784-3.453,5.784-3.453s4.184-4.306,6.856-4.137C36.281,78.96,41.669,83.504,38.217,86.756z"/>
<path fill="none" stroke="#231F20" stroke-width="1.2" d="M21.232,95.383c1.247,1.824,5.87,7.497-0.334,9.496&#xD;&#xA;&#x9;&#x9;c-5.333,1.717-15.329-13.344-11.03-16.145c2.626-1.713,5.101-0.01,5.101-0.01s3.474-1.072,6.001-2.348&#xD;&#xA;&#x9;&#x9;c4.303-2.161,5.784-3.453,5.784-3.453"/>
<path fill="none" stroke="#231F20" stroke-width="1.2" d="M38.217,86.756c0,0-10.123,7.107-18.804,8.819"/>
</g>
<path fill="#FAA51D" stroke="#231F20" stroke-width="1.2" d="M52.169,74.885c0,0,1.235,0.165,4.744,3.676&#xD;&#xA;&#x9;c3.509,3.508,6.026,2.16,8.911,0.724c2.877-1.443,10.537-6.126,6.49-9.817c-4.049-3.688-6.207,1.146-9.715,2.405&#xD;&#xA;&#x9;c-3.512,1.26-5.061-2.487-6.858-4.287c-0.589-0.593-1.188-1.099-1.729-1.505c0,0-0.971-0.76-1.906,2.79&#xD;&#xA;&#x9;C51.172,72.412,50.162,73.415,52.169,74.885z"/>
<g id="Layer_2_1_">
<path fill="#FAA51D" stroke="#231F20" stroke-width="1.2" d="M46.753,82.012c1.188-0.912,2.397-2.402,3.951-4.713&#xD;&#xA;&#x9;&#x9;c1.296-1.927,2.7-5.578,2.7-5.578c0.875-2.521,1.934-6.576-1.902-7.296c-1.553-0.291-4.079-0.098-7.67-0.776&#xD;&#xA;&#x9;&#x9;c-3.593-0.681-6.798-2.522-9.517,2.233c-2.718,4.757-9.59,8.271-1.056,16.563c0,0,4.901,3.842,10.764,9.639&#xD;&#xA;&#x9;&#x9;c4.831,4.775,12.045,10.602,12.045,10.602s18.972,2.188,19.535-0.693c1.922-9.79-14.777-6.911-14.777-6.911&#xD;&#xA;&#x9;&#x9;s-4.605-3.933-6.725-5.794c-3.478-3.059-11.125-10.771-11.125-10.771"/>
<path fill="#FFFFFF" d="M51.253,75.434c0,0,2.47-2.66-2.469-5.317c-4.939-2.657-7.213-0.017-8.739,1.521&#xD;&#xA;&#x9;&#x9;c-2.644,2.655,3.443,6.611,3.443,6.611l3.176,3.204c0,0,1.738-1.647,2.499-2.979C50.036,77.26,51.253,75.434,51.253,75.434"/>
</g>
<g id="Layer_8"/>
<path fill="#FAA51D" stroke="#231F20" stroke-width="1.2" d="M29.926,73.218c0.749-0.571,2.889-2.202,4.854-3.657&#xD;&#xA;&#x9;c2.428-1.799,6.117-5.849,1.077-7.646c-5.04-1.801-7.507,1.604-11.519,4.946c-2.159,1.801-5.308,2.699-4.319,6.209&#xD;&#xA;&#x9;c0.993,3.511,4.862,13.408,11.789,10.17c6.929-3.239-1.799-9.18-3.06-11.157"/>
<g id="Layer_2">
<path fill="#FAA51D" stroke="#231F20" stroke-width="1.2" d="M52.709,14.156c-1.54-0.143-4.75-0.316-6.518-0.231&#xD;&#xA;&#x9;&#x9;c-4.728,0.225-9.224,1.928-9.224,1.928L23.949,7.357l2.235,18.906c0.646-0.782-10.555,12.804-3.479,24.224&#xD;&#xA;&#x9;&#x9;c7.08,11.426,22.233,16.518,40.988,12.792c18.755-3.729,23.229-14.531,21.986-20.246c-1.242-5.714-8.322-7.823-8.322-7.823&#xD;&#xA;&#x9;&#x9;s-0.09-4.48-3.328-9.97c-1.926-3.268-8.348-8.041-8.348-8.041L62.822,5.647l-7.452,7.204L52.709,14.156z"/>
<path fill="#FFFFFF" d="M76.42,35.066l-2.482-2.064l-9.115,2.661c0,0,0,3.419-4.367,4.367c-4.37,0.951-11.211-2.277-11.211-2.277&#xD;&#xA;&#x9;&#x9;L41.46,41.17c0,0-8.437,0.928-8.739,6.081C32.048,58.704,46.1,63.479,51.425,63.783c2.905,0.167,8.235-0.338,12.277-1.141&#xD;&#xA;&#x9;&#x9;c17.752-3.234,22.551-13.919,21.31-19.635c-1.242-5.714-7.978-7.196-7.978-7.196L76.42,35.066z"/>
<path fill="none" stroke="#231F20" stroke-width="1.2" d="M10.673,46.155c0,0,4.107,0.374,5.974,0.268&#xD;&#xA;&#x9;&#x9;c1.865-0.107,5.492-0.587,5.492-0.587"/>
<path fill="none" stroke="#231F20" stroke-width="1.2" d="M81.656,40.671c0,0,4.549-0.743,6.859-1.549&#xD;&#xA;&#x9;&#x9;c2.715-0.942,4.543-2.545,4.543-2.545"/>
<path fill="none" stroke="#231F20" stroke-width="1.2" d="M22.337,41.909c0,0-2.384-1.777-6.117-3.43&#xD;&#xA;&#x9;&#x9;c-4.134-1.831-6.405-2.303-6.405-2.303"/>
<path fill="none" stroke="#231F20" stroke-width="1.2" d="M82.117,46.622c0,0,2.726,1.104,5.533,1.385&#xD;&#xA;&#x9;&#x9;c2.77,0.276,4.646,0.11,4.646,0.11"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-miterlimit="10" d="M52.35,14.212&#xD;&#xA;&#x9;&#x9;c2.84,0.7,3.887,1.469,3.887,1.469"/>
<line fill="none" stroke="#000000" x1="33.898" y1="13.684" x2="39.956" y2="18.042"/>
</g>
<g id="Layer_5">
<path fill="#FFFFFF" stroke="#231F20" d="M71.84,25.366c2.924,4.479,3.033,9.591,0.242,11.415&#xD;&#xA;&#x9;&#x9;c-2.793,1.825-7.426-0.332-10.354-4.813c-2.933-4.48-3.037-9.589-0.244-11.415C64.275,18.73,68.913,20.884,71.84,25.366z"/>
<path fill="#231F20" d="M71.089,32.522c0,1.08-0.802,1.956-1.8,1.956c-0.993,0-1.803-0.877-1.803-1.956&#xD;&#xA;&#x9;&#x9;c0-1.08,0.81-1.958,1.803-1.958C70.287,30.564,71.089,31.442,71.089,32.522"/>
</g>
<g id="Layer_7">
<path fill="#FFFFFF" stroke="#231F20" d="M47.867,28.619c2.926,4.48,2.619,9.862-0.681,12.015&#xD;&#xA;&#x9;&#x9;c-3.302,2.159-8.351,0.272-11.276-4.208c-2.928-4.48-2.624-9.86,0.678-12.017C39.891,22.253,44.938,24.137,47.867,28.619z"/>
<path fill="#231F20" d="M46.079,34.507c0,1.081-0.803,1.957-1.801,1.957c-0.992,0-1.803-0.878-1.803-1.957&#xD;&#xA;&#x9;&#x9;c0-1.08,0.811-1.957,1.803-1.957C45.274,32.55,46.079,33.427,46.079,34.507"/>
</g>
<path fill="#5E4A42" stroke="#000000" d="M59.766,37.926c1.854,0,4.555-0.284,4.697,0.569c0.143,0.855-1.709,4.203-2.988,4.345&#xD;&#xA;&#x9;c-1.283,0.142-6.125-2.353-6.195-3.919C55.206,37.355,58.055,37.926,59.766,37.926z"/>
<g id="Layer_4">
<path fill="none" stroke="#231F20" stroke-width="1.2" d="M46.774,45.235c0,0,10.347,3.054,14.217,3.897&#xD;&#xA;&#x9;&#x9;c3.868,0.842,10.851,1.684,10.851,1.684s-7.99,10.245-17.328,7.644C45.176,55.863,45.345,49.975,46.774,45.235z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

View file

@ -11,16 +11,23 @@
},
"main": "./dist.js",
"scripts": {
"prepublish": "./node_modules/.bin/webpack --bail",
"start": "webpack-dev-server",
"build": "webpack --colors --progress",
"test": "make test",
"watch": "webpack --colors --progress --watch",
"build": "./node_modules/.bin/webpack --progress --colors --bail",
"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)\"",
"lint": "./node_modules/.bin/eslint .",
"prepublish": "npm run build",
"prepublish-watch": "npm run watch",
"start": "./node_modules/.bin/webpack-dev-server",
"tap": "./node_modules/.bin/tap ./test/{unit,integration}/*.js",
"test": "npm run lint && npm run tap",
"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)'\""
},
"devDependencies": {
"babel-eslint": "7.0.0",
"copy-webpack-plugin": "3.0.1",
"eslint": "2.7.0",
"eslint": "3.8.1",
"eslint-config-scratch": "^2.0.0",
"expose-loader": "0.7.1",
"gh-pages": "0.11.0",
"highlightjs": "8.7.0",
@ -28,10 +35,11 @@
"json": "9.0.4",
"json-loader": "0.5.4",
"lodash.defaultsdeep": "4.6.0",
"minilog": "3.0.1",
"promise": "7.1.1",
"scratch-audioengine": "^0.1.0-prepublish",
"scratch-blocks": "^0.1.0-prepublish",
"scratch-render": "^0.1.0-prepublish",
"scratch-audioengine": "latest",
"scratch-blocks": "latest",
"scratch-render": "latest",
"script-loader": "0.7.0",
"stats.js": "0.16.0",
"tap": "5.7.1",

View file

@ -15,6 +15,12 @@
<button id="greenflag">Green flag</button>
<button id="stopall">Stop</button>
</div>
<div>
Turbo: <input id='turbomode' type='checkbox' />
</div>
<div>
Compatibility (30 TPS): <input id='compatmode' type='checkbox' />
</div>
<br />
<ul id="playgroundLinks">
<li><a id="renderexplorer-link" href="#">Renderer</a></li>
@ -25,6 +31,12 @@
<div id="tab-renderexplorer">
Renderer<br />
<canvas id="scratch-stage" style="width: 480px; height: 360px;"></canvas><br />
x: <input id='sinfo-x' />
y: <input id='sinfo-y' /><br />
dir: <input id='sinfo-direction' />
rotation style: <input id='sinfo-rotationstyle' /><br />
visible: <input id='sinfo-visible' />
<button id='sinfo-post'>Post</button>
</div>
<div id="tab-threadexplorer">
Thread explorer

View file

@ -1,11 +1,6 @@
var NEW_PROJECT_HASH = 'createEmptyProject';
var loadProject = function () {
var id = location.hash.substring(1);
if (id === NEW_PROJECT_HASH) {
window.vm.createEmptyProject();
return;
}
if (id.length < 1 || !isFinite(id)) {
id = '119615668';
}
@ -16,8 +11,6 @@ var loadProject = function () {
if (this.readyState === 4) {
if (r.status === 200) {
window.vm.loadProject(this.responseText);
} else {
window.vm.createEmptyProject();
}
}
};
@ -36,11 +29,6 @@ window.onload = function() {
document.location = '#' + document.getElementById('projectId').value;
location.reload();
};
document.getElementById('createEmptyProject').addEventListener('click',
function() {
document.location = '#' + NEW_PROJECT_HASH;
location.reload();
});
loadProject();
// Instantiate the renderer and connect it to the VM.
@ -136,13 +124,13 @@ window.onload = function() {
// Generate new select box.
for (var i = 0; i < data.targetList.length; i++) {
var targetOption = document.createElement('option');
targetOption.setAttribute('value', data.targetList[i][0]);
targetOption.setAttribute('value', data.targetList[i].id);
// If target id matches editingTarget id, select it.
if (data.targetList[i][0] == data.editingTarget) {
if (data.targetList[i].id == data.editingTarget) {
targetOption.setAttribute('selected', 'selected');
}
targetOption.appendChild(
document.createTextNode(data.targetList[i][1])
document.createTextNode(data.targetList[i].name)
);
selectedTarget.appendChild(targetOption);
}
@ -152,10 +140,10 @@ window.onload = function() {
};
// Feedback for stacks and blocks running.
vm.on('STACK_GLOW_ON', function(data) {
vm.on('SCRIPT_GLOW_ON', function(data) {
workspace.glowStack(data.id, true);
});
vm.on('STACK_GLOW_OFF', function(data) {
vm.on('SCRIPT_GLOW_OFF', function(data) {
workspace.glowStack(data.id, false);
});
vm.on('BLOCK_GLOW_ON', function(data) {
@ -168,6 +156,25 @@ window.onload = function() {
workspace.reportValue(data.id, data.value);
});
vm.on('SPRITE_INFO_REPORT', function(data) {
if (data.id !== selectedTarget.value) return; // Not the editingTarget
document.getElementById('sinfo-x').value = data.x;
document.getElementById('sinfo-y').value = data.y;
document.getElementById('sinfo-direction').value = data.direction;
document.getElementById('sinfo-rotationstyle').value = data.rotationStyle;
document.getElementById('sinfo-visible').value = data.visible;
});
document.getElementById('sinfo-post').addEventListener('click', function () {
var data = {};
data.x = document.getElementById('sinfo-x').value;
data.y = document.getElementById('sinfo-y').value;
data.direction = document.getElementById('sinfo-direction').value;
data.rotationStyle = document.getElementById('sinfo-rotationstyle').value;
data.visible = document.getElementById('sinfo-visible').value === 'true';
vm.postSpriteInfo(data);
});
// Feed mouse events as VM I/O events.
document.addEventListener('mousemove', function (e) {
var rect = canvas.getBoundingClientRect();
@ -234,9 +241,7 @@ window.onload = function() {
// Inform VM of animation frames.
var animate = function() {
stats.end();
stats.begin();
window.vm.animationFrame();
stats.update();
requestAnimationFrame(animate);
};
requestAnimationFrame(animate);
@ -248,7 +253,15 @@ window.onload = function() {
document.getElementById('stopall').addEventListener('click', function() {
vm.stopAll();
});
document.getElementById('turbomode').addEventListener('change', function() {
var turboOn = document.getElementById('turbomode').checked;
vm.setTurboMode(turboOn);
});
document.getElementById('compatmode').addEventListener('change',
function() {
var compatibilityMode = document.getElementById('compatmode').checked;
vm.setCompatibilityMode(compatibilityMode);
});
var tabBlockExplorer = document.getElementById('tab-blockexplorer');
var tabThreadExplorer = document.getElementById('tab-threadexplorer');
var tabRenderExplorer = document.getElementById('tab-renderexplorer');

10
src/.eslintrc.js Normal file
View file

@ -0,0 +1,10 @@
module.exports = {
root: true,
extends: 'scratch',
env: {
browser: true
},
globals: {
Promise: true
}
};

View file

@ -1,158 +1,122 @@
var Cast = require('../util/cast');
var Promise = require('promise');
var Timer = require('../util/timer');
function Scratch3ControlBlocks(runtime) {
var Scratch3ControlBlocks = function (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.
*/
Scratch3ControlBlocks.prototype.getPrimitives = function() {
Scratch3ControlBlocks.prototype.getPrimitives = function () {
return {
'control_repeat': this.repeat,
'control_repeat_until': this.repeatUntil,
'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_menu': this.createCloneMenu,
'control_create_clone_of': this.createClone,
'control_delete_this_clone': this.deleteClone
control_repeat: this.repeat,
control_repeat_until: this.repeatUntil,
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
};
};
Scratch3ControlBlocks.prototype.getHats = function () {
return {
'control_start_as_clone': {
control_start_as_clone: {
restartExistingThreads: false
}
};
};
Scratch3ControlBlocks.prototype.repeat = function(args, util) {
Scratch3ControlBlocks.prototype.repeat = function (args, util) {
var times = Math.floor(Cast.toNumber(args.TIMES));
// Initialize loop
if (util.stackFrame.loopCounter === undefined) {
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.
if (!util.stackFrame.executedInFrame) {
util.stackFrame.executedInFrame = true;
// Decrease counter
util.stackFrame.loopCounter--;
// If we still have some left, start the branch.
if (util.stackFrame.loopCounter >= 0) {
util.startBranch();
}
} else {
util.stackFrame.executedInFrame = false;
util.yieldFrame();
// Decrease counter
util.stackFrame.loopCounter--;
// If we still have some left, start the branch.
if (util.stackFrame.loopCounter >= 0) {
util.startBranch(1, true);
}
};
Scratch3ControlBlocks.prototype.repeatUntil = function(args, util) {
Scratch3ControlBlocks.prototype.repeatUntil = function (args, util) {
var condition = Cast.toBoolean(args.CONDITION);
// 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.
if (!util.stackFrame.executedInFrame) {
util.stackFrame.executedInFrame = true;
// If the condition is true, start the branch.
if (!condition) {
util.startBranch();
}
} else {
util.stackFrame.executedInFrame = false;
util.yieldFrame();
}
};
Scratch3ControlBlocks.prototype.waitUntil = function(args, util) {
var condition = Cast.toBoolean(args.CONDITION);
// Only execute once per frame.
// If the condition is true, start the branch.
if (!condition) {
util.yieldFrame();
util.startBranch(1, true);
}
};
Scratch3ControlBlocks.prototype.forever = function(args, util) {
// Only execute once per frame.
// When the branch finishes, `forever` will be executed again and
// the second branch will be taken, yielding for the rest of the frame.
if (!util.stackFrame.executedInFrame) {
util.stackFrame.executedInFrame = true;
util.startBranch();
Scratch3ControlBlocks.prototype.waitUntil = function (args, util) {
var condition = Cast.toBoolean(args.CONDITION);
if (!condition) {
util.yield();
}
};
Scratch3ControlBlocks.prototype.forever = function (args, util) {
util.startBranch(1, true);
};
Scratch3ControlBlocks.prototype.wait = function (args, util) {
if (!util.stackFrame.timer) {
util.stackFrame.timer = new Timer();
util.stackFrame.timer.start();
util.yield();
this.runtime.requestRedraw();
} else {
util.stackFrame.executedInFrame = false;
util.yieldFrame();
}
};
Scratch3ControlBlocks.prototype.wait = function(args) {
var duration = Cast.toNumber(args.DURATION);
return new Promise(function(resolve) {
setTimeout(function() {
resolve();
}, 1000 * duration);
});
};
Scratch3ControlBlocks.prototype.if = function(args, util) {
var condition = Cast.toBoolean(args.CONDITION);
// Only execute one time. `if` will be returned to
// when the branch finishes, but it shouldn't execute again.
if (util.stackFrame.executedInFrame === undefined) {
util.stackFrame.executedInFrame = true;
if (condition) {
util.startBranch();
var duration = Math.max(0, 1000 * Cast.toNumber(args.DURATION));
if (util.stackFrame.timer.timeElapsed() < duration) {
util.yield();
}
}
};
Scratch3ControlBlocks.prototype.ifElse = function(args, util) {
Scratch3ControlBlocks.prototype.if = function (args, util) {
var condition = Cast.toBoolean(args.CONDITION);
// Only execute one time. `ifElse` will be returned to
// when the branch finishes, but it shouldn't execute again.
if (util.stackFrame.executedInFrame === undefined) {
util.stackFrame.executedInFrame = true;
if (condition) {
util.startBranch(1);
} else {
util.startBranch(2);
}
if (condition) {
util.startBranch(1, false);
}
};
Scratch3ControlBlocks.prototype.stop = function(args, util) {
Scratch3ControlBlocks.prototype.ifElse = function (args, util) {
var condition = Cast.toBoolean(args.CONDITION);
if (condition) {
util.startBranch(1, false);
} else {
util.startBranch(2, false);
}
};
Scratch3ControlBlocks.prototype.stop = function (args, util) {
var option = args.STOP_OPTION;
if (option == 'all') {
if (option === 'all') {
util.stopAll();
} else if (option == 'other scripts in sprite' ||
option == 'other scripts in stage') {
} else if (option === 'other scripts in sprite' ||
option === 'other scripts in stage') {
util.stopOtherTargetThreads();
} else if (option == 'this script') {
} else if (option === 'this script') {
util.stopThread();
}
};
// @todo (GH-146): remove.
Scratch3ControlBlocks.prototype.createCloneMenu = function (args) {
return args.CLONE_OPTION;
};
Scratch3ControlBlocks.prototype.createClone = function (args, util) {
var cloneTarget;
if (args.CLONE_OPTION == '_myself_') {
if (args.CLONE_OPTION === '_myself_') {
cloneTarget = util.target;
} else {
cloneTarget = this.runtime.getSpriteTargetByName(args.CLONE_OPTION);

View file

@ -1,12 +1,12 @@
var Cast = require('../util/cast');
function Scratch3DataBlocks(runtime) {
var Scratch3DataBlocks = function (runtime) {
/**
* The runtime instantiating this block package.
* @type {Runtime}
*/
this.runtime = runtime;
}
};
/**
* Retrieve the block primitives implemented by this package.
@ -14,17 +14,17 @@ function Scratch3DataBlocks(runtime) {
*/
Scratch3DataBlocks.prototype.getPrimitives = function () {
return {
'data_variable': this.getVariable,
'data_setvariableto': this.setVariableTo,
'data_changevariableby': this.changeVariableBy,
'data_listcontents': 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
data_variable: this.getVariable,
data_setvariableto: this.setVariableTo,
data_changevariableby: this.changeVariableBy,
data_listcontents: 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
};
};
@ -54,7 +54,7 @@ Scratch3DataBlocks.prototype.getListContents = function (args, util) {
for (var i = 0; i < list.contents.length; i++) {
var listItem = list.contents[i];
if (!((typeof listItem === 'string') &&
(listItem.length == 1))) {
(listItem.length === 1))) {
allSingleLetters = false;
break;
}
@ -126,7 +126,7 @@ Scratch3DataBlocks.prototype.listContainsItem = function (args, util) {
// 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) {
if (Cast.compare(list.contents[i], item) === 0) {
return true;
}
}

View file

@ -1,44 +1,44 @@
var Cast = require('../util/cast');
function Scratch3EventBlocks(runtime) {
var Scratch3EventBlocks = function (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.
*/
Scratch3EventBlocks.prototype.getPrimitives = function() {
Scratch3EventBlocks.prototype.getPrimitives = function () {
return {
'event_broadcast': this.broadcast,
'event_broadcastandwait': this.broadcastAndWait,
'event_whengreaterthan': this.hatGreaterThanPredicate
event_broadcast: this.broadcast,
event_broadcastandwait: this.broadcastAndWait,
event_whengreaterthan: this.hatGreaterThanPredicate
};
};
Scratch3EventBlocks.prototype.getHats = function () {
return {
'event_whenflagclicked': {
event_whenflagclicked: {
restartExistingThreads: true
},
'event_whenkeypressed': {
event_whenkeypressed: {
restartExistingThreads: false
},
'event_whenthisspriteclicked': {
event_whenthisspriteclicked: {
restartExistingThreads: true
},
'event_whenbackdropswitchesto': {
event_whenbackdropswitchesto: {
restartExistingThreads: true
},
'event_whengreaterthan': {
event_whengreaterthan: {
restartExistingThreads: false,
edgeActivated: true
},
'event_whenbroadcastreceived': {
event_whenbroadcastreceived: {
restartExistingThreads: true
}
};
@ -48,16 +48,16 @@ Scratch3EventBlocks.prototype.hatGreaterThanPredicate = function (args, util) {
var option = Cast.toString(args.WHENGREATERTHANMENU).toLowerCase();
var value = Cast.toNumber(args.VALUE);
// @todo: Other cases :)
if (option == 'timer') {
if (option === 'timer') {
return util.ioQuery('clock', 'projectTimer') > value;
}
return false;
};
Scratch3EventBlocks.prototype.broadcast = function(args, util) {
Scratch3EventBlocks.prototype.broadcast = function (args, util) {
var broadcastOption = Cast.toString(args.BROADCAST_OPTION);
util.startHats('event_whenbroadcastreceived', {
'BROADCAST_OPTION': broadcastOption
BROADCAST_OPTION: broadcastOption
});
};
@ -68,21 +68,21 @@ Scratch3EventBlocks.prototype.broadcastAndWait = function (args, util) {
// No - start hats for this broadcast.
util.stackFrame.startedThreads = util.startHats(
'event_whenbroadcastreceived', {
'BROADCAST_OPTION': broadcastOption
BROADCAST_OPTION: broadcastOption
}
);
if (util.stackFrame.startedThreads.length == 0) {
if (util.stackFrame.startedThreads.length === 0) {
// Nothing was started.
return;
}
}
// We've run before; check if the wait is still going on.
var instance = this;
var waiting = util.stackFrame.startedThreads.some(function(thread) {
var waiting = util.stackFrame.startedThreads.some(function (thread) {
return instance.runtime.isActiveThread(thread);
});
if (waiting) {
util.yieldFrame();
util.yield();
}
};

View file

@ -1,39 +1,41 @@
var Cast = require('../util/cast');
function Scratch3LooksBlocks(runtime) {
var Scratch3LooksBlocks = function (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.
*/
Scratch3LooksBlocks.prototype.getPrimitives = function() {
Scratch3LooksBlocks.prototype.getPrimitives = function () {
return {
'looks_say': this.say,
'looks_sayforsecs': this.sayforsecs,
'looks_think': this.think,
'looks_thinkforsecs': this.sayforsecs,
'looks_show': this.show,
'looks_hide': this.hide,
'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_size': this.getSize,
'looks_costumeorder': this.getCostumeIndex,
'looks_backdroporder': this.getBackdropIndex,
'looks_backdropname': this.getBackdropName
looks_say: this.say,
looks_sayforsecs: this.sayforsecs,
looks_think: this.think,
looks_thinkforsecs: this.sayforsecs,
looks_show: this.show,
looks_hide: this.hide,
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_gotofront: this.goToFront,
looks_gobacklayers: this.goBackLayers,
looks_size: this.getSize,
looks_costumeorder: this.getCostumeIndex,
looks_backdroporder: this.getBackdropIndex,
looks_backdropname: this.getBackdropName
};
};
@ -43,8 +45,8 @@ Scratch3LooksBlocks.prototype.say = function (args, util) {
Scratch3LooksBlocks.prototype.sayforsecs = function (args, util) {
util.target.setSay('say', args.MESSAGE);
return new Promise(function(resolve) {
setTimeout(function() {
return new Promise(function (resolve) {
setTimeout(function () {
// Clear say bubble and proceed.
util.target.setSay();
resolve();
@ -58,8 +60,8 @@ Scratch3LooksBlocks.prototype.think = function (args, util) {
Scratch3LooksBlocks.prototype.thinkforsecs = function (args, util) {
util.target.setSay('think', args.MESSAGE);
return new Promise(function(resolve) {
setTimeout(function() {
return new Promise(function (resolve) {
setTimeout(function () {
// Clear say bubble and proceed.
util.target.setSay();
resolve();
@ -80,37 +82,37 @@ Scratch3LooksBlocks.prototype.hide = function (args, util) {
* Matches the behavior of Scratch 2.0 for different types of arguments.
* @param {!Target} target Target to set costume/backdrop to.
* @param {Any} requestedCostume Costume requested, e.g., 0, 'name', etc.
* @param {boolean=} opt_zeroIndex Set to zero-index the requestedCostume.
* @param {boolean=} optZeroIndex Set to zero-index the requestedCostume.
* @return {Array.<!Thread>} Any threads started by this switch.
*/
Scratch3LooksBlocks.prototype._setCostumeOrBackdrop = function (target,
requestedCostume, opt_zeroIndex) {
requestedCostume, optZeroIndex) {
if (typeof requestedCostume === 'number') {
target.setCostume(opt_zeroIndex ?
target.setCostume(optZeroIndex ?
requestedCostume : requestedCostume - 1);
} else {
var costumeIndex = target.getCostumeIndexByName(requestedCostume);
if (costumeIndex > -1) {
target.setCostume(costumeIndex);
} else if (costumeIndex == 'previous costume' ||
costumeIndex == 'previous backdrop') {
} else if (requestedCostume === 'previous costume' ||
requestedCostume === 'previous backdrop') {
target.setCostume(target.currentCostume - 1);
} else if (costumeIndex == 'next costume' ||
costumeIndex == 'next backdrop') {
} else if (requestedCostume === 'next costume' ||
requestedCostume === 'next backdrop') {
target.setCostume(target.currentCostume + 1);
} else {
var forcedNumber = Cast.toNumber(requestedCostume);
if (!isNaN(forcedNumber)) {
target.setCostume(opt_zeroIndex ?
target.setCostume(optZeroIndex ?
forcedNumber : forcedNumber - 1);
}
}
}
if (target == this.runtime.getTargetForStage()) {
if (target === this.runtime.getTargetForStage()) {
// Target is the stage - start hats.
var newName = target.sprite.costumes[target.currentCostume].name;
return this.runtime.startHats('event_whenbackdropswitchesto', {
'BACKDROP': newName
BACKDROP: newName
});
}
return [];
@ -140,18 +142,18 @@ Scratch3LooksBlocks.prototype.switchBackdropAndWait = function (args, util) {
args.BACKDROP
)
);
if (util.stackFrame.startedThreads.length == 0) {
if (util.stackFrame.startedThreads.length === 0) {
// Nothing was started.
return;
}
}
// We've run before; check if the wait is still going on.
var instance = this;
var waiting = util.stackFrame.startedThreads.some(function(thread) {
var waiting = util.stackFrame.startedThreads.some(function (thread) {
return instance.runtime.isActiveThread(thread);
});
if (waiting) {
util.yieldFrame();
util.yield();
}
};
@ -190,6 +192,14 @@ Scratch3LooksBlocks.prototype.setSize = function (args, util) {
util.target.setSize(size);
};
Scratch3LooksBlocks.prototype.goToFront = function (args, util) {
util.target.goToFront();
};
Scratch3LooksBlocks.prototype.goBackLayers = function (args, util) {
util.target.goBackLayers(args.NUM);
};
Scratch3LooksBlocks.prototype.getSize = function (args, util) {
return util.target.size;
};

View file

@ -2,36 +2,37 @@ var Cast = require('../util/cast');
var MathUtil = require('../util/math-util');
var Timer = require('../util/timer');
function Scratch3MotionBlocks(runtime) {
var Scratch3MotionBlocks = function (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.
*/
Scratch3MotionBlocks.prototype.getPrimitives = function() {
Scratch3MotionBlocks.prototype.getPrimitives = function () {
return {
'motion_movesteps': this.moveSteps,
'motion_gotoxy': this.goToXY,
'motion_goto': this.goTo,
'motion_turnright': this.turnRight,
'motion_turnleft': this.turnLeft,
'motion_pointindirection': this.pointInDirection,
'motion_pointtowards': this.pointTowards,
'motion_glidesecstoxy': this.glide,
'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
motion_movesteps: this.moveSteps,
motion_gotoxy: this.goToXY,
motion_goto: this.goTo,
motion_turnright: this.turnRight,
motion_turnleft: this.turnLeft,
motion_pointindirection: this.pointInDirection,
motion_pointtowards: this.pointTowards,
motion_glidesecstoxy: this.glide,
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
};
};
@ -118,7 +119,7 @@ Scratch3MotionBlocks.prototype.glide = function (args, util) {
util.target.setXY(util.stackFrame.endX, util.stackFrame.endY);
return;
}
util.yieldFrame();
util.yield();
} else {
var timeElapsed = util.stackFrame.timer.timeElapsed();
if (timeElapsed < util.stackFrame.duration * 1000) {
@ -130,7 +131,7 @@ Scratch3MotionBlocks.prototype.glide = function (args, util) {
util.stackFrame.startX + dx,
util.stackFrame.startY + dy
);
util.yieldFrame();
util.yield();
} else {
// Finished: move to final position.
util.target.setXY(util.stackFrame.endX, util.stackFrame.endY);
@ -138,6 +139,62 @@ Scratch3MotionBlocks.prototype.glide = function (args, util) {
}
};
Scratch3MotionBlocks.prototype.ifOnEdgeBounce = function (args, util) {
var bounds = util.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.
var stageWidth = this.runtime.constructor.STAGE_WIDTH;
var stageHeight = this.runtime.constructor.STAGE_HEIGHT;
var distLeft = Math.max(0, (stageWidth / 2) + bounds.left);
var distTop = Math.max(0, (stageHeight / 2) - bounds.top);
var distRight = Math.max(0, (stageWidth / 2) - bounds.right);
var distBottom = Math.max(0, (stageHeight / 2) + bounds.bottom);
// Find the nearest edge.
var nearestEdge = '';
var 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.
var radians = MathUtil.degToRad(90 - util.target.direction);
var dx = Math.cos(radians);
var 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));
}
var newDirection = MathUtil.radToDeg(Math.atan2(dy, dx)) + 90;
util.target.setDirection(newDirection);
// Keep within the stage.
var fencedPosition = util.target.keepInFence(util.target.x, util.target.y);
util.target.setXY(fencedPosition[0], fencedPosition[1]);
};
Scratch3MotionBlocks.prototype.setRotationStyle = function (args, util) {
util.target.setRotationStyle(args.STYLE);
};

View file

@ -1,36 +1,36 @@
var Cast = require('../util/cast.js');
function Scratch3OperatorsBlocks(runtime) {
var Scratch3OperatorsBlocks = function (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.
*/
Scratch3OperatorsBlocks.prototype.getPrimitives = function() {
Scratch3OperatorsBlocks.prototype.getPrimitives = function () {
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_mod': this.mod,
'operator_round': this.round,
'operator_mathop': this.mathop
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_mod: this.mod,
operator_round: this.round,
operator_mathop: this.mathop
};
};
@ -55,7 +55,7 @@ Scratch3OperatorsBlocks.prototype.lt = function (args) {
};
Scratch3OperatorsBlocks.prototype.equals = function (args) {
return Cast.compare(args.OPERAND1, args.OPERAND2) == 0;
return Cast.compare(args.OPERAND1, args.OPERAND2) === 0;
};
Scratch3OperatorsBlocks.prototype.gt = function (args) {
@ -79,10 +79,10 @@ Scratch3OperatorsBlocks.prototype.random = function (args) {
var nTo = Cast.toNumber(args.TO);
var low = nFrom <= nTo ? nFrom : nTo;
var high = nFrom <= nTo ? nTo : nFrom;
if (low == high) return low;
if (low === high) return low;
// If both arguments are ints, truncate the result to an int.
if (Cast.isInt(args.FROM) && Cast.isInt(args.TO)) {
return low + parseInt(Math.random() * ((high + 1) - low));
return low + parseInt(Math.random() * ((high + 1) - low), 10);
}
return (Math.random() * (high - low)) + low;
};

View file

@ -1,20 +1,20 @@
function Scratch3ProcedureBlocks(runtime) {
var Scratch3ProcedureBlocks = function (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.
*/
Scratch3ProcedureBlocks.prototype.getPrimitives = function() {
Scratch3ProcedureBlocks.prototype.getPrimitives = function () {
return {
'procedures_defnoreturn': this.defNoReturn,
'procedures_callnoreturn': this.callNoReturn,
'procedures_param': this.param
procedures_defnoreturn: this.defNoReturn,
procedures_callnoreturn: this.callNoReturn,
procedures_param: this.param
};
};
@ -24,23 +24,20 @@ Scratch3ProcedureBlocks.prototype.defNoReturn = function () {
Scratch3ProcedureBlocks.prototype.callNoReturn = function (args, util) {
if (!util.stackFrame.executed) {
var procedureName = args.mutation.proccode;
var paramNames = util.getProcedureParamNames(procedureName);
var procedureCode = args.mutation.proccode;
var paramNames = util.getProcedureParamNames(procedureCode);
for (var i = 0; i < paramNames.length; i++) {
if (args.hasOwnProperty('input' + i)) {
util.pushParam(paramNames[i], args['input' + i]);
}
}
util.stackFrame.executed = true;
util.startProcedure(procedureName);
util.startProcedure(procedureCode);
}
};
Scratch3ProcedureBlocks.prototype.param = function (args, util) {
var value = util.getParam(args.mutation.paramname);
if (!value) {
return 0;
}
return value;
};

View file

@ -1,33 +1,48 @@
var Cast = require('../util/cast');
function Scratch3SensingBlocks(runtime) {
var Scratch3SensingBlocks = function (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.
*/
Scratch3SensingBlocks.prototype.getPrimitives = function() {
Scratch3SensingBlocks.prototype.getPrimitives = function () {
return {
'sensing_touchingcolor': this.touchingColor,
'sensing_coloristouchingcolor': this.colorTouchingColor,
'sensing_distanceto': this.distanceTo,
'sensing_timer': this.getTimer,
'sensing_resettimer': this.resetTimer,
'sensing_mousex': this.getMouseX,
'sensing_mousey': this.getMouseY,
'sensing_mousedown': this.getMouseDown,
'sensing_keypressed': this.getKeyPressed,
'sensing_current': this.current,
'sensing_dayssince2000': this.daysSince2000
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_mousedown: this.getMouseDown,
sensing_keypressed: this.getKeyPressed,
sensing_current: this.current,
sensing_dayssince2000: this.daysSince2000
};
};
Scratch3SensingBlocks.prototype.touchingObject = function (args, util) {
var requestedObject = args.TOUCHINGOBJECTMENU;
if (requestedObject === '_mouse_') {
var mouseX = util.ioQuery('mouse', 'getX');
var mouseY = util.ioQuery('mouse', 'getY');
return util.target.isTouchingPoint(mouseX, mouseY);
} else if (requestedObject === '_edge_') {
return util.target.isTouchingEdge();
} else {
return util.target.isTouchingSprite(requestedObject);
}
};
Scratch3SensingBlocks.prototype.touchingColor = function (args, util) {
var color = Cast.toRgbColorList(args.COLOR);
return util.target.isTouchingColor(color);
@ -100,15 +115,57 @@ Scratch3SensingBlocks.prototype.getKeyPressed = function (args, util) {
return util.ioQuery('keyboard', 'getKeyIsDown', args.KEY_OPTION);
};
Scratch3SensingBlocks.prototype.daysSince2000 = function()
{
Scratch3SensingBlocks.prototype.daysSince2000 = function () {
var msPerDay = 24 * 60 * 60 * 1000;
var start = new Date(2000, 1-1, 1);
var today = new Date();
var start = new Date(2000, 0, 1); // Months are 0-indexed.
var today = new Date();
var dstAdjust = today.getTimezoneOffset() - start.getTimezoneOffset();
var mSecsSinceStart = today.valueOf() - start.valueOf();
mSecsSinceStart += ((today.getTimezoneOffset() - dstAdjust) * 60 * 1000);
return mSecsSinceStart / msPerDay;
};
Scratch3SensingBlocks.prototype.getAttributeOf = function (args) {
var attrTarget;
if (args.OBJECT === '_stage_') {
attrTarget = this.runtime.getTargetForStage();
} else {
attrTarget = this.runtime.getSpriteTargetByName(args.OBJECT);
}
// 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.sprite.costumes[attrTarget.currentCostume].name;
case 'volume': return; // @todo: Keep this in mind for sound blocks!
}
} 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.sprite.costumes[attrTarget.currentCostume].name;
case 'size': return attrTarget.size;
case 'volume': return; // @todo: above, keep in mind for sound blocks..
}
}
// Variables
var varName = args.PROPERTY;
if (attrTarget.variables.hasOwnProperty(varName)) {
return attrTarget.variables[varName].value;
}
// Otherwise, 0
return 0;
};
module.exports = Scratch3SensingBlocks;

View file

@ -1,20 +1,6 @@
var mutationAdapter = require('./mutation-adapter');
var html = require('htmlparser2');
/**
* Adapter between block creation events and block representation which can be
* used by the Scratch runtime.
* @param {Object} e `Blockly.events.create`
* @return {Array.<Object>} List of blocks from this CREATE event.
*/
module.exports = function (e) {
// Validate input
if (typeof e !== 'object') return;
if (typeof e.xml !== 'object') return;
return domToBlocks(html.parseDOM(e.xml.outerHTML));
};
/**
* Convert outer blocks DOM from a Blockly CREATE event
* to a usable form for the Scratch runtime.
@ -22,7 +8,7 @@ module.exports = function (e) {
* @param {Element} blocksDOM DOM tree for this event.
* @return {Array.<Object>} Usable list of blocks from this CREATE event.
*/
function domToBlocks (blocksDOM) {
var domToBlocks = function (blocksDOM) {
// At this level, there could be multiple blocks adjacent in the DOM tree.
var blocks = {};
for (var i = 0; i < blocksDOM.length; i++) {
@ -31,7 +17,7 @@ function domToBlocks (blocksDOM) {
continue;
}
var tagName = block.name.toLowerCase();
if (tagName == 'block' || tagName == 'shadow') {
if (tagName === 'block' || tagName === 'shadow') {
domToBlock(block, blocks, true, null);
}
}
@ -41,7 +27,21 @@ function domToBlocks (blocksDOM) {
blocksList.push(blocks[b]);
}
return blocksList;
}
};
/**
* Adapter between block creation events and block representation which can be
* used by the Scratch runtime.
* @param {Object} e `Blockly.events.create`
* @return {Array.<Object>} List of blocks from this CREATE event.
*/
var adapter = function (e) {
// Validate input
if (typeof e !== 'object') return;
if (typeof e.xml !== 'object') return;
return domToBlocks(html.parseDOM(e.xml.outerHTML));
};
/**
* Convert and an individual block DOM to the representation tree.
@ -50,8 +50,9 @@ function domToBlocks (blocksDOM) {
* @param {Object} blocks Collection of blocks to add to.
* @param {Boolean} isTopBlock Whether blocks at this level are "top blocks."
* @param {?string} parent Parent block ID.
* @return {undefined}
*/
function domToBlock (blockDOM, blocks, isTopBlock, parent) {
var domToBlock = function (blockDOM, blocks, isTopBlock, parent) {
// Block skeleton.
var block = {
id: blockDOM.attribs.id, // Block ID
@ -61,7 +62,7 @@ function domToBlock (blockDOM, blocks, isTopBlock, parent) {
next: null, // Next block in the stack, if one exists.
topLevel: isTopBlock, // If this block starts a stack.
parent: parent, // Parent block ID, if available.
shadow: blockDOM.name == 'shadow', // If this represents a shadow/slot.
shadow: blockDOM.name === 'shadow', // If this represents a shadow/slot.
x: blockDOM.attribs.x, // X position of script, if top-level.
y: blockDOM.attribs.y // Y position of script, if top-level.
};
@ -82,9 +83,9 @@ function domToBlock (blockDOM, blocks, isTopBlock, parent) {
continue;
}
var grandChildNodeName = grandChildNode.name.toLowerCase();
if (grandChildNodeName == 'block') {
if (grandChildNodeName === 'block') {
childBlockNode = grandChildNode;
} else if (grandChildNodeName == 'shadow') {
} else if (grandChildNodeName === 'shadow') {
childShadowNode = grandChildNode;
}
}
@ -117,7 +118,7 @@ function domToBlock (blockDOM, blocks, isTopBlock, parent) {
case 'statement':
// Recursively generate block structure for input block.
domToBlock(childBlockNode, blocks, false, block.id);
if (childShadowNode && childBlockNode != childShadowNode) {
if (childShadowNode && childBlockNode !== childShadowNode) {
// Also generate the shadow block.
domToBlock(childShadowNode, blocks, false, block.id);
}
@ -144,4 +145,6 @@ function domToBlock (blockDOM, blocks, isTopBlock, parent) {
break;
}
}
}
};
module.exports = adapter;

View file

@ -8,7 +8,7 @@ var xmlEscape = require('../util/xml-escape');
* and handle updates from Scratch Blocks events.
*/
function Blocks () {
var Blocks = function () {
/**
* All blocks in the workspace.
* Keys are block IDs, values are metadata about the block.
@ -22,7 +22,7 @@ function Blocks () {
* @type {Array.<String>}
*/
this._scripts = [];
}
};
/**
* Blockly inputs that represent statements/branch.
@ -109,8 +109,8 @@ Blocks.prototype.getInputs = function (id) {
var inputs = {};
for (var input in this._blocks[id].inputs) {
// Ignore blocks prefixed with branch prefix.
if (input.substring(0, Blocks.BRANCH_INPUT_PREFIX.length)
!= Blocks.BRANCH_INPUT_PREFIX) {
if (input.substring(0, Blocks.BRANCH_INPUT_PREFIX.length) !==
Blocks.BRANCH_INPUT_PREFIX) {
inputs[input] = this._blocks[id].inputs[input];
}
}
@ -149,9 +149,9 @@ Blocks.prototype.getTopLevelScript = function (id) {
Blocks.prototype.getProcedureDefinition = function (name) {
for (var id in this._blocks) {
var block = this._blocks[id];
if ((block.opcode == 'procedures_defnoreturn' ||
block.opcode == 'procedures_defreturn') &&
block.mutation.proccode == name) {
if ((block.opcode === 'procedures_defnoreturn' ||
block.opcode === 'procedures_defreturn') &&
block.mutation.proccode === name) {
return id;
}
}
@ -166,9 +166,9 @@ Blocks.prototype.getProcedureDefinition = function (name) {
Blocks.prototype.getProcedureParamNames = function (name) {
for (var id in this._blocks) {
var block = this._blocks[id];
if ((block.opcode == 'procedures_defnoreturn' ||
block.opcode == 'procedures_defreturn') &&
block.mutation.proccode == name) {
if ((block.opcode === 'procedures_defnoreturn' ||
block.opcode === 'procedures_defreturn') &&
block.mutation.proccode === name) {
return JSON.parse(block.mutation.argumentnames);
}
}
@ -181,18 +181,18 @@ Blocks.prototype.getProcedureParamNames = function (name) {
* Create event listener for blocks. Handles validation and serves as a generic
* adapter between the blocks and the runtime interface.
* @param {Object} e Blockly "block" event
* @param {?Runtime} opt_runtime Optional runtime to forward click events to.
* @param {?Runtime} optRuntime Optional runtime to forward click events to.
*/
Blocks.prototype.blocklyListen = function (e, opt_runtime) {
Blocks.prototype.blocklyListen = function (e, optRuntime) {
// Validate event
if (typeof e !== 'object') return;
if (typeof e.blockId !== 'string') return;
// UI event: clicked scripts toggle in the runtime.
if (e.element === 'stackclick') {
if (opt_runtime) {
opt_runtime.toggleScript(e.blockId);
if (optRuntime) {
optRuntime.toggleScript(e.blockId);
}
return;
}
@ -232,8 +232,8 @@ Blocks.prototype.blocklyListen = function (e, opt_runtime) {
return;
}
// Inform any runtime to forget about glows on this script.
if (opt_runtime && this._blocks[e.blockId].topLevel) {
opt_runtime.quietGlow(e.blockId);
if (optRuntime && this._blocks[e.blockId].topLevel) {
optRuntime.quietGlow(e.blockId);
}
this.deleteBlock({
id: e.blockId
@ -273,11 +273,11 @@ Blocks.prototype.changeBlock = function (args) {
if (args.element !== 'field' && args.element !== 'mutation') return;
if (typeof this._blocks[args.id] === 'undefined') return;
if (args.element == 'field') {
if (args.element === 'field') {
// Update block value
if (!this._blocks[args.id].fields[args.name]) return;
this._blocks[args.id].fields[args.name].value = args.value;
} else if (args.element == 'mutation') {
} else if (args.element === 'mutation') {
this._blocks[args.id].mutation = mutationAdapter(args.value);
}
};
@ -298,9 +298,9 @@ Blocks.prototype.moveBlock = function (e) {
}
// Remove from any old parent.
if (e.oldParent !== undefined) {
if (typeof e.oldParent !== 'undefined') {
var oldParent = this._blocks[e.oldParent];
if (e.oldInput !== undefined &&
if (typeof e.oldInput !== 'undefined' &&
oldParent.inputs[e.oldInput].block === e.id) {
// This block was connected to the old parent's input.
oldParent.inputs[e.oldInput].block = null;
@ -312,13 +312,16 @@ Blocks.prototype.moveBlock = function (e) {
}
// Has the block become a top-level block?
if (e.newParent === undefined) {
if (typeof e.newParent === 'undefined') {
this._addScript(e.id);
} else {
// Remove script, if one exists.
this._deleteScript(e.id);
// Otherwise, try to connect it in its new place.
if (e.newInput !== undefined) {
if (typeof e.newInput === 'undefined') {
// Moved to the new parent's next connection.
this._blocks[e.newParent].next = e.id;
} else {
// Moved to the new parent's input.
// Don't obscure the shadow block.
var oldShadow = null;
@ -330,9 +333,6 @@ Blocks.prototype.moveBlock = function (e) {
block: e.id,
shadow: oldShadow
};
} else {
// Moved to the new parent's next connection.
this._blocks[e.newParent].next = e.id;
}
this._blocks[e.id].parent = e.newParent;
}
@ -399,7 +399,7 @@ Blocks.prototype.blockToXML = function (blockId) {
// Encode properties of this block.
var tagName = (block.shadow) ? 'shadow' : 'block';
var xy = (block.topLevel) ?
' x="' + block.x +'"' + ' y="' + block.y +'"' :
' x="' + block.x + '" y="' + block.y + '"' :
'';
var xmlString = '';
xmlString += '<' + tagName +
@ -420,7 +420,7 @@ Blocks.prototype.blockToXML = function (blockId) {
if (blockInput.block) {
xmlString += this.blockToXML(blockInput.block);
}
if (blockInput.shadow && blockInput.shadow != blockInput.block) {
if (blockInput.shadow && blockInput.shadow !== blockInput.block) {
// Obscured shadow.
xmlString += this.blockToXML(blockInput.shadow);
}
@ -453,7 +453,7 @@ Blocks.prototype.blockToXML = function (blockId) {
Blocks.prototype.mutationToXML = function (mutation) {
var mutationString = '<' + mutation.tagName;
for (var prop in mutation) {
if (prop == 'children' || prop == 'tagName') continue;
if (prop === 'children' || prop === 'tagName') continue;
var mutationValue = (typeof mutation[prop] === 'string') ?
xmlEscape(mutation[prop]) : mutation[prop];
mutationString += ' ' + prop + '="' + mutationValue + '"';

View file

@ -1,3 +1,4 @@
var log = require('../util/log');
var Thread = require('./thread');
/**
@ -52,7 +53,7 @@ var execute = function (sequencer, thread) {
if (!opcode) {
console.warn('Could not get opcode for block: ' + currentBlockId);
log.warn('Could not get opcode for block: ' + currentBlockId);
return;
}
@ -90,7 +91,7 @@ var execute = function (sequencer, thread) {
runtime.visualReport(currentBlockId, resolvedValue);
}
// Finished any yields.
thread.setStatus(Thread.STATUS_RUNNING);
thread.status = Thread.STATUS_RUNNING;
}
};
@ -105,14 +106,14 @@ var execute = function (sequencer, thread) {
// Skip through the block (hat with no predicate).
return;
} else {
if (Object.keys(fields).length == 1 &&
Object.keys(inputs).length == 0) {
if (Object.keys(fields).length === 1 &&
Object.keys(inputs).length === 0) {
// One field and no inputs - treat as arg.
for (var fieldKey in fields) { // One iteration.
handleReport(fields[fieldKey].value);
}
} else {
console.warn('Could not get implementation for opcode: ' +
log.warn('Could not get implementation for opcode: ' +
opcode);
}
thread.requestScriptGlowInFrame = true;
@ -133,15 +134,22 @@ var execute = function (sequencer, thread) {
var input = inputs[inputName];
var inputBlockId = input.block;
// Is there no value for this input waiting in the stack frame?
if (typeof currentStackFrame.reported[inputName] === 'undefined') {
if (typeof currentStackFrame.reported[inputName] === 'undefined' &&
inputBlockId) {
// If there's not, we need to evaluate the block.
var reporterYielded = (
sequencer.stepToReporter(thread, inputBlockId, inputName)
);
// If the reporter yielded, return immediately;
// it needs time to finish and report its value.
if (reporterYielded) {
// Push to the stack to evaluate the reporter block.
thread.pushStack(inputBlockId);
// Save name of input for `Thread.pushReportedValue`.
currentStackFrame.waitingReporter = inputName;
// Actually execute the block.
execute(sequencer, thread);
if (thread.status === Thread.STATUS_PROMISE_WAIT) {
return;
} else {
// Execution returned immediately,
// and presumably a value was reported, so pop the stack.
currentStackFrame.waitingReporter = null;
thread.popStack();
}
}
argValues[inputName] = currentStackFrame.reported[inputName];
@ -163,33 +171,26 @@ var execute = function (sequencer, thread) {
primitiveReportedValue = blockFunction(argValues, {
stackFrame: currentStackFrame.executionContext,
target: target,
yield: function() {
thread.setStatus(Thread.STATUS_YIELD);
yield: function () {
thread.status = Thread.STATUS_YIELD;
},
yieldFrame: function() {
thread.setStatus(Thread.STATUS_YIELD_FRAME);
},
done: function() {
thread.setStatus(Thread.STATUS_RUNNING);
sequencer.proceedThread(thread);
},
startBranch: function (branchNum) {
sequencer.stepToBranch(thread, branchNum);
startBranch: function (branchNum, isLoop) {
sequencer.stepToBranch(thread, branchNum, isLoop);
},
stopAll: function () {
runtime.stopAll();
},
stopOtherTargetThreads: function() {
stopOtherTargetThreads: function () {
runtime.stopForTarget(target, thread);
},
stopThread: function() {
stopThread: function () {
sequencer.retireThread(thread);
},
startProcedure: function (procedureName) {
sequencer.stepToProcedure(thread, procedureName);
startProcedure: function (procedureCode) {
sequencer.stepToProcedure(thread, procedureCode);
},
getProcedureParamNames: function (procedureName) {
return blockContainer.getProcedureParamNames(procedureName);
getProcedureParamNames: function (procedureCode) {
return blockContainer.getProcedureParamNames(procedureCode);
},
pushParam: function (paramName, paramValue) {
thread.pushParam(paramName, paramValue);
@ -197,15 +198,20 @@ var execute = function (sequencer, thread) {
getParam: function (paramName) {
return thread.getParam(paramName);
},
startHats: function(requestedHat, opt_matchFields, opt_target) {
startHats: function (requestedHat, optMatchFields, optTarget) {
return (
runtime.startHats(requestedHat, opt_matchFields, opt_target)
runtime.startHats(requestedHat, optMatchFields, optTarget)
);
},
ioQuery: function (device, func, args) {
// Find the I/O device and execute the query/function call.
if (runtime.ioDevices[device] && runtime.ioDevices[device][func]) {
var devObject = runtime.ioDevices[device];
// @todo Figure out why eslint complains about no-useless-call
// no-useless-call can't tell if the call is useless for dynamic
// expressions... or something. Not exactly sure why it
// complains here.
// eslint-disable-next-line no-useless-call
return devObject[func].call(devObject, args);
}
}
@ -221,18 +227,24 @@ var execute = function (sequencer, thread) {
if (isPromise(primitiveReportedValue)) {
if (thread.status === Thread.STATUS_RUNNING) {
// Primitive returned a promise; automatically yield thread.
thread.setStatus(Thread.STATUS_YIELD);
thread.status = Thread.STATUS_PROMISE_WAIT;
}
// Promise handlers
primitiveReportedValue.then(function(resolvedValue) {
primitiveReportedValue.then(function (resolvedValue) {
handleReport(resolvedValue);
sequencer.proceedThread(thread);
}, function(rejectionReason) {
if (typeof resolvedValue === 'undefined') {
var popped = thread.popStack();
var nextBlockId = thread.target.blocks.getNextBlock(popped);
thread.pushStack(nextBlockId);
} else {
thread.popStack();
}
}, function (rejectionReason) {
// Promise rejected: the primitive had some error.
// Log it and proceed.
console.warn('Primitive rejected promise: ', rejectionReason);
thread.setStatus(Thread.STATUS_RUNNING);
sequencer.proceedThread(thread);
log.warn('Primitive rejected promise: ', rejectionReason);
thread.status = Thread.STATUS_RUNNING;
thread.popStack();
});
} else if (thread.status === Thread.STATUS_RUNNING) {
handleReport(primitiveReportedValue);

View file

@ -8,9 +8,9 @@
* @param {Array} contents Contents of the list, as an array.
* @constructor
*/
function List (name, contents) {
var List = function (name, contents) {
this.name = name;
this.contents = contents;
}
};
module.exports = List;

View file

@ -1,12 +1,33 @@
var html = require('htmlparser2');
/**
* Convert a part of a mutation DOM to a mutation VM object, recursively.
* @param {Object} dom DOM object for mutation tag.
* @return {Object} Object representing useful parts of this mutation.
*/
var mutatorTagToObject = function (dom) {
var obj = Object.create(null);
obj.tagName = dom.name;
obj.children = [];
for (var prop in dom.attribs) {
if (prop === 'xmlns') continue;
obj[prop] = dom.attribs[prop];
}
for (var i = 0; i < dom.children.length; i++) {
obj.children.push(
mutatorTagToObject(dom.children[i])
);
}
return obj;
};
/**
* Adapter between mutator XML or DOM and block representation which can be
* used by the Scratch runtime.
* @param {(Object|string)} mutation Mutation XML string or DOM.
* @return {Object} Object representing the mutation.
*/
module.exports = function (mutation) {
var mutationAdpater = function (mutation) {
var mutationParsed;
// Check if the mutation is already parsed; if not, parse it.
if (typeof mutation === 'object') {
@ -17,23 +38,4 @@ module.exports = function (mutation) {
return mutatorTagToObject(mutationParsed);
};
/**
* Convert a part of a mutation DOM to a mutation VM object, recursively.
* @param {Object} dom DOM object for mutation tag.
* @return {Object} Object representing useful parts of this mutation.
*/
function mutatorTagToObject (dom) {
var obj = Object.create(null);
obj.tagName = dom.name;
obj.children = [];
for (var prop in dom.attribs) {
if (prop == 'xmlns') continue;
obj[prop] = dom.attribs[prop];
}
for (var i = 0; i < dom.children.length; i++) {
obj.children.push(
mutatorTagToObject(dom.children[i])
);
}
return obj;
}
module.exports = mutationAdpater;

View file

@ -10,26 +10,24 @@ var Keyboard = require('../io/keyboard');
var Mouse = require('../io/mouse');
var defaultBlockPackages = {
'scratch3_control': require('../blocks/scratch3_control'),
'scratch3_event': require('../blocks/scratch3_event'),
'scratch3_looks': require('../blocks/scratch3_looks'),
'scratch3_motion': require('../blocks/scratch3_motion'),
'scratch3_operators': require('../blocks/scratch3_operators'),
'scratch3_sound': require('../blocks/scratch3_sound'),
'scratch3_sensing': require('../blocks/scratch3_sensing'),
'scratch3_data': require('../blocks/scratch3_data'),
'scratch3_procedures': require('../blocks/scratch3_procedures')
scratch3_control: require('../blocks/scratch3_control'),
scratch3_event: require('../blocks/scratch3_event'),
scratch3_looks: require('../blocks/scratch3_looks'),
scratch3_motion: require('../blocks/scratch3_motion'),
scratch3_operators: require('../blocks/scratch3_operators'),
scratch3_sound: require('../blocks/scratch3_sound'),
scratch3_sensing: require('../blocks/scratch3_sensing'),
scratch3_data: require('../blocks/scratch3_data'),
scratch3_procedures: require('../blocks/scratch3_procedures')
};
/**
* Manages targets, scripts, and the sequencer.
*/
function Runtime () {
var Runtime = function () {
// Bind event emitter
EventEmitter.call(this);
// State for the runtime
/**
* Target management and storage.
* @type {Array.<!Target>}
@ -46,32 +44,111 @@ function Runtime () {
/** @type {!Sequencer} */
this.sequencer = new Sequencer(this);
/**
* Storage container for flyout blocks.
* These will execute on `_editingTarget.`
* @type {!Blocks}
*/
this.flyoutBlocks = new Blocks();
/**
* Currently known editing target for the VM.
* @type {?Target}
*/
this._editingTarget = null;
/**
* Map to look up a block primitive's implementation function by its opcode.
* This is a two-step lookup: package name first, then primitive name.
* @type {Object.<string, Function>}
*/
this._primitives = {};
this._hats = {};
this._edgeActivatedHatValues = {};
this._registerBlockPackages();
this.ioDevices = {
'clock': new Clock(),
'keyboard': new Keyboard(this),
'mouse': new Mouse(this)
};
this._scriptGlowsPreviousFrame = [];
this._editingTarget = null;
/**
* Currently known number of clones.
* Map to look up hat blocks' metadata.
* Keys are opcode for hat, values are metadata objects.
* @type {Object.<string, Object>}
*/
this._hats = {};
/**
* Currently known values for edge-activated hats.
* Keys are block ID for the hat; values are the currently known values.
* @type {Object.<string, *>}
*/
this._edgeActivatedHatValues = {};
/**
* A list of script block IDs that were glowing during the previous frame.
* @type {!Array.<!string>}
*/
this._scriptGlowsPreviousFrame = [];
/**
* Number of threads running during the previous frame
* @type {number}
*/
this._threadCount = 0;
/**
* Currently known number of clones, used to enforce clone limit.
* @type {number}
*/
this._cloneCounter = 0;
}
/**
* Whether the project is in "turbo mode."
* @type {Boolean}
*/
this.turboMode = false;
/**
* Whether the project is in "compatibility mode" (30 TPS).
* @type {Boolean}
*/
this.compatibilityMode = false;
/**
* A reference to the current runtime stepping interval, set
* by a `setInterval`.
* @type {!number}
*/
this._steppingInterval = null;
/**
* Current length of a step.
* Changes as mode switches, and used by the sequencer to calculate
* WORK_TIME.
* @type {!number}
*/
this.currentStepTime = null;
/**
* Whether any primitive has requested a redraw.
* Affects whether `Sequencer.stepThreads` will yield
* after stepping each thread.
* Reset on every frame.
* @type {boolean}
*/
this.redrawRequested = false;
// Register all given block packages.
this._registerBlockPackages();
// Register and initialize "IO devices", containers for processing
// I/O related data.
/** @type {Object.<string, Object>} */
this.ioDevices = {
clock: new Clock(),
keyboard: new Keyboard(this),
mouse: new Mouse(this)
};
};
/**
* Inherit from EventEmitter
*/
util.inherits(Runtime, EventEmitter);
/**
* Width of the stage, in pixels.
@ -89,13 +166,13 @@ Runtime.STAGE_HEIGHT = 360;
* Event name for glowing a script.
* @const {string}
*/
Runtime.SCRIPT_GLOW_ON = 'STACK_GLOW_ON';
Runtime.SCRIPT_GLOW_ON = 'SCRIPT_GLOW_ON';
/**
* Event name for unglowing a script.
* @const {string}
*/
Runtime.SCRIPT_GLOW_OFF = 'STACK_GLOW_OFF';
Runtime.SCRIPT_GLOW_OFF = 'SCRIPT_GLOW_OFF';
/**
* Event name for glowing a block.
@ -109,6 +186,18 @@ Runtime.BLOCK_GLOW_ON = 'BLOCK_GLOW_ON';
*/
Runtime.BLOCK_GLOW_OFF = 'BLOCK_GLOW_OFF';
/**
* Event name for glowing the green flag
* @const {string}
*/
Runtime.PROJECT_RUN_START = 'PROJECT_RUN_START';
/**
* Event name for unglowing the green flag
* @const {string}
*/
Runtime.PROJECT_RUN_STOP = 'PROJECT_RUN_STOP';
/**
* Event name for visual value report.
* @const {string}
@ -116,15 +205,21 @@ Runtime.BLOCK_GLOW_OFF = 'BLOCK_GLOW_OFF';
Runtime.VISUAL_REPORT = 'VISUAL_REPORT';
/**
* Inherit from EventEmitter
* Event name for sprite info report.
* @const {string}
*/
util.inherits(Runtime, EventEmitter);
Runtime.SPRITE_INFO_REPORT = 'SPRITE_INFO_REPORT';
/**
* How rapidly we try to step threads, in ms.
* How rapidly we try to step threads by default, in ms.
*/
Runtime.THREAD_STEP_INTERVAL = 1000 / 60;
/**
* In compatibility mode, how rapidly we try to step threads, in ms.
*/
Runtime.THREAD_STEP_INTERVAL_COMPATIBILITY = 1000 / 30;
/**
* How many clones can be created at a time.
* @const {number}
@ -176,9 +271,6 @@ Runtime.prototype.getOpcodeFunction = function (opcode) {
return this._primitives[opcode];
};
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
/**
* Return whether an opcode represents a hat block.
* @param {!string} opcode The opcode to look up.
@ -236,7 +328,7 @@ Runtime.prototype.attachRenderer = function (renderer) {
*/
Runtime.prototype._pushThread = function (id, target) {
var thread = new Thread(id);
thread.setTarget(target);
thread.target = target;
thread.pushStack(id);
this.threads.push(thread);
return thread;
@ -256,6 +348,24 @@ Runtime.prototype._removeThread = function (thread) {
}
};
/**
* Restart a thread in place, maintaining its position in the list of threads.
* This is used by `startHats` to and is necessary to ensure 2.0-like execution order.
* Test project: https://scratch.mit.edu/projects/130183108/
* @param {!Thread} thread Thread object to restart.
*/
Runtime.prototype._restartThread = function (thread) {
var newThread = new Thread(thread.topBlock);
newThread.target = thread.target;
newThread.pushStack(thread.topBlock);
var i = this.threads.indexOf(thread);
if (i > -1) {
this.threads[i] = newThread;
} else {
this.threads.push(thread);
}
};
/**
* Return whether a thread is currently active/running.
* @param {?Thread} thread Thread object to check.
@ -272,7 +382,7 @@ Runtime.prototype.isActiveThread = function (thread) {
Runtime.prototype.toggleScript = function (topBlockId) {
// Remove any existing thread.
for (var i = 0; i < this.threads.length; i++) {
if (this.threads[i].topBlock == topBlockId) {
if (this.threads[i].topBlock === topBlockId) {
this._removeThread(this.threads[i]);
return;
}
@ -287,12 +397,12 @@ Runtime.prototype.toggleScript = function (topBlockId) {
* - the top block ID of the script.
* - the target that owns the script.
* @param {!Function} f Function to call for each script.
* @param {Target=} opt_target Optionally, a target to restrict to.
* @param {Target=} optTarget Optionally, a target to restrict to.
*/
Runtime.prototype.allScriptsDo = function (f, opt_target) {
Runtime.prototype.allScriptsDo = function (f, optTarget) {
var targets = this.targets;
if (opt_target) {
targets = [opt_target];
if (optTarget) {
targets = [optTarget];
}
for (var t = 0; t < targets.length; t++) {
var target = targets[t];
@ -307,12 +417,12 @@ Runtime.prototype.allScriptsDo = function (f, opt_target) {
/**
* Start all relevant hats.
* @param {!string} requestedHatOpcode Opcode of hats to start.
* @param {Object=} opt_matchFields Optionally, fields to match on the hat.
* @param {Target=} opt_target Optionally, a target to restrict to.
* @param {Object=} optMatchFields Optionally, fields to match on the hat.
* @param {Target=} optTarget Optionally, a target to restrict to.
* @return {Array.<Thread>} List of threads started by this function.
*/
Runtime.prototype.startHats = function (requestedHatOpcode,
opt_matchFields, opt_target) {
optMatchFields, optTarget) {
if (!this._hats.hasOwnProperty(requestedHatOpcode)) {
// No known hat with this opcode.
return;
@ -320,7 +430,7 @@ Runtime.prototype.startHats = function (requestedHatOpcode,
var instance = this;
var newThreads = [];
// Consider all scripts, looking for hats with opcode `requestedHatOpcode`.
this.allScriptsDo(function(topBlockId, target) {
this.allScriptsDo(function (topBlockId, target) {
var potentialHatOpcode = target.blocks.getBlock(topBlockId).opcode;
if (potentialHatOpcode !== requestedHatOpcode) {
// Not the right hat.
@ -332,10 +442,10 @@ Runtime.prototype.startHats = function (requestedHatOpcode,
// (i.e., before the predicate can be run) because "broadcast and wait"
// needs to have a precise collection of started threads.
var hatFields = target.blocks.getFields(topBlockId);
if (opt_matchFields) {
for (var matchField in opt_matchFields) {
if (optMatchFields) {
for (var matchField in optMatchFields) {
if (hatFields[matchField].value !==
opt_matchFields[matchField]) {
optMatchFields[matchField]) {
// Field mismatch.
return;
}
@ -348,8 +458,9 @@ Runtime.prototype.startHats = function (requestedHatOpcode,
// any existing threads starting with the top block.
for (var i = 0; i < instance.threads.length; i++) {
if (instance.threads[i].topBlock === topBlockId &&
instance.threads[i].target == target) {
instance._removeThread(instance.threads[i]);
instance.threads[i].target === target) {
instance._restartThread(instance.threads[i]);
return;
}
}
} else {
@ -357,7 +468,7 @@ Runtime.prototype.startHats = function (requestedHatOpcode,
// give up if any threads with the top block are running.
for (var j = 0; j < instance.threads.length; j++) {
if (instance.threads[j].topBlock === topBlockId &&
instance.threads[j].target == target) {
instance.threads[j].target === target) {
// Some thread is already running.
return;
}
@ -365,7 +476,7 @@ Runtime.prototype.startHats = function (requestedHatOpcode,
}
// Start the thread with this top block.
newThreads.push(instance._pushThread(topBlockId, target));
}, opt_target);
}, optTarget);
return newThreads;
};
@ -394,15 +505,15 @@ Runtime.prototype.disposeTarget = function (disposingTarget) {
/**
* Stop any threads acting on the target.
* @param {!Target} target Target to stop threads for.
* @param {Thread=} opt_threadException Optional thread to skip.
* @param {Thread=} optThreadException Optional thread to skip.
*/
Runtime.prototype.stopForTarget = function (target, opt_threadException) {
Runtime.prototype.stopForTarget = function (target, optThreadException) {
// Stop any threads on the target.
for (var i = 0; i < this.threads.length; i++) {
if (this.threads[i] === opt_threadException) {
if (this.threads[i] === optThreadException) {
continue;
}
if (this.threads[i].target == target) {
if (this.threads[i].target === target) {
this._removeThread(this.threads[i]);
}
}
@ -458,37 +569,72 @@ Runtime.prototype._step = function () {
this.startHats(hatType);
}
}
var inactiveThreads = this.sequencer.stepThreads(this.threads);
this._updateScriptGlows();
for (var i = 0; i < inactiveThreads.length; i++) {
this._removeThread(inactiveThreads[i]);
this.redrawRequested = false;
var doneThreads = this.sequencer.stepThreads();
this._updateGlows(doneThreads);
this._setThreadCount(this.threads.length + doneThreads.length);
if (this.renderer) {
// @todo: Only render when this.redrawRequested or clones rendered.
this.renderer.draw();
}
};
/**
* Set the current editing target known by the runtime.
* @param {!Target} editingTarget New editing target.
*/
Runtime.prototype.setEditingTarget = function (editingTarget) {
this._scriptGlowsPreviousFrame = [];
this._editingTarget = editingTarget;
this._updateScriptGlows();
// Script glows must be cleared.
this._scriptGlowsPreviousFrame = [];
this._updateGlows();
this.spriteInfoReport(editingTarget);
};
Runtime.prototype._updateScriptGlows = function () {
/**
* Set whether we are in 30 TPS compatibility mode.
* @param {boolean} compatibilityModeOn True iff in compatibility mode.
*/
Runtime.prototype.setCompatibilityMode = function (compatibilityModeOn) {
this.compatibilityMode = compatibilityModeOn;
if (this._steppingInterval) {
self.clearInterval(this._steppingInterval);
this.start();
}
};
/**
* Emit glows/glow clears for scripts after a single tick.
* Looks at `this.threads` and notices which have turned on/off new glows.
* @param {Array.<Thread>=} optExtraThreads Optional list of inactive threads.
*/
Runtime.prototype._updateGlows = function (optExtraThreads) {
var searchThreads = [];
searchThreads.push.apply(searchThreads, this.threads);
if (optExtraThreads) {
searchThreads.push.apply(searchThreads, optExtraThreads);
}
// Set of scripts that request a glow this frame.
var requestedGlowsThisFrame = [];
// Final set of scripts glowing during this frame.
var finalScriptGlows = [];
// Find all scripts that should be glowing.
for (var i = 0; i < this.threads.length; i++) {
var thread = this.threads[i];
for (var i = 0; i < searchThreads.length; i++) {
var thread = searchThreads[i];
var target = thread.target;
if (thread.requestScriptGlowInFrame && target == this._editingTarget) {
var blockForThread = thread.peekStack() || thread.topBlock;
var script = target.blocks.getTopLevelScript(blockForThread);
if (!script) {
// Attempt to find in flyout blocks.
script = this.flyoutBlocks.getTopLevelScript(blockForThread);
}
if (script) {
requestedGlowsThisFrame.push(script);
if (target === this._editingTarget) {
var blockForThread = thread.blockGlowInFrame;
if (thread.requestScriptGlowInFrame) {
var script = target.blocks.getTopLevelScript(blockForThread);
if (!script) {
// Attempt to find in flyout blocks.
script = this.flyoutBlocks.getTopLevelScript(
blockForThread
);
}
if (script) {
requestedGlowsThisFrame.push(script);
}
}
}
}
@ -514,6 +660,22 @@ Runtime.prototype._updateScriptGlows = function () {
this._scriptGlowsPreviousFrame = finalScriptGlows;
};
/**
* Emit run start/stop after each tick. Emits when `this.threads.length` goes
* between non-zero and zero
*
* @param {number} threadCount The new threadCount
*/
Runtime.prototype._setThreadCount = function (threadCount) {
if (this._threadCount === 0 && threadCount > 0) {
this.emit(Runtime.PROJECT_RUN_START);
}
if (this._threadCount > 0 && threadCount === 0) {
this.emit(Runtime.PROJECT_RUN_STOP);
}
this._threadCount = threadCount;
};
/**
* "Quiet" a script's glow: stop the VM from generating glow/unglow events
* about that script. Use when a script has just been deleted, but we may
@ -534,9 +696,9 @@ Runtime.prototype.quietGlow = function (scriptBlockId) {
*/
Runtime.prototype.glowBlock = function (blockId, isGlowing) {
if (isGlowing) {
this.emit(Runtime.BLOCK_GLOW_ON, blockId);
this.emit(Runtime.BLOCK_GLOW_ON, {id: blockId});
} else {
this.emit(Runtime.BLOCK_GLOW_OFF, blockId);
this.emit(Runtime.BLOCK_GLOW_OFF, {id: blockId});
}
};
@ -547,9 +709,9 @@ Runtime.prototype.glowBlock = function (blockId, isGlowing) {
*/
Runtime.prototype.glowScript = function (topBlockId, isGlowing) {
if (isGlowing) {
this.emit(Runtime.SCRIPT_GLOW_ON, topBlockId);
this.emit(Runtime.SCRIPT_GLOW_ON, {id: topBlockId});
} else {
this.emit(Runtime.SCRIPT_GLOW_OFF, topBlockId);
this.emit(Runtime.SCRIPT_GLOW_OFF, {id: topBlockId});
}
};
@ -559,7 +721,17 @@ Runtime.prototype.glowScript = function (topBlockId, isGlowing) {
* @param {string} value Value to show associated with the block.
*/
Runtime.prototype.visualReport = function (blockId, value) {
this.emit(Runtime.VISUAL_REPORT, blockId, String(value));
this.emit(Runtime.VISUAL_REPORT, {id: blockId, value: String(value)});
};
/**
* Emit a sprite info report if the provided target is the original sprite
* @param {!Target} target Target to report sprite info for.
*/
Runtime.prototype.spriteInfoReport = function (target) {
if (!target.isOriginal) return;
this.emit(Runtime.SPRITE_INFO_REPORT, target.toJSON());
};
/**
@ -570,7 +742,7 @@ Runtime.prototype.visualReport = function (blockId, value) {
Runtime.prototype.getTargetById = function (targetId) {
for (var i = 0; i < this.targets.length; i++) {
var target = this.targets[i];
if (target.id == targetId) {
if (target.id === targetId) {
return target;
}
}
@ -584,7 +756,7 @@ Runtime.prototype.getTargetById = function (targetId) {
Runtime.prototype.getSpriteTargetByName = function (spriteName) {
for (var i = 0; i < this.targets.length; i++) {
var target = this.targets[i];
if (target.sprite && target.sprite.name == spriteName) {
if (target.sprite && target.sprite.name === spriteName) {
return target;
}
}
@ -620,21 +792,25 @@ Runtime.prototype.getTargetForStage = function () {
};
/**
* Handle an animation frame from the main thread.
* Tell the runtime to request a redraw.
* Use after a clone/sprite has completed some visible operation on the stage.
*/
Runtime.prototype.animationFrame = function () {
if (this.renderer) {
this.renderer.draw();
}
Runtime.prototype.requestRedraw = function () {
this.redrawRequested = true;
};
/**
* Set up timers to repeatedly step in a browser
* Set up timers to repeatedly step in a browser.
*/
Runtime.prototype.start = function () {
self.setInterval(function() {
var interval = Runtime.THREAD_STEP_INTERVAL;
if (this.compatibilityMode) {
interval = Runtime.THREAD_STEP_INTERVAL_COMPATIBILITY;
}
this.currentStepTime = interval;
this._steppingInterval = self.setInterval(function () {
this._step();
}.bind(this), Runtime.THREAD_STEP_INTERVAL);
}.bind(this), interval);
};
module.exports = Runtime;

View file

@ -2,7 +2,7 @@ var Timer = require('../util/timer');
var Thread = require('./thread');
var execute = require('./execute.js');
function Sequencer (runtime) {
var Sequencer = function (runtime) {
/**
* A utility timer for timing thread sequencing.
* @type {!Timer}
@ -14,90 +14,152 @@ function Sequencer (runtime) {
* @type {!Runtime}
*/
this.runtime = runtime;
}
/**
* The sequencer does as much work as it can within WORK_TIME milliseconds,
* then yields. This is essentially a rate-limiter for blocks.
* In Scratch 2.0, this is set to 75% of the target stage frame-rate (30fps).
* @const {!number}
*/
Sequencer.WORK_TIME = 10;
/**
* Step through all threads in `this.threads`, running them in order.
* @param {Array.<Thread>} threads List of which threads to step.
* @return {Array.<Thread>} All threads which have finished in this iteration.
*/
Sequencer.prototype.stepThreads = function (threads) {
// Start counting toward WORK_TIME
this.timer.start();
// List of threads which have been killed by this step.
var inactiveThreads = [];
// If all of the threads are yielding, we should yield.
var numYieldingThreads = 0;
// Clear all yield statuses that were for the previous frame.
for (var t = 0; t < threads.length; t++) {
if (threads[t].status === Thread.STATUS_YIELD_FRAME) {
threads[t].setStatus(Thread.STATUS_RUNNING);
}
}
// While there are still threads to run and we are within WORK_TIME,
// continue executing threads.
while (threads.length > 0 &&
threads.length > numYieldingThreads &&
this.timer.timeElapsed() < Sequencer.WORK_TIME) {
// New threads at the end of the iteration.
var newThreads = [];
// Reset yielding thread count.
numYieldingThreads = 0;
// Attempt to run each thread one time
for (var i = 0; i < threads.length; i++) {
var activeThread = threads[i];
if (activeThread.status === Thread.STATUS_RUNNING) {
// Normal-mode thread: step.
this.startThread(activeThread);
} else if (activeThread.status === Thread.STATUS_YIELD ||
activeThread.status === Thread.STATUS_YIELD_FRAME) {
// Yielding thread: do nothing for this step.
numYieldingThreads++;
}
if (activeThread.stack.length === 0 &&
activeThread.status === Thread.STATUS_DONE) {
// Finished with this thread - tell runtime to clean it up.
inactiveThreads.push(activeThread);
} else {
// Keep this thead in the loop.
newThreads.push(activeThread);
}
}
// Effectively filters out threads that have stopped.
threads = newThreads;
}
return inactiveThreads;
};
/**
* Step the requested thread
* @param {!Thread} thread Thread object to step
* Time to run a warp-mode thread, in ms.
* @type {number}
*/
Sequencer.prototype.startThread = function (thread) {
Sequencer.WARP_TIME = 500;
/**
* Step through all threads in `this.runtime.threads`, running them in order.
* @return {Array.<!Thread>} List of inactive threads after stepping.
*/
Sequencer.prototype.stepThreads = function () {
// Work time is 75% of the thread stepping interval.
var WORK_TIME = 0.75 * this.runtime.currentStepTime;
// Start counting toward WORK_TIME.
this.timer.start();
// Count of active threads.
var numActiveThreads = Infinity;
// Whether `stepThreads` has run through a full single tick.
var ranFirstTick = false;
var doneThreads = [];
// Conditions for continuing to stepping threads:
// 1. We must have threads in the list, and some must be active.
// 2. Time elapsed must be less than WORK_TIME.
// 3. Either turbo mode, or no redraw has been requested by a primitive.
while (this.runtime.threads.length > 0 &&
numActiveThreads > 0 &&
this.timer.timeElapsed() < WORK_TIME &&
(this.runtime.turboMode || !this.runtime.redrawRequested)) {
numActiveThreads = 0;
// Attempt to run each thread one time.
for (var i = 0; i < this.runtime.threads.length; i++) {
var activeThread = this.runtime.threads[i];
if (activeThread.stack.length === 0 ||
activeThread.status === Thread.STATUS_DONE) {
// Finished with this thread.
if (doneThreads.indexOf(activeThread) < 0) {
doneThreads.push(activeThread);
}
continue;
}
if (activeThread.status === Thread.STATUS_YIELD_TICK &&
!ranFirstTick) {
// Clear single-tick yield from the last call of `stepThreads`.
activeThread.status = Thread.STATUS_RUNNING;
}
if (activeThread.status === Thread.STATUS_RUNNING ||
activeThread.status === Thread.STATUS_YIELD) {
// Normal-mode thread: step.
this.stepThread(activeThread);
activeThread.warpTimer = null;
}
if (activeThread.status === Thread.STATUS_RUNNING) {
numActiveThreads++;
}
}
// We successfully ticked once. Prevents running STATUS_YIELD_TICK
// threads on the next tick.
ranFirstTick = true;
}
// Filter inactive threads from `this.runtime.threads`.
this.runtime.threads = this.runtime.threads.filter(function (thread) {
if (doneThreads.indexOf(thread) > -1) {
return false;
}
return true;
});
return doneThreads;
};
/**
* Step the requested thread for as long as necessary.
* @param {!Thread} thread Thread object to step.
*/
Sequencer.prototype.stepThread = function (thread) {
var currentBlockId = thread.peekStack();
if (!currentBlockId) {
// A "null block" - empty branch.
// Yield for the frame.
thread.popStack();
thread.setStatus(Thread.STATUS_YIELD_FRAME);
return;
}
// Execute the current block
execute(this, thread);
// If the block executed without yielding and without doing control flow,
// move to done.
if (thread.status === Thread.STATUS_RUNNING &&
thread.peekStack() === currentBlockId) {
this.proceedThread(thread);
while (thread.peekStack()) {
var isWarpMode = thread.peekStackFrame().warpMode;
if (isWarpMode && !thread.warpTimer) {
// Initialize warp-mode timer if it hasn't been already.
// This will start counting the thread toward `Sequencer.WARP_TIME`.
thread.warpTimer = new Timer();
thread.warpTimer.start();
}
// Execute the current block.
// Save the current block ID to notice if we did control flow.
currentBlockId = thread.peekStack();
execute(this, thread);
thread.blockGlowInFrame = currentBlockId;
// If the thread has yielded or is waiting, yield to other threads.
if (thread.status === Thread.STATUS_YIELD) {
// Mark as running for next iteration.
thread.status = Thread.STATUS_RUNNING;
// In warp mode, yielded blocks are re-executed immediately.
if (isWarpMode &&
thread.warpTimer.timeElapsed() <= Sequencer.WARP_TIME) {
continue;
}
return;
} else if (thread.status === Thread.STATUS_PROMISE_WAIT) {
// A promise was returned by the primitive. Yield the thread
// until the promise resolves. Promise resolution should reset
// thread.status to Thread.STATUS_RUNNING.
return;
}
// If no control flow has happened, switch to next block.
if (thread.peekStack() === currentBlockId) {
thread.goToNextBlock();
}
// If no next block has been found at this point, look on the stack.
while (!thread.peekStack()) {
thread.popStack();
if (thread.stack.length === 0) {
// No more stack to run!
thread.status = Thread.STATUS_DONE;
return;
}
if (thread.peekStackFrame().isLoop) {
// The current level of the stack is marked as a loop.
// Return to yield for the frame/tick in general.
// Unless we're in warp mode - then only return if the
// warp timer is up.
if (!isWarpMode ||
thread.warpTimer.timeElapsed() > Sequencer.WARP_TIME) {
// Don't do anything to the stack, since loops need
// to be re-executed.
return;
} else {
// Don't go to the next block for this level of the stack,
// since loops need to be re-executed.
continue;
}
} else if (thread.peekStackFrame().waitingReporter) {
// This level of the stack was waiting for a value.
// This means a reporter has just returned - so don't go
// to the next block for this level of the stack.
return;
}
// Get next block of existing block on the stack.
thread.goToNextBlock();
}
}
};
@ -105,8 +167,9 @@ Sequencer.prototype.startThread = function (thread) {
* Step a thread into a block's branch.
* @param {!Thread} thread Thread object to step to branch.
* @param {Number} branchNum Which branch to step to (i.e., 1, 2).
* @param {Boolean} isLoop Whether this block is a loop.
*/
Sequencer.prototype.stepToBranch = function (thread, branchNum) {
Sequencer.prototype.stepToBranch = function (thread, branchNum, isLoop) {
if (!branchNum) {
branchNum = 1;
}
@ -115,11 +178,11 @@ Sequencer.prototype.stepToBranch = function (thread, branchNum) {
currentBlockId,
branchNum
);
thread.peekStackFrame().isLoop = isLoop;
if (branchId) {
// Push branch ID to the thread's stack.
thread.pushStack(branchId);
} else {
// Push null, so we come back to the current block.
thread.pushStack(null);
}
};
@ -127,56 +190,39 @@ Sequencer.prototype.stepToBranch = function (thread, branchNum) {
/**
* Step a procedure.
* @param {!Thread} thread Thread object to step to procedure.
* @param {!string} procedureName Name of procedure defined in this target.
* @param {!string} procedureCode Procedure code of procedure to step to.
*/
Sequencer.prototype.stepToProcedure = function (thread, procedureName) {
var definition = thread.target.blocks.getProcedureDefinition(procedureName);
Sequencer.prototype.stepToProcedure = function (thread, procedureCode) {
var definition = thread.target.blocks.getProcedureDefinition(procedureCode);
if (!definition) {
return;
}
// Check if the call is recursive.
// If so, set the thread to yield after pushing.
var isRecursive = thread.isRecursiveCall(procedureCode);
// To step to a procedure, we put its definition on the stack.
// Execution for the thread will proceed through the definition hat
// and on to the main definition of the procedure.
// When that set of blocks finishes executing, it will be popped
// from the stack by the sequencer, returning control to the caller.
thread.pushStack(definition);
// Check if the call is recursive. If so, yield.
// @todo: Have behavior match Scratch 2.0.
if (thread.stack.indexOf(definition) > -1) {
thread.setStatus(Thread.STATUS_YIELD_FRAME);
}
};
/**
* Step a thread into an input reporter, and manage its status appropriately.
* @param {!Thread} thread Thread object to step to reporter.
* @param {!string} blockId ID of reporter block.
* @param {!string} inputName Name of input on parent block.
* @return {boolean} True if yielded, false if it finished immediately.
*/
Sequencer.prototype.stepToReporter = function (thread, blockId, inputName) {
var currentStackFrame = thread.peekStackFrame();
// Push to the stack to evaluate the reporter block.
thread.pushStack(blockId);
// Save name of input for `Thread.pushReportedValue`.
currentStackFrame.waitingReporter = inputName;
// Actually execute the block.
this.startThread(thread);
// If a reporter yielded, caller must wait for it to unyield.
// The value will be populated once the reporter unyields,
// and passed up to the currentStackFrame on next execution.
return thread.status === Thread.STATUS_YIELD;
};
/**
* Finish stepping a thread and proceed it to the next block.
* @param {!Thread} thread Thread object to proceed.
*/
Sequencer.prototype.proceedThread = function (thread) {
var currentBlockId = thread.peekStack();
// Mark the status as done and proceed to the next block.
// Pop from the stack - finished this level of execution.
thread.popStack();
// Push next connected block, if there is one.
var nextBlockId = thread.target.blocks.getNextBlock(currentBlockId);
if (nextBlockId) {
thread.pushStack(nextBlockId);
}
// If we can't find a next block to run, mark the thread as done.
if (!thread.peekStack()) {
thread.setStatus(Thread.STATUS_DONE);
// In known warp-mode threads, only yield when time is up.
if (thread.peekStackFrame().warpMode &&
thread.warpTimer.timeElapsed() > Sequencer.WARP_TIME) {
thread.status = Thread.STATUS_YIELD;
} else {
// Look for warp-mode flag on definition, and set the thread
// to warp-mode if needed.
var definitionBlock = thread.target.blocks.getBlock(definition);
var doWarp = definitionBlock.mutation.warp;
if (doWarp) {
thread.peekStackFrame().warpMode = true;
} else {
// In normal-mode threads, yield any time we have a recursive call.
if (isRecursive) {
thread.status = Thread.STATUS_YIELD;
}
}
}
};
@ -188,7 +234,7 @@ Sequencer.prototype.retireThread = function (thread) {
thread.stack = [];
thread.stackFrame = [];
thread.requestScriptGlowInFrame = false;
thread.setStatus(Thread.STATUS_DONE);
thread.status = Thread.STATUS_DONE;
};
module.exports = Sequencer;

View file

@ -13,7 +13,7 @@ var uid = require('../util/uid');
* @param {?Blocks} blocks Blocks instance for the blocks owned by this target.
* @constructor
*/
function Target (blocks) {
var Target = function (blocks) {
if (!blocks) {
blocks = new Blocks(this);
}
@ -39,7 +39,7 @@ function Target (blocks) {
* @type {Object.<string,*>}
*/
this.lists = {};
}
};
/**
* Called when the project receives a "green flag."
@ -105,12 +105,17 @@ Target.prototype.lookupOrCreateList = function (name) {
return newList;
};
/**
* Post/edit sprite info.
* @param {object} data An object with sprite info data to set.
* @abstract
*/
Target.prototype.postSpriteInfo = function () {};
/**
* Call to destroy a target.
* @abstract
*/
Target.prototype.dispose = function () {
};
Target.prototype.dispose = function () {};
module.exports = Target;

View file

@ -3,7 +3,7 @@
* @param {?string} firstBlock First block to execute in the thread.
* @constructor
*/
function Thread (firstBlock) {
var Thread = function (firstBlock) {
/**
* ID of top block of the thread
* @type {!string}
@ -40,7 +40,20 @@ function Thread (firstBlock) {
* @type {boolean}
*/
this.requestScriptGlowInFrame = false;
}
/**
* Which block ID should glow during this frame, if any.
* @type {?string}
*/
this.blockGlowInFrame = null;
/**
* A timer for when the thread enters warp mode.
* Substitutes the sequencer's count toward WORK_TIME on a per-thread basis.
* @type {?Timer}
*/
this.warpTimer = null;
};
/**
* Thread status for initialized or running thread.
@ -51,25 +64,31 @@ function Thread (firstBlock) {
Thread.STATUS_RUNNING = 0;
/**
* Thread status for a yielded thread.
* Threads are in this state when a primitive has yielded; execution is paused
* until the relevant primitive unyields.
* Threads are in this state when a primitive is waiting on a promise;
* execution is paused until the promise changes thread status.
* @const
*/
Thread.STATUS_YIELD = 1;
Thread.STATUS_PROMISE_WAIT = 1;
/**
* Thread status for a single-frame yield.
* Thread status for yield.
* @const
*/
Thread.STATUS_YIELD_FRAME = 2;
Thread.STATUS_YIELD = 2;
/**
* Thread status for a single-tick yield. This will be cleared when the
* thread is resumed.
* @const
*/
Thread.STATUS_YIELD_TICK = 3;
/**
* Thread status for a finished/done thread.
* Thread is in this state when there are no more blocks to execute.
* @const
*/
Thread.STATUS_DONE = 3;
Thread.STATUS_DONE = 4;
/**
* Push stack and update stack frames appropriately.
@ -80,7 +99,14 @@ Thread.prototype.pushStack = function (blockId) {
// Push an empty stack frame, if we need one.
// Might not, if we just popped the stack.
if (this.stack.length > this.stackFrames.length) {
// Copy warp mode from any higher level.
var warpMode = false;
if (this.stackFrames[this.stackFrames.length - 1]) {
warpMode = this.stackFrames[this.stackFrames.length - 1].warpMode;
}
this.stackFrames.push({
isLoop: false, // Whether this level of the stack is a loop.
warpMode: warpMode, // Whether this level is in warp mode.
reported: {}, // Collects reported input values.
waitingReporter: null, // Name of waiting reporter.
params: {}, // Procedure parameters.
@ -125,22 +151,32 @@ Thread.prototype.peekParentStackFrame = function () {
/**
* Push a reported value to the parent of the current stack frame.
* @param {!Any} value Reported value to push.
* @param {*} value Reported value to push.
*/
Thread.prototype.pushReportedValue = function (value) {
var parentStackFrame = this.peekParentStackFrame();
if (parentStackFrame) {
var waitingReporter = parentStackFrame.waitingReporter;
parentStackFrame.reported[waitingReporter] = value;
parentStackFrame.waitingReporter = null;
}
};
/**
* Add a parameter to the stack frame.
* Use when calling a procedure with parameter values.
* @param {!string} paramName Name of parameter.
* @param {*} value Value to set for parameter.
*/
Thread.prototype.pushParam = function (paramName, value) {
var stackFrame = this.peekStackFrame();
stackFrame.params[paramName] = value;
};
/**
* Get a parameter at the lowest possible level of the stack.
* @param {!string} paramName Name of parameter.
* @return {*} value Value for parameter.
*/
Thread.prototype.getParam = function (paramName) {
for (var i = this.stackFrames.length - 1; i >= 0; i--) {
var frame = this.stackFrames[i];
@ -159,28 +195,43 @@ Thread.prototype.atStackTop = function () {
return this.peekStack() === this.topBlock;
};
/**
* Set thread status.
* @param {number} status Enum representing thread status.
* Switch the thread to the next block at the current level of the stack.
* For example, this is used in a standard sequence of blocks,
* where execution proceeds from one block to the next.
*/
Thread.prototype.setStatus = function (status) {
this.status = status;
Thread.prototype.goToNextBlock = function () {
var nextBlockId = this.target.blocks.getNextBlock(this.peekStack());
// Copy warp mode to next block.
var warpMode = this.peekStackFrame().warpMode;
// The current block is on the stack - pop it and push the next.
// Note that this could push `null` - that is handled by the sequencer.
this.popStack();
this.pushStack(nextBlockId);
if (this.peekStackFrame()) {
this.peekStackFrame().warpMode = warpMode;
}
};
/**
* Set thread target.
* @param {?Target} target Target for this thread.
* Attempt to determine whether a procedure call is recursive,
* by examining the stack.
* @param {!string} procedureCode Procedure code of procedure being called.
* @return {boolean} True if the call appears recursive.
*/
Thread.prototype.setTarget = function (target) {
this.target = target;
};
/**
* Get thread target.
* @return {?Target} Target for this thread, if available.
*/
Thread.prototype.getTarget = function () {
return this.target;
Thread.prototype.isRecursiveCall = function (procedureCode) {
var callCount = 5; // Max number of enclosing procedure calls to examine.
var sp = this.stack.length - 1;
for (var i = sp - 1; i >= 0; i--) {
var block = this.target.blocks.getBlock(this.stack[i]);
if (block.opcode === 'procedures_callnoreturn' &&
block.mutation.proccode === procedureCode) {
return true;
}
if (--callCount < 0) return false;
}
return false;
};
module.exports = Thread;

View file

@ -9,10 +9,10 @@
* @param {boolean} isCloud Whether the variable is stored in the cloud.
* @constructor
*/
function Variable (name, value, isCloud) {
var Variable = function (name, value, isCloud) {
this.name = name;
this.value = value;
this.isCloud = isCloud;
}
};
module.exports = Variable;

View file

@ -6,35 +6,23 @@
*/
var Blocks = require('../engine/blocks');
var Clone = require('../sprites/clone');
var RenderedTarget = require('../sprites/rendered-target');
var Sprite = require('../sprites/sprite');
var Color = require('../util/color.js');
var log = require('../util/log');
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,
* and process the top-level object (the stage object).
* @param {!string} json SB2-format JSON to load.
* @param {!Runtime} runtime Runtime object to load all structures into.
*/
function sb2import (json, runtime) {
parseScratchObject(
JSON.parse(json),
runtime,
true
);
}
/**
* Parse a single "Scratch object" and create all its in-memory VM objects.
* @param {!Object} object From-JSON "Scratch object:" sprite, stage, watcher.
* @param {!Runtime} runtime Runtime object to load all structures into.
* @param {boolean} topLevel Whether this is the top-level object (stage).
* @return {?Target} Target created (stage or sprite).
*/
function parseScratchObject (object, runtime, topLevel) {
var parseScratchObject = function (object, runtime, topLevel) {
if (!object.hasOwnProperty('objName')) {
// Watcher/monitor - skip this object until those are implemented in VM.
// @todo
@ -54,8 +42,8 @@ function parseScratchObject (object, runtime, topLevel) {
var costume = object.costumes[i];
// @todo: Make sure all the relevant metadata is being pulled out.
sprite.costumes.push({
skin: 'https://cdn.assets.scratch.mit.edu/internalapi/asset/'
+ costume.baseLayerMD5 + '/get/',
skin: 'https://cdn.assets.scratch.mit.edu/internalapi/asset/' +
costume.baseLayerMD5 + '/get/',
name: costume.costumeName,
bitmapResolution: costume.bitmapResolution,
rotationCenterX: costume.rotationCenterX,
@ -127,12 +115,12 @@ function parseScratchObject (object, runtime, topLevel) {
target.currentCostume = Math.round(object.currentCostumeIndex);
}
if (object.hasOwnProperty('rotationStyle')) {
if (object.rotationStyle == 'none') {
target.rotationStyle = Clone.ROTATION_STYLE_NONE;
} else if (object.rotationStyle == 'leftRight') {
target.rotationStyle = Clone.ROTATION_STYLE_LEFT_RIGHT;
} else if (object.rotationStyle == 'normal') {
target.rotationStyle = Clone.ROTATION_STYLE_ALL_AROUND;
if (object.rotationStyle === 'none') {
target.rotationStyle = RenderedTarget.ROTATION_STYLE_NONE;
} else if (object.rotationStyle === 'leftRight') {
target.rotationStyle = RenderedTarget.ROTATION_STYLE_LEFT_RIGHT;
} else if (object.rotationStyle === 'normal') {
target.rotationStyle = RenderedTarget.ROTATION_STYLE_ALL_AROUND;
}
}
target.isStage = topLevel;
@ -143,7 +131,24 @@ function parseScratchObject (object, runtime, topLevel) {
parseScratchObject(object.children[m], runtime, false);
}
}
}
return target;
};
/**
* Top-level handler. Parse provided JSON,
* and process the top-level object (the stage object).
* @param {!string} json SB2-format JSON to load.
* @param {!Runtime} runtime Runtime object to load all structures into.
* @param {Boolean=} optForceSprite If set, treat as sprite (Sprite2).
* @return {?Target} Top-level target created (stage or sprite).
*/
var sb2import = function (json, runtime, optForceSprite) {
return parseScratchObject(
JSON.parse(json),
runtime,
!optForceSprite
);
};
/**
* Parse a Scratch object's scripts into VM blocks.
@ -151,7 +156,7 @@ function parseScratchObject (object, runtime, topLevel) {
* @param {!Object} scripts Scripts object from SB2 JSON.
* @param {!Blocks} blocks Blocks object to load parsed blocks into.
*/
function parseScripts (scripts, blocks) {
var parseScripts = function (scripts, blocks) {
for (var i = 0; i < scripts.length; i++) {
var script = scripts[i];
var scriptX = script[0];
@ -173,7 +178,7 @@ function parseScripts (scripts, blocks) {
blocks.createBlock(convertedBlocks[j]);
}
}
}
};
/**
* Parse any list of blocks from SB2 JSON into a list of VM-format blocks.
@ -183,7 +188,7 @@ function parseScripts (scripts, blocks) {
* @param {Array.<Object>} blockList SB2 JSON-format block list.
* @return {Array.<Object>} Scratch VM-format block list.
*/
function parseBlockList (blockList) {
var parseBlockList = function (blockList) {
var resultingList = [];
var previousBlock = null; // For setting next.
for (var i = 0; i < blockList.length; i++) {
@ -197,7 +202,7 @@ function parseBlockList (blockList) {
resultingList.push(parsedBlock);
}
return resultingList;
}
};
/**
* Flatten a block tree into a block list.
@ -205,7 +210,7 @@ function parseBlockList (blockList) {
* @param {Array.<Object>} blocks list generated by `parseBlockList`.
* @return {Array.<Object>} Flattened list to be passed to `blocks.createBlock`.
*/
function flatten (blocks) {
var flatten = function (blocks) {
var finalBlocks = [];
for (var i = 0; i < blocks.length; i++) {
var block = blocks[i];
@ -216,7 +221,7 @@ function flatten (blocks) {
delete block.children;
}
return finalBlocks;
}
};
/**
* Convert a Scratch 2.0 procedure string (e.g., "my_procedure %s %b %n")
@ -225,44 +230,44 @@ function flatten (blocks) {
* @param {string} procCode Scratch 2.0 procedure string.
* @return {Object} Argument map compatible with those in sb2specmap.
*/
function parseProcedureArgMap (procCode) {
var parseProcedureArgMap = function (procCode) {
var argMap = [
{} // First item in list is op string.
];
var INPUT_PREFIX = 'input';
var inputCount = 0;
// Split by %n, %b, %s.
var parts = procCode.split(/(?=[^\\]\%[nbs])/);
var parts = procCode.split(/(?=[^\\]%[nbs])/);
for (var i = 0; i < parts.length; i++) {
var part = parts[i].trim();
if (part.substring(0, 1) == '%') {
if (part.substring(0, 1) === '%') {
var argType = part.substring(1, 2);
var arg = {
type: 'input',
inputName: INPUT_PREFIX + (inputCount++)
};
if (argType == 'n') {
if (argType === 'n') {
arg.inputOp = 'math_number';
} else if (argType == 's') {
} else if (argType === 's') {
arg.inputOp = 'text';
}
argMap.push(arg);
}
}
return argMap;
}
};
/**
* Parse a single SB2 JSON-formatted block and its children.
* @param {!Object} sb2block SB2 JSON-formatted block.
* @return {Object} Scratch VM format block.
*/
function parseBlock (sb2block) {
var parseBlock = function (sb2block) {
// First item in block object is the old opcode (e.g., 'forward:').
var oldOpcode = sb2block[0];
// Convert the block using the specMap. See sb2specmap.js.
if (!oldOpcode || !specMap[oldOpcode]) {
console.warn('Couldn\'t find SB2 block: ', oldOpcode);
log.warn('Couldn\'t find SB2 block: ', oldOpcode);
return;
}
var blockMetadata = specMap[oldOpcode];
@ -277,7 +282,7 @@ function parseBlock (sb2block) {
children: [] // Store any generated children, flattened in `flatten`.
};
// For a procedure call, generate argument map from proc string.
if (oldOpcode == 'call') {
if (oldOpcode === 'call') {
blockMetadata.argMap = parseProcedureArgMap(sb2block[1]);
}
// Look at the expected arguments in `blockMetadata.argMap.`
@ -289,7 +294,7 @@ function parseBlock (sb2block) {
// Whether the input is obscuring a shadow.
var shadowObscured = false;
// Positional argument is an input.
if (expectedArg.type == 'input') {
if (expectedArg.type === 'input') {
// Create a new block and input metadata.
var inputUid = uid();
activeBlock.inputs[expectedArg.inputName] = {
@ -297,10 +302,10 @@ function parseBlock (sb2block) {
block: null,
shadow: null
};
if (typeof providedArg == 'object' && providedArg) {
if (typeof providedArg === 'object' && providedArg) {
// Block or block list occupies the input.
var innerBlocks;
if (typeof providedArg[0] == 'object' && providedArg[0]) {
if (typeof providedArg[0] === 'object' && providedArg[0]) {
// Block list occupies the input.
innerBlocks = parseBlockList(providedArg);
} else {
@ -309,7 +314,7 @@ function parseBlock (sb2block) {
}
var previousBlock = null;
for (var j = 0; j < innerBlocks.length; j++) {
if (j == 0) {
if (j === 0) {
innerBlocks[j].parent = activeBlock.id;
} else {
innerBlocks[j].parent = previousBlock;
@ -335,28 +340,31 @@ function parseBlock (sb2block) {
var fieldValue = providedArg;
// Shadows' field names match the input name, except for these:
var fieldName = expectedArg.inputName;
if (expectedArg.inputOp == 'math_number' ||
expectedArg.inputOp == 'math_whole_number' ||
expectedArg.inputOp == 'math_positive_number' ||
expectedArg.inputOp == 'math_integer' ||
expectedArg.inputOp == 'math_angle') {
if (expectedArg.inputOp === 'math_number' ||
expectedArg.inputOp === 'math_whole_number' ||
expectedArg.inputOp === 'math_positive_number' ||
expectedArg.inputOp === 'math_integer' ||
expectedArg.inputOp === 'math_angle') {
fieldName = 'NUM';
// Fields are given Scratch 2.0 default values if obscured.
if (shadowObscured) {
fieldValue = 10;
}
} else if (expectedArg.inputOp == 'text') {
} else if (expectedArg.inputOp === 'text') {
fieldName = 'TEXT';
if (shadowObscured) {
fieldValue = '';
}
} else if (expectedArg.inputOp == 'colour_picker') {
} else if (expectedArg.inputOp === 'colour_picker') {
// Convert SB2 color to hex.
fieldValue = Color.decimalToHex(providedArg);
fieldName = 'COLOUR';
if (shadowObscured) {
fieldValue = '#990000';
}
} else if (shadowObscured) {
// Filled drop-down menu.
fieldValue = '';
}
var fields = {};
fields[fieldName] = {
@ -378,7 +386,7 @@ function parseBlock (sb2block) {
if (!activeBlock.inputs[expectedArg.inputName].block) {
activeBlock.inputs[expectedArg.inputName].block = inputUid;
}
} else if (expectedArg.type == 'field') {
} else if (expectedArg.type === 'field') {
// Add as a field on this block.
activeBlock.fields[expectedArg.fieldName] = {
name: expectedArg.fieldName,
@ -387,18 +395,18 @@ function parseBlock (sb2block) {
}
}
// Special cases to generate mutations.
if (oldOpcode == 'stopScripts') {
if (oldOpcode === 'stopScripts') {
// Mutation for stop block: if the argument is 'other scripts',
// the block needs a next connection.
if (sb2block[1] == 'other scripts in sprite' ||
sb2block[1] == 'other scripts in stage') {
if (sb2block[1] === 'other scripts in sprite' ||
sb2block[1] === 'other scripts in stage') {
activeBlock.mutation = {
tagName: 'mutation',
hasnext: 'true',
children: []
};
}
} else if (oldOpcode == 'procDef') {
} else if (oldOpcode === 'procDef') {
// Mutation for procedure definition:
// store all 2.0 proc data.
var procData = sb2block.slice(1);
@ -410,7 +418,7 @@ function parseBlock (sb2block) {
warp: procData[3], // Warp mode, e.g., true/false.
children: []
};
} else if (oldOpcode == 'call') {
} else if (oldOpcode === 'call') {
// Mutation for procedure call:
// string for proc code (e.g., "abc %n %b %s").
activeBlock.mutation = {
@ -418,7 +426,7 @@ function parseBlock (sb2block) {
children: [],
proccode: sb2block[1]
};
} else if (oldOpcode == 'getParam') {
} else if (oldOpcode === 'getParam') {
// Mutation for procedure parameter.
activeBlock.mutation = {
tagName: 'mutation',
@ -428,6 +436,6 @@ function parseBlock (sb2block) {
};
}
return activeBlock;
}
};
module.exports = sb2import;

File diff suppressed because it is too large Load diff

View file

@ -3,15 +3,13 @@ var util = require('util');
var Runtime = require('./engine/runtime');
var sb2import = require('./import/sb2import');
var Sprite = require('./sprites/sprite');
var Blocks = require('./engine/blocks');
/**
* Handles connections between blocks, stage, and extensions.
*
* @author Andrew Sliwinski <ascii@media.mit.edu>
*/
function VirtualMachine () {
var VirtualMachine = function () {
var instance = this;
// Bind event emitter and runtime to VM instance
EventEmitter.call(instance);
@ -27,25 +25,34 @@ function VirtualMachine () {
*/
instance.editingTarget = null;
// Runtime emits are passed along as VM emits.
instance.runtime.on(Runtime.SCRIPT_GLOW_ON, function (id) {
instance.emit(Runtime.SCRIPT_GLOW_ON, {id: id});
instance.runtime.on(Runtime.SCRIPT_GLOW_ON, function (glowData) {
instance.emit(Runtime.SCRIPT_GLOW_ON, glowData);
});
instance.runtime.on(Runtime.SCRIPT_GLOW_OFF, function (id) {
instance.emit(Runtime.SCRIPT_GLOW_OFF, {id: id});
instance.runtime.on(Runtime.SCRIPT_GLOW_OFF, function (glowData) {
instance.emit(Runtime.SCRIPT_GLOW_OFF, glowData);
});
instance.runtime.on(Runtime.BLOCK_GLOW_ON, function (id) {
instance.emit(Runtime.BLOCK_GLOW_ON, {id: id});
instance.runtime.on(Runtime.BLOCK_GLOW_ON, function (glowData) {
instance.emit(Runtime.BLOCK_GLOW_ON, glowData);
});
instance.runtime.on(Runtime.BLOCK_GLOW_OFF, function (id) {
instance.emit(Runtime.BLOCK_GLOW_OFF, {id: id});
instance.runtime.on(Runtime.BLOCK_GLOW_OFF, function (glowData) {
instance.emit(Runtime.BLOCK_GLOW_OFF, glowData);
});
instance.runtime.on(Runtime.VISUAL_REPORT, function (id, value) {
instance.emit(Runtime.VISUAL_REPORT, {id: id, value: value});
instance.runtime.on(Runtime.PROJECT_RUN_START, function () {
instance.emit(Runtime.PROJECT_RUN_START);
});
instance.runtime.on(Runtime.PROJECT_RUN_STOP, function () {
instance.emit(Runtime.PROJECT_RUN_STOP);
});
instance.runtime.on(Runtime.VISUAL_REPORT, function (visualReport) {
instance.emit(Runtime.VISUAL_REPORT, visualReport);
});
instance.runtime.on(Runtime.SPRITE_INFO_REPORT, function (spriteInfo) {
instance.emit(Runtime.SPRITE_INFO_REPORT, spriteInfo);
});
this.blockListener = this.blockListener.bind(this);
this.flyoutBlockListener = this.flyoutBlockListener.bind(this);
}
};
/**
* Inherit from EventEmitter
@ -66,6 +73,24 @@ VirtualMachine.prototype.greenFlag = function () {
this.runtime.greenFlag();
};
/**
* Set whether the VM is in "turbo mode."
* When true, loops don't yield to redraw.
* @param {Boolean} turboModeOn Whether turbo mode should be set.
*/
VirtualMachine.prototype.setTurboMode = function (turboModeOn) {
this.runtime.turboMode = !!turboModeOn;
};
/**
* Set whether the VM is in 2.0 "compatibility mode."
* When true, ticks go at 2.0 speed (30 TPS).
* @param {Boolean} compatibilityModeOn Whether compatibility mode is set.
*/
VirtualMachine.prototype.setCompatibilityMode = function (compatibilityModeOn) {
this.runtime.setCompatibilityMode(!!compatibilityModeOn);
};
/**
* Stop all threads and running activities.
*/
@ -88,12 +113,12 @@ VirtualMachine.prototype.clear = function () {
VirtualMachine.prototype.getPlaygroundData = function () {
var instance = this;
// Only send back thread data for the current editingTarget.
var threadData = this.runtime.threads.filter(function(thread) {
return thread.target == instance.editingTarget;
var threadData = this.runtime.threads.filter(function (thread) {
return thread.target === instance.editingTarget;
});
// Remove the target key, since it's a circular reference.
var filteredThreadData = JSON.stringify(threadData, function(key, value) {
if (key == 'target') return undefined;
var filteredThreadData = JSON.stringify(threadData, function (key, value) {
if (key === 'target') return;
return value;
}, 2);
this.emit('playgroundData', {
@ -102,13 +127,6 @@ VirtualMachine.prototype.getPlaygroundData = function () {
});
};
/**
* Handle an animation frame.
*/
VirtualMachine.prototype.animationFrame = function () {
this.runtime.animationFrame();
};
/**
* Post I/O data to the virtual devices.
* @param {?string} device Name of virtual I/O device.
@ -128,8 +146,8 @@ VirtualMachine.prototype.loadProject = function (json) {
this.clear();
// @todo: Handle other formats, e.g., Scratch 1.4, Scratch 3.0.
sb2import(json, this.runtime);
// Select the first target for editing, e.g., the stage.
this.editingTarget = this.runtime.targets[0];
// Select the first target for editing, e.g., the first sprite.
this.editingTarget = this.runtime.targets[1];
// Update the VM user's knowledge of targets and blocks on the workspace.
this.emitTargetsUpdate();
this.emitWorkspaceUpdate();
@ -137,50 +155,92 @@ VirtualMachine.prototype.loadProject = function (json) {
};
/**
* Temporary way to make an empty project, in case the desired project
* cannot be loaded from the online server.
* Add a single sprite from the "Sprite2" (i.e., SB2 sprite) format.
* @param {?string} json JSON string representing the sprite.
*/
VirtualMachine.prototype.createEmptyProject = function () {
// Stage.
var blocks2 = new Blocks();
var stage = new Sprite(blocks2, this.runtime);
stage.name = 'Stage';
stage.costumes.push({
skin: './assets/stage.png',
name: 'backdrop1',
bitmapResolution: 2,
rotationCenterX: 480,
rotationCenterY: 360
});
var target2 = stage.createClone();
this.runtime.targets.push(target2);
target2.x = 0;
target2.y = 0;
target2.direction = 90;
target2.size = 200;
target2.visible = true;
target2.isStage = true;
// Sprite1 (cat).
var blocks1 = new Blocks();
var sprite = new Sprite(blocks1, this.runtime);
sprite.name = 'Sprite1';
sprite.costumes.push({
skin: './assets/scratch_cat.svg',
name: 'costume1',
bitmapResolution: 1,
rotationCenterX: 47,
rotationCenterY: 55
});
var target1 = sprite.createClone();
this.runtime.targets.push(target1);
target1.x = 0;
target1.y = 0;
target1.direction = 90;
target1.size = 100;
target1.visible = true;
this.editingTarget = this.runtime.targets[0];
VirtualMachine.prototype.addSprite2 = function (json) {
// Select new sprite.
this.editingTarget = sb2import(json, this.runtime, true);
// Update the VM user's knowledge of targets and blocks on the workspace.
this.emitTargetsUpdate();
this.emitWorkspaceUpdate();
this.runtime.setEditingTarget(this.editingTarget);
};
/**
* Add a costume to the current editing target.
* @param {!Object} costumeObject Object representing the costume.
*/
VirtualMachine.prototype.addCostume = function (costumeObject) {
this.editingTarget.sprite.costumes.push(costumeObject);
// Switch to the costume.
this.editingTarget.setCostume(
this.editingTarget.sprite.costumes.length - 1
);
};
/**
* Add a backdrop to the stage.
* @param {!Object} backdropObject Object representing the backdrop.
*/
VirtualMachine.prototype.addBackdrop = function (backdropObject) {
var stage = this.runtime.getTargetForStage();
stage.sprite.costumes.push(backdropObject);
// Switch to the backdrop.
stage.setCostume(stage.sprite.costumes.length - 1);
};
/**
* Rename a sprite.
* @param {string} targetId ID of a target whose sprite to rename.
* @param {string} newName New name of the sprite.
*/
VirtualMachine.prototype.renameSprite = function (targetId, newName) {
var target = this.runtime.getTargetById(targetId);
if (target) {
if (!target.isSprite()) {
throw new Error('Cannot rename non-sprite targets.');
}
var sprite = target.sprite;
if (!sprite) {
throw new Error('No sprite associated with this target.');
}
sprite.name = newName;
this.emitTargetsUpdate();
} else {
throw new Error('No target with the provided id.');
}
};
/**
* Delete a sprite and all its clones.
* @param {string} targetId ID of a target whose sprite to delete.
*/
VirtualMachine.prototype.deleteSprite = function (targetId) {
var target = this.runtime.getTargetById(targetId);
if (target) {
if (!target.isSprite()) {
throw new Error('Cannot delete non-sprite targets.');
}
var sprite = target.sprite;
if (!sprite) {
throw new Error('No sprite associated with this target.');
}
var currentEditingTarget = this.editingTarget;
for (var i = 0; i < sprite.clones.length; i++) {
var clone = sprite.clones[i];
this.runtime.stopForTarget(sprite.clones[i]);
this.runtime.disposeTarget(sprite.clones[i]);
// Ensure editing target is switched if we are deleting it.
if (clone === currentEditingTarget) {
this.setEditingTarget(this.runtime.targets[0].id);
}
}
// Sprite object should be deleted by GC.
this.emitTargetsUpdate();
} else {
throw new Error('No target with the provided id.');
}
};
/**
@ -219,7 +279,7 @@ VirtualMachine.prototype.flyoutBlockListener = function (e) {
*/
VirtualMachine.prototype.setEditingTarget = function (targetId) {
// Has the target id changed? If not, exit.
if (targetId == this.editingTarget.id) {
if (targetId === this.editingTarget.id) {
return;
}
var target = this.runtime.getTargetById(targetId);
@ -243,8 +303,8 @@ VirtualMachine.prototype.emitTargetsUpdate = function () {
targetList: this.runtime.targets.filter(function (target) {
// Don't report clones.
return !target.hasOwnProperty('isOriginal') || target.isOriginal;
}).map(function(target) {
return [target.id, target.getName()];
}).map(function (target) {
return target.toJSON();
}),
// Currently editing target id.
editingTarget: this.editingTarget ? this.editingTarget.id : null
@ -257,8 +317,16 @@ VirtualMachine.prototype.emitTargetsUpdate = function () {
*/
VirtualMachine.prototype.emitWorkspaceUpdate = function () {
this.emit('workspaceUpdate', {
'xml': this.editingTarget.blocks.toXML()
xml: this.editingTarget.blocks.toXML()
});
};
/**
* Post/edit sprite info for the current editing target.
* @param {object} data An object with sprite info data to set.
*/
VirtualMachine.prototype.postSpriteInfo = function (data) {
this.editingTarget.postSpriteInfo(data);
};
module.exports = VirtualMachine;

View file

@ -1,14 +1,35 @@
var Timer = require('../util/timer');
function Clock () {
var Clock = function (runtime) {
this._projectTimer = new Timer();
this._projectTimer.start();
}
this._pausedTime = null;
this._paused = false;
/**
* Reference to the owning Runtime.
* @type{!Runtime}
*/
this.runtime = runtime;
};
Clock.prototype.projectTimer = function () {
if (this._paused) {
return this._pausedTime / 1000;
}
return this._projectTimer.timeElapsed() / 1000;
};
Clock.prototype.pause = function () {
this._paused = true;
this._pausedTime = this._projectTimer.timeElapsed();
};
Clock.prototype.resume = function () {
this._paused = false;
var dt = this._projectTimer.timeElapsed() - this._pausedTime;
this._projectTimer.startTime += dt;
};
Clock.prototype.resetProjectTimer = function () {
this._projectTimer.start();
};

View file

@ -1,6 +1,6 @@
var Cast = require('../util/cast');
function Keyboard (runtime) {
var Keyboard = function (runtime) {
/**
* List of currently pressed keys.
* @type{Array.<number>}
@ -12,15 +12,16 @@ function Keyboard (runtime) {
* @type{!Runtime}
*/
this.runtime = runtime;
}
};
/**
* Convert a Scratch key name to a DOM keyCode.
* @param {Any} keyName Scratch key argument.
* @return {number} Key code corresponding to a DOM event.
* @private
*/
Keyboard.prototype._scratchKeyToKeyCode = function (keyName) {
if (typeof keyName == 'number') {
if (typeof keyName === 'number') {
// Key codes placed in with number blocks.
return keyName;
}
@ -37,6 +38,12 @@ Keyboard.prototype._scratchKeyToKeyCode = function (keyName) {
return keyString.toUpperCase().charCodeAt(0);
};
/**
* Convert a DOM keyCode into a Scratch key name.
* @param {number} keyCode Key code from DOM event.
* @return {Any} Scratch key argument.
* @private
*/
Keyboard.prototype._keyCodeToScratchKey = function (keyCode) {
if (keyCode >= 48 && keyCode <= 90) {
// Standard letter.
@ -52,6 +59,10 @@ Keyboard.prototype._keyCodeToScratchKey = function (keyCode) {
return null;
};
/**
* Keyboard DOM event handler.
* @param {object} data Data from DOM event.
*/
Keyboard.prototype.postData = function (data) {
if (data.keyCode) {
var index = this._keysPressed.indexOf(data.keyCode);
@ -62,10 +73,10 @@ Keyboard.prototype.postData = function (data) {
}
// Always trigger hats, even if it was already pressed.
this.runtime.startHats('event_whenkeypressed', {
'KEY_OPTION': this._keyCodeToScratchKey(data.keyCode)
KEY_OPTION: this._keyCodeToScratchKey(data.keyCode)
});
this.runtime.startHats('event_whenkeypressed', {
'KEY_OPTION': 'any'
KEY_OPTION: 'any'
});
} else if (index > -1) {
// If already present, remove from the list.
@ -74,8 +85,13 @@ Keyboard.prototype.postData = function (data) {
}
};
/**
* Get key down state for a specified Scratch key name.
* @param {Any} key Scratch key argument.
* @return {boolean} Is the specified key down?
*/
Keyboard.prototype.getKeyIsDown = function (key) {
if (key == 'any') {
if (key === 'any') {
return this._keysPressed.length > 0;
}
var keyCode = this._scratchKeyToKeyCode(key);

View file

@ -1,6 +1,6 @@
var MathUtil = require('../util/math-util');
function Mouse (runtime) {
var Mouse = function (runtime) {
this._x = 0;
this._y = 0;
this._isDown = false;
@ -10,9 +10,34 @@ function Mouse (runtime) {
* @type{!Runtime}
*/
this.runtime = runtime;
}
};
Mouse.prototype.postData = function(data) {
/**
* Activate "event_whenthisspriteclicked" hats if needed.
* @param {number} x X position to be sent to the renderer.
* @param {number} y Y position to be sent to the renderer.
* @private
*/
Mouse.prototype._activateClickHats = function (x, y) {
if (this.runtime.renderer) {
var drawableID = this.runtime.renderer.pick(x, y);
for (var i = 0; i < this.runtime.targets.length; i++) {
var target = this.runtime.targets[i];
if (target.hasOwnProperty('drawableID') &&
target.drawableID === drawableID) {
this.runtime.startHats('event_whenthisspriteclicked',
null, target);
return;
}
}
}
};
/**
* Mouse DOM event handler.
* @param {object} data Data from DOM event.
*/
Mouse.prototype.postData = function (data) {
if (data.x) {
this._x = data.x - data.canvasWidth / 2;
}
@ -27,29 +52,26 @@ Mouse.prototype.postData = function(data) {
}
};
Mouse.prototype._activateClickHats = function (x, y) {
if (this.runtime.renderer) {
var drawableID = this.runtime.renderer.pick(x, y);
for (var i = 0; i < this.runtime.targets.length; i++) {
var target = this.runtime.targets[i];
if (target.hasOwnProperty('drawableID') &&
target.drawableID == drawableID) {
this.runtime.startHats('event_whenthisspriteclicked',
null, target);
return;
}
}
}
};
/**
* Get the X position of the mouse.
* @return {number} Clamped X position of the mouse cursor.
*/
Mouse.prototype.getX = function () {
return MathUtil.clamp(this._x, -240, 240);
};
/**
* Get the Y position of the mouse.
* @return {number} Clamped Y position of the mouse cursor.
*/
Mouse.prototype.getY = function () {
return MathUtil.clamp(-this._y, -180, 180);
};
/**
* Get the down state of the mouse.
* @return {boolean} Is the mouse down?
*/
Mouse.prototype.getIsDown = function () {
return this._isDown;
};

View file

@ -1,465 +0,0 @@
var util = require('util');
var MathUtil = require('../util/math-util');
var Target = require('../engine/target');
var AudioEngine = require('scratch-audioengine');
/**
* Clone (instance) of a sprite.
* @param {!Sprite} sprite Reference to the sprite.
* @param {Runtime} runtime Reference to the runtime.
* @constructor
*/
function Clone(sprite, runtime) {
Target.call(this, sprite.blocks);
this.runtime = runtime;
/**
* Reference to the sprite that this is a clone of.
* @type {!Sprite}
*/
this.sprite = sprite;
/**
* Reference to the global renderer for this VM, if one exists.
* @type {?RenderWebGLWorker}
*/
this.renderer = null;
if (this.runtime) {
this.renderer = this.runtime.renderer;
}
/**
* ID of the drawable for this clone returned by the renderer, if rendered.
* @type {?Number}
*/
this.drawableID = null;
/**
* Map of current graphic effect values.
* @type {!Object.<string, number>}
*/
this.effects = {
'color': 0,
'fisheye': 0,
'whirl': 0,
'pixelate': 0,
'mosaic': 0,
'brightness': 0,
'ghost': 0
};
/**
* Audio engine
*/
this.audioEngine = null;
if (this.runtime) {
this.audioEngine = new AudioEngine(this.sprite.sounds);
}
}
util.inherits(Clone, Target);
/**
* Create a clone's drawable with the this.renderer.
*/
Clone.prototype.initDrawable = function () {
if (this.renderer) {
this.drawableID = this.renderer.createDrawable();
}
// If we're a clone, start the hats.
if (!this.isOriginal) {
this.runtime.startHats(
'control_start_as_clone', null, this
);
}
};
// Clone-level properties.
/**
* Whether this represents an "original" clone, i.e., created by the editor
* and not clone blocks. In interface terms, this true for a "sprite."
* @type {boolean}
*/
Clone.prototype.isOriginal = true;
/**
* Whether this clone represents the Scratch stage.
* @type {boolean}
*/
Clone.prototype.isStage = false;
/**
* Scratch X coordinate. Currently should range from -240 to 240.
* @type {Number}
*/
Clone.prototype.x = 0;
/**
* Scratch Y coordinate. Currently should range from -180 to 180.
* @type {number}
*/
Clone.prototype.y = 0;
/**
* Scratch direction. Currently should range from -179 to 180.
* @type {number}
*/
Clone.prototype.direction = 90;
/**
* Whether the clone is currently visible.
* @type {boolean}
*/
Clone.prototype.visible = true;
/**
* Size of clone as a percent of costume size. Ranges from 5% to 535%.
* @type {number}
*/
Clone.prototype.size = 100;
/**
* Currently selected costume index.
* @type {number}
*/
Clone.prototype.currentCostume = 0;
/**
* Rotation style for "all around"/spinning.
* @enum
*/
Clone.ROTATION_STYLE_ALL_AROUND = 'all around';
/**
* Rotation style for "left-right"/flipping.
* @enum
*/
Clone.ROTATION_STYLE_LEFT_RIGHT = 'left-right';
/**
* Rotation style for "no rotation."
* @enum
*/
Clone.ROTATION_STYLE_NONE = 'don\'t rotate';
/**
* Current rotation style.
* @type {!string}
*/
Clone.prototype.rotationStyle = Clone.ROTATION_STYLE_ALL_AROUND;
// End clone-level properties.
/**
* Set the X and Y coordinates of a clone.
* @param {!number} x New X coordinate of clone, in Scratch coordinates.
* @param {!number} y New Y coordinate of clone, in Scratch coordinates.
*/
Clone.prototype.setXY = function (x, y) {
if (this.isStage) {
return;
}
this.x = x;
this.y = y;
if (this.renderer) {
this.renderer.updateDrawableProperties(this.drawableID, {
position: [this.x, this.y]
});
}
};
/**
* Get the rendered direction and scale, after applying rotation style.
* @return {Object<string, number>} Direction and scale to render.
*/
Clone.prototype._getRenderedDirectionAndScale = function () {
// Default: no changes to `this.direction` or `this.scale`.
var finalDirection = this.direction;
var finalScale = [this.size, this.size];
if (this.rotationStyle == Clone.ROTATION_STYLE_NONE) {
// Force rendered direction to be 90.
finalDirection = 90;
} else if (this.rotationStyle === Clone.ROTATION_STYLE_LEFT_RIGHT) {
// Force rendered direction to be 90, and flip drawable if needed.
finalDirection = 90;
var scaleFlip = (this.direction < 0) ? -1 : 1;
finalScale = [scaleFlip * this.size, this.size];
}
return {direction: finalDirection, scale: finalScale};
};
/**
* Set the direction of a clone.
* @param {!number} direction New direction of clone.
*/
Clone.prototype.setDirection = function (direction) {
if (this.isStage) {
return;
}
// Keep direction between -179 and +180.
this.direction = MathUtil.wrapClamp(direction, -179, 180);
if (this.renderer) {
var renderedDirectionScale = this._getRenderedDirectionAndScale();
this.renderer.updateDrawableProperties(this.drawableID, {
direction: renderedDirectionScale.direction,
scale: renderedDirectionScale.scale
});
}
};
/**
* Set a say bubble on this clone.
* @param {?string} type Type of say bubble: "say", "think", or null.
* @param {?string} message Message to put in say bubble.
*/
Clone.prototype.setSay = function (type, message) {
if (this.isStage) {
return;
}
// @todo: Render to stage.
if (!type || !message) {
console.log('Clearing say bubble');
return;
}
console.log('Setting say bubble:', type, message);
};
/**
* Set visibility of the clone; i.e., whether it's shown or hidden.
* @param {!boolean} visible True if the sprite should be shown.
*/
Clone.prototype.setVisible = function (visible) {
if (this.isStage) {
return;
}
this.visible = visible;
if (this.renderer) {
this.renderer.updateDrawableProperties(this.drawableID, {
visible: this.visible
});
}
};
/**
* Set size of the clone, as a percentage of the costume size.
* @param {!number} size Size of clone, from 5 to 535.
*/
Clone.prototype.setSize = function (size) {
if (this.isStage) {
return;
}
// Keep size between 5% and 535%.
this.size = MathUtil.clamp(size, 5, 535);
if (this.renderer) {
var renderedDirectionScale = this._getRenderedDirectionAndScale();
this.renderer.updateDrawableProperties(this.drawableID, {
direction: renderedDirectionScale.direction,
scale: renderedDirectionScale.scale
});
}
};
/**
* Set a particular graphic effect on this clone.
* @param {!string} effectName Name of effect (see `Clone.prototype.effects`).
* @param {!number} value Numerical magnitude of effect.
*/
Clone.prototype.setEffect = function (effectName, value) {
if (!this.effects.hasOwnProperty(effectName)) return;
this.effects[effectName] = value;
if (this.renderer) {
var props = {};
props[effectName] = this.effects[effectName];
this.renderer.updateDrawableProperties(this.drawableID, props);
}
};
/**
* Clear all graphic effects on this clone.
*/
Clone.prototype.clearEffects = function () {
for (var effectName in this.effects) {
this.effects[effectName] = 0;
}
if (this.renderer) {
this.renderer.updateDrawableProperties(this.drawableID, this.effects);
}
};
/**
* Set the current costume of this clone.
* @param {number} index New index of costume.
*/
Clone.prototype.setCostume = function (index) {
// Keep the costume index within possible values.
index = Math.round(index);
this.currentCostume = MathUtil.wrapClamp(
index, 0, this.sprite.costumes.length - 1
);
if (this.renderer) {
this.renderer.updateDrawableProperties(this.drawableID, {
skin: this.sprite.costumes[this.currentCostume].skin
});
}
};
/**
* Update the rotation style for this clone.
* @param {!string} rotationStyle New rotation style.
*/
Clone.prototype.setRotationStyle = function (rotationStyle) {
if (rotationStyle == Clone.ROTATION_STYLE_NONE) {
this.rotationStyle = Clone.ROTATION_STYLE_NONE;
} else if (rotationStyle == Clone.ROTATION_STYLE_ALL_AROUND) {
this.rotationStyle = Clone.ROTATION_STYLE_ALL_AROUND;
} else if (rotationStyle == Clone.ROTATION_STYLE_LEFT_RIGHT) {
this.rotationStyle = Clone.ROTATION_STYLE_LEFT_RIGHT;
}
if (this.renderer) {
var renderedDirectionScale = this._getRenderedDirectionAndScale();
this.renderer.updateDrawableProperties(this.drawableID, {
direction: renderedDirectionScale.direction,
scale: renderedDirectionScale.scale
});
}
};
/**
* Get a costume index of this clone, by name of the costume.
* @param {?string} costumeName Name of a costume.
* @return {number} Index of the named costume, or -1 if not present.
*/
Clone.prototype.getCostumeIndexByName = function (costumeName) {
for (var i = 0; i < this.sprite.costumes.length; i++) {
if (this.sprite.costumes[i].name == costumeName) {
return i;
}
}
return -1;
};
/**
* Get a sound index of this clone, by name of the sound.
* @param {?string} soundName Name of a sound.
* @return {number} Index of the named sound, or -1 if not present.
*/
Clone.prototype.getSoundIndexByName = function (soundName) {
for (var i = 0; i < this.sprite.sounds.length; i++) {
if (this.sprite.sounds[i].name == soundName) {
return i;
}
}
return -1;
};
/**
* Update all drawable properties for this clone.
* Use when a batch has changed, e.g., when the drawable is first created.
*/
Clone.prototype.updateAllDrawableProperties = function () {
if (this.renderer) {
var renderedDirectionScale = this._getRenderedDirectionAndScale();
this.renderer.updateDrawableProperties(this.drawableID, {
position: [this.x, this.y],
direction: renderedDirectionScale.direction,
scale: renderedDirectionScale.scale,
visible: this.visible,
skin: this.sprite.costumes[this.currentCostume].skin
});
}
};
/**
* Return the human-readable name for this clone, i.e., the sprite's name.
* @override
* @returns {string} Human-readable name for the clone.
*/
Clone.prototype.getName = function () {
return this.sprite.name;
};
/**
* Return whether the clone is touching a color.
* @param {Array.<number>} rgb [r,g,b], values between 0-255.
* @return {Promise.<Boolean>} True iff the clone is touching the color.
*/
Clone.prototype.isTouchingColor = function (rgb) {
if (this.renderer) {
return this.renderer.isTouchingColor(this.drawableID, rgb);
}
return false;
};
/**
* Return whether the clone's color is touching a color.
* @param {Object} targetRgb {Array.<number>} [r,g,b], values between 0-255.
* @param {Object} maskRgb {Array.<number>} [r,g,b], values between 0-255.
* @return {Promise.<Boolean>} True iff the clone's color is touching the color.
*/
Clone.prototype.colorIsTouchingColor = function (targetRgb, maskRgb) {
if (this.renderer) {
return this.renderer.isTouchingColor(
this.drawableID,
targetRgb,
maskRgb
);
}
return false;
};
/**
* Make a clone of this clone, copying any run-time properties.
* If we've hit the global clone limit, returns null.
* @return {!Clone} New clone object.
*/
Clone.prototype.makeClone = function () {
if (!this.runtime.clonesAvailable()) {
return; // Hit max clone limit.
}
this.runtime.changeCloneCounter(1);
var newClone = this.sprite.createClone();
newClone.x = this.x;
newClone.y = this.y;
newClone.direction = this.direction;
newClone.visible = this.visible;
newClone.size = this.size;
newClone.currentCostume = this.currentCostume;
newClone.rotationStyle = this.rotationStyle;
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;
};
/**
* Called when the project receives a "green flag."
* For a clone, this clears graphic effects.
*/
Clone.prototype.onGreenFlag = function () {
this.clearEffects();
if (this.audioEngine) {
this.audioEngine.stopAllSounds();
this.audioEngine.clearEffects();
}
};
/**
* Called when the project receives a "stop all"
* Stop all sounds
*/
Clone.prototype.onStopAll = function () {
if (this.audioEngine) {
this.audioEngine.stopAllSounds();
this.audioEngine.clearEffects();
}
};
/**
* Dispose of this clone, destroying any run-time properties.
*/
Clone.prototype.dispose = function () {
this.runtime.changeCloneCounter(-1);
if (this.renderer && this.drawableID !== null) {
this.renderer.destroyDrawable(this.drawableID);
}
};
module.exports = Clone;

View file

@ -0,0 +1,722 @@
var AudioEngine = require('scratch-audioengine');
var util = require('util');
var log = require('../util/log');
var MathUtil = require('../util/math-util');
var Target = require('../engine/target');
/**
* Rendered target: instance of a sprite (clone), or the stage.
* @param {!Sprite} sprite Reference to the parent sprite.
* @param {Runtime} runtime Reference to the runtime.
* @constructor
*/
var RenderedTarget = function (sprite, runtime) {
Target.call(this, sprite.blocks);
this.runtime = runtime;
/**
* Reference to the sprite that this is a render of.
* @type {!Sprite}
*/
this.sprite = sprite;
/**
* Reference to the global renderer for this VM, if one exists.
* @type {?RenderWebGLWorker}
*/
this.renderer = null;
if (this.runtime) {
this.renderer = this.runtime.renderer;
}
/**
* ID of the drawable for this rendered target,
* returned by the renderer, if rendered.
* @type {?Number}
*/
this.drawableID = null;
/**
* Map of current graphic effect values.
* @type {!Object.<string, number>}
*/
this.effects = {
color: 0,
fisheye: 0,
whirl: 0,
pixelate: 0,
mosaic: 0,
brightness: 0,
ghost: 0
};
/**
* Audio engine
*/
this.audioEngine = null;
if (this.runtime) {
this.audioEngine = new AudioEngine(this.sprite.sounds);
}
};
util.inherits(RenderedTarget, Target);
/**
* Create a drawable with the this.renderer.
*/
RenderedTarget.prototype.initDrawable = function () {
if (this.renderer) {
this.drawableID = this.renderer.createDrawable();
}
// If we're a clone, start the hats.
if (!this.isOriginal) {
this.runtime.startHats(
'control_start_as_clone', null, this
);
}
};
/**
* Whether this represents an "original" non-clone rendered-target for a sprite,
* i.e., created by the editor and not clone blocks.
* @type {boolean}
*/
RenderedTarget.prototype.isOriginal = true;
/**
* Whether this rendered target represents the Scratch stage.
* @type {boolean}
*/
RenderedTarget.prototype.isStage = false;
/**
* Scratch X coordinate. Currently should range from -240 to 240.
* @type {Number}
*/
RenderedTarget.prototype.x = 0;
/**
* Scratch Y coordinate. Currently should range from -180 to 180.
* @type {number}
*/
RenderedTarget.prototype.y = 0;
/**
* Scratch direction. Currently should range from -179 to 180.
* @type {number}
*/
RenderedTarget.prototype.direction = 90;
/**
* Whether the rendered target is currently visible.
* @type {boolean}
*/
RenderedTarget.prototype.visible = true;
/**
* Size of rendered target as a percent of costume size.
* @type {number}
*/
RenderedTarget.prototype.size = 100;
/**
* Currently selected costume index.
* @type {number}
*/
RenderedTarget.prototype.currentCostume = 0;
/**
* Rotation style for "all around"/spinning.
* @enum
*/
RenderedTarget.ROTATION_STYLE_ALL_AROUND = 'all around';
/**
* Rotation style for "left-right"/flipping.
* @enum
*/
RenderedTarget.ROTATION_STYLE_LEFT_RIGHT = 'left-right';
/**
* Rotation style for "no rotation."
* @enum
*/
RenderedTarget.ROTATION_STYLE_NONE = 'don\'t rotate';
/**
* Current rotation style.
* @type {!string}
*/
RenderedTarget.prototype.rotationStyle = (
RenderedTarget.ROTATION_STYLE_ALL_AROUND
);
/**
* Set the X and Y coordinates.
* @param {!number} x New X coordinate, in Scratch coordinates.
* @param {!number} y New Y coordinate, in Scratch coordinates.
*/
RenderedTarget.prototype.setXY = function (x, y) {
if (this.isStage) {
return;
}
this.x = x;
this.y = y;
if (this.renderer) {
this.renderer.updateDrawableProperties(this.drawableID, {
position: [this.x, this.y]
});
if (this.visible) {
this.runtime.requestRedraw();
}
}
this.runtime.spriteInfoReport(this);
};
/**
* Get the rendered direction and scale, after applying rotation style.
* @return {Object<string, number>} Direction and scale to render.
*/
RenderedTarget.prototype._getRenderedDirectionAndScale = function () {
// Default: no changes to `this.direction` or `this.scale`.
var finalDirection = this.direction;
var finalScale = [this.size, this.size];
if (this.rotationStyle === RenderedTarget.ROTATION_STYLE_NONE) {
// Force rendered direction to be 90.
finalDirection = 90;
} else if (this.rotationStyle === RenderedTarget.ROTATION_STYLE_LEFT_RIGHT) {
// Force rendered direction to be 90, and flip drawable if needed.
finalDirection = 90;
var scaleFlip = (this.direction < 0) ? -1 : 1;
finalScale = [scaleFlip * this.size, this.size];
}
return {direction: finalDirection, scale: finalScale};
};
/**
* Set the direction.
* @param {!number} direction New direction.
*/
RenderedTarget.prototype.setDirection = function (direction) {
if (this.isStage) {
return;
}
// Keep direction between -179 and +180.
this.direction = MathUtil.wrapClamp(direction, -179, 180);
if (this.renderer) {
var renderedDirectionScale = this._getRenderedDirectionAndScale();
this.renderer.updateDrawableProperties(this.drawableID, {
direction: renderedDirectionScale.direction,
scale: renderedDirectionScale.scale
});
if (this.visible) {
this.runtime.requestRedraw();
}
}
this.runtime.spriteInfoReport(this);
};
/**
* Set a say bubble.
* @param {?string} type Type of say bubble: "say", "think", or null.
* @param {?string} message Message to put in say bubble.
*/
RenderedTarget.prototype.setSay = function (type, message) {
if (this.isStage) {
return;
}
// @todo: Render to stage.
if (!type || !message) {
log.info('Clearing say bubble');
return;
}
log.info('Setting say bubble:', type, message);
};
/**
* Set visibility; i.e., whether it's shown or hidden.
* @param {!boolean} visible True if should be shown.
*/
RenderedTarget.prototype.setVisible = function (visible) {
if (this.isStage) {
return;
}
this.visible = !!visible;
if (this.renderer) {
this.renderer.updateDrawableProperties(this.drawableID, {
visible: this.visible
});
if (this.visible) {
this.runtime.requestRedraw();
}
}
this.runtime.spriteInfoReport(this);
};
/**
* Set size, as a percentage of the costume size.
* @param {!number} size Size of rendered target, as % of costume size.
*/
RenderedTarget.prototype.setSize = function (size) {
if (this.isStage) {
return;
}
if (this.renderer) {
// Clamp to scales relative to costume and stage size.
// See original ScratchSprite.as:setSize.
var costumeSize = this.renderer.getSkinSize(this.drawableID);
var origW = Math.round(costumeSize[0]);
var origH = Math.round(costumeSize[1]);
var minScale = Math.min(1, Math.max(5 / origW, 5 / origH));
var maxScale = Math.min(
(1.5 * this.runtime.constructor.STAGE_WIDTH) / origW,
(1.5 * this.runtime.constructor.STAGE_HEIGHT) / origH
);
this.size = Math.round(MathUtil.clamp(size / 100, minScale, maxScale) * 100);
var renderedDirectionScale = this._getRenderedDirectionAndScale();
this.renderer.updateDrawableProperties(this.drawableID, {
direction: renderedDirectionScale.direction,
scale: renderedDirectionScale.scale
});
if (this.visible) {
this.runtime.requestRedraw();
}
}
};
/**
* Set a particular graphic effect value.
* @param {!string} effectName Name of effect (see `RenderedTarget.prototype.effects`).
* @param {!number} value Numerical magnitude of effect.
*/
RenderedTarget.prototype.setEffect = function (effectName, value) {
if (!this.effects.hasOwnProperty(effectName)) return;
this.effects[effectName] = value;
if (this.renderer) {
var props = {};
props[effectName] = this.effects[effectName];
this.renderer.updateDrawableProperties(this.drawableID, props);
if (this.visible) {
this.runtime.requestRedraw();
}
}
};
/**
* Clear all graphic effects on this rendered target.
*/
RenderedTarget.prototype.clearEffects = function () {
for (var effectName in this.effects) {
this.effects[effectName] = 0;
}
if (this.renderer) {
this.renderer.updateDrawableProperties(this.drawableID, this.effects);
if (this.visible) {
this.runtime.requestRedraw();
}
}
};
/**
* Set the current costume.
* @param {number} index New index of costume.
*/
RenderedTarget.prototype.setCostume = function (index) {
// Keep the costume index within possible values.
index = Math.round(index);
this.currentCostume = MathUtil.wrapClamp(
index, 0, this.sprite.costumes.length - 1
);
if (this.renderer) {
var costume = this.sprite.costumes[this.currentCostume];
this.renderer.updateDrawableProperties(this.drawableID, {
skin: costume.skin,
costumeResolution: costume.bitmapResolution,
rotationCenter: [
costume.rotationCenterX / costume.bitmapResolution,
costume.rotationCenterY / costume.bitmapResolution
]
});
if (this.visible) {
this.runtime.requestRedraw();
}
}
this.runtime.spriteInfoReport(this);
};
/**
* Update the rotation style.
* @param {!string} rotationStyle New rotation style.
*/
RenderedTarget.prototype.setRotationStyle = function (rotationStyle) {
if (rotationStyle === RenderedTarget.ROTATION_STYLE_NONE) {
this.rotationStyle = RenderedTarget.ROTATION_STYLE_NONE;
} else if (rotationStyle === RenderedTarget.ROTATION_STYLE_ALL_AROUND) {
this.rotationStyle = RenderedTarget.ROTATION_STYLE_ALL_AROUND;
} else if (rotationStyle === RenderedTarget.ROTATION_STYLE_LEFT_RIGHT) {
this.rotationStyle = RenderedTarget.ROTATION_STYLE_LEFT_RIGHT;
}
if (this.renderer) {
var renderedDirectionScale = this._getRenderedDirectionAndScale();
this.renderer.updateDrawableProperties(this.drawableID, {
direction: renderedDirectionScale.direction,
scale: renderedDirectionScale.scale
});
if (this.visible) {
this.runtime.requestRedraw();
}
}
this.runtime.spriteInfoReport(this);
};
/**
* Get a costume index of this rendered target, by name of the costume.
* @param {?string} costumeName Name of a costume.
* @return {number} Index of the named costume, or -1 if not present.
*/
RenderedTarget.prototype.getCostumeIndexByName = function (costumeName) {
for (var i = 0; i < this.sprite.costumes.length; i++) {
if (this.sprite.costumes[i].name === costumeName) {
return i;
}
}
return -1;
};
/**
* Get a costume of this rendered target by id.
* @return {object} current costume
*/
RenderedTarget.prototype.getCurrentCostume = function () {
return this.sprite.costumes[this.currentCostume];
};
/**
* Get full costume list
* @return {object[]} list of costumes
*/
RenderedTarget.prototype.getCostumes = function () {
return this.sprite.costumes;
};
/**
* Update all drawable properties for this rendered target.
* Use when a batch has changed, e.g., when the drawable is first created.
*/
RenderedTarget.prototype.updateAllDrawableProperties = function () {
if (this.renderer) {
var renderedDirectionScale = this._getRenderedDirectionAndScale();
var costume = this.sprite.costumes[this.currentCostume];
this.renderer.updateDrawableProperties(this.drawableID, {
position: [this.x, this.y],
direction: renderedDirectionScale.direction,
scale: renderedDirectionScale.scale,
visible: this.visible,
skin: costume.skin,
costumeResolution: costume.bitmapResolution,
rotationCenter: [
costume.rotationCenterX / costume.bitmapResolution,
costume.rotationCenterY / costume.bitmapResolution
]
});
if (this.visible) {
this.runtime.requestRedraw();
}
}
this.runtime.spriteInfoReport(this);
};
/**
* Return the human-readable name for this rendered target, e.g., the sprite's name.
* @override
* @returns {string} Human-readable name.
*/
RenderedTarget.prototype.getName = function () {
return this.sprite.name;
};
/**
* Return whether this rendered target is a sprite (not a clone, not the stage).
* @return {boolean} True if not a clone and not the stage.
*/
RenderedTarget.prototype.isSprite = function () {
return !this.isStage && this.isOriginal;
};
/**
* Return the rendered target's tight bounding box.
* Includes top, left, bottom, right attributes in Scratch coordinates.
* @return {?Object} Tight bounding box, or null.
*/
RenderedTarget.prototype.getBounds = function () {
if (this.renderer) {
return this.runtime.renderer.getBounds(this.drawableID);
}
return null;
};
/**
* Return whether touching a point.
* @param {number} x X coordinate of test point.
* @param {number} y Y coordinate of test point.
* @return {Boolean} True iff the rendered target is touching the point.
*/
RenderedTarget.prototype.isTouchingPoint = function (x, y) {
if (this.renderer) {
// @todo: Update once pick is in Scratch coordinates.
// Limits test to this Drawable, so this will return true
// even if the clone is obscured by another Drawable.
var pickResult = this.runtime.renderer.pick(
x + (this.runtime.constructor.STAGE_WIDTH / 2),
-y + (this.runtime.constructor.STAGE_HEIGHT / 2),
null, null,
[this.drawableID]
);
return pickResult === this.drawableID;
}
return false;
};
/**
* Return whether touching a stage edge.
* @return {Boolean} True iff the rendered target is touching the stage edge.
*/
RenderedTarget.prototype.isTouchingEdge = function () {
if (this.renderer) {
var stageWidth = this.runtime.constructor.STAGE_WIDTH;
var stageHeight = this.runtime.constructor.STAGE_HEIGHT;
var bounds = this.getBounds();
if (bounds.left < -stageWidth / 2 ||
bounds.right > stageWidth / 2 ||
bounds.top > stageHeight / 2 ||
bounds.bottom < -stageHeight / 2) {
return true;
}
}
return false;
};
/**
* Return whether touching any of a named sprite's clones.
* @param {string} spriteName Name of the sprite.
* @return {Boolean} True iff touching a clone of the sprite.
*/
RenderedTarget.prototype.isTouchingSprite = function (spriteName) {
var firstClone = this.runtime.getSpriteTargetByName(spriteName);
if (!firstClone || !this.renderer) {
return false;
}
var drawableCandidates = firstClone.sprite.clones.map(function (clone) {
return clone.drawableID;
});
return this.renderer.isTouchingDrawables(
this.drawableID, drawableCandidates);
};
/**
* Return whether touching a color.
* @param {Array.<number>} rgb [r,g,b], values between 0-255.
* @return {Promise.<Boolean>} True iff the rendered target is touching the color.
*/
RenderedTarget.prototype.isTouchingColor = function (rgb) {
if (this.renderer) {
return this.renderer.isTouchingColor(this.drawableID, rgb);
}
return false;
};
/**
* Return whether rendered target's color is touching a color.
* @param {Object} targetRgb {Array.<number>} [r,g,b], values between 0-255.
* @param {Object} maskRgb {Array.<number>} [r,g,b], values between 0-255.
* @return {Promise.<Boolean>} True iff the color is touching the color.
*/
RenderedTarget.prototype.colorIsTouchingColor = function (targetRgb, maskRgb) {
if (this.renderer) {
return this.renderer.isTouchingColor(
this.drawableID,
targetRgb,
maskRgb
);
}
return false;
};
/**
* Move to the front layer.
*/
RenderedTarget.prototype.goToFront = function () {
if (this.renderer) {
this.renderer.setDrawableOrder(this.drawableID, Infinity);
}
};
/**
* Move back a number of layers.
* @param {number} nLayers How many layers to go back.
*/
RenderedTarget.prototype.goBackLayers = function (nLayers) {
if (this.renderer) {
this.renderer.setDrawableOrder(this.drawableID, -nLayers, true, 1);
}
};
/**
* Move behind some other rendered target.
* @param {!Clone} other Other rendered target to move behind.
*/
RenderedTarget.prototype.goBehindOther = function (other) {
if (this.renderer) {
var otherLayer = this.renderer.setDrawableOrder(
other.drawableID, 0, true);
this.renderer.setDrawableOrder(this.drawableID, otherLayer);
}
};
/**
* Keep a desired position within a fence.
* @param {number} newX New desired X position.
* @param {number} newY New desired Y position.
* @param {Object=} optFence Optional fence with left, right, top bottom.
* @return {Array.<number>} Fenced X and Y coordinates.
*/
RenderedTarget.prototype.keepInFence = function (newX, newY, optFence) {
var fence = optFence;
if (!fence) {
fence = {
left: -this.runtime.constructor.STAGE_WIDTH / 2,
right: this.runtime.constructor.STAGE_WIDTH / 2,
top: this.runtime.constructor.STAGE_HEIGHT / 2,
bottom: -this.runtime.constructor.STAGE_HEIGHT / 2
};
}
var bounds = this.getBounds();
if (!bounds) return;
// Adjust the known bounds to the target position.
bounds.left += (newX - this.x);
bounds.right += (newX - this.x);
bounds.top += (newY - this.y);
bounds.bottom += (newY - this.y);
// Find how far we need to move the target position.
var dx = 0;
var dy = 0;
if (bounds.left < fence.left) {
dx += fence.left - bounds.left;
}
if (bounds.right > fence.right) {
dx += fence.right - bounds.right;
}
if (bounds.top > fence.top) {
dy += fence.top - bounds.top;
}
if (bounds.bottom < fence.bottom) {
dy += fence.bottom - bounds.bottom;
}
return [newX + dx, newY + dy];
};
/**
* Make a clone, copying any run-time properties.
* If we've hit the global clone limit, returns null.
* @return {!RenderedTarget} New clone.
*/
RenderedTarget.prototype.makeClone = function () {
if (!this.runtime.clonesAvailable() || this.isStage) {
return; // Hit max clone limit, or this is the stage.
}
this.runtime.changeCloneCounter(1);
var newClone = this.sprite.createClone();
// Copy all properties.
newClone.x = this.x;
newClone.y = this.y;
newClone.direction = this.direction;
newClone.visible = this.visible;
newClone.size = this.size;
newClone.currentCostume = this.currentCostume;
newClone.rotationStyle = this.rotationStyle;
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();
// Place behind the current target.
newClone.goBehindOther(this);
return newClone;
};
/**
* Called when the project receives a "green flag."
* For a rendered target, this clears graphic effects.
*/
RenderedTarget.prototype.onGreenFlag = function () {
this.clearEffects();
if (this.audioEngine) {
this.audioEngine.stopAllSounds();
this.audioEngine.clearEffects();
}
};
/**
* Called when the project receives a "stop all"
* Stop all sounds
*/
RenderedTarget.prototype.onStopAll = function () {
if (this.audioEngine) {
this.audioEngine.stopAllSounds();
this.audioEngine.clearEffects();
}
};
/**
* Post/edit sprite info.
* @param {object} data An object with sprite info data to set.
*/
RenderedTarget.prototype.postSpriteInfo = function (data) {
if (data.hasOwnProperty('x')) {
this.setXY(data.x, this.y);
}
if (data.hasOwnProperty('y')) {
this.setXY(this.x, data.y);
}
if (data.hasOwnProperty('direction')) {
this.setDirection(data.direction);
}
if (data.hasOwnProperty('rotationStyle')) {
this.setRotationStyle(data.rotationStyle);
}
if (data.hasOwnProperty('visible')) {
this.setVisible(data.visible);
}
};
/**
* Serialize sprite info, used when emitting events about the sprite
* @returns {object} sprite data as a simple object
*/
RenderedTarget.prototype.toJSON = function () {
return {
id: this.id,
name: this.getName(),
isStage: this.isStage,
x: this.x,
y: this.y,
direction: this.direction,
costume: this.getCurrentCostume(),
costumeCount: this.getCostumes().length,
visible: this.visible,
rotationStyle: this.rotationStyle
};
};
/**
* Dispose, destroying any run-time properties.
*/
RenderedTarget.prototype.dispose = function () {
this.runtime.changeCloneCounter(-1);
if (this.renderer && this.drawableID !== null) {
this.renderer.destroyDrawable(this.drawableID);
if (this.visible) {
this.runtime.requestRedraw();
}
}
};
module.exports = RenderedTarget;

View file

@ -1,4 +1,4 @@
var Clone = require('./clone');
var RenderedTarget = require('./rendered-target');
var Blocks = require('../engine/blocks');
/**
@ -8,7 +8,7 @@ var Blocks = require('../engine/blocks');
* @param {Runtime} runtime Reference to the runtime.
* @constructor
*/
function Sprite (blocks, runtime) {
var Sprite = function (blocks, runtime) {
this.runtime = runtime;
if (!blocks) {
// Shared set of blocks for all clones.
@ -39,18 +39,18 @@ function Sprite (blocks, runtime) {
this.sounds = [];
/**
* List of clones for this sprite, including the original.
* @type {Array.<!Clone>}
* @type {Array.<!RenderedTarget>}
*/
this.clones = [];
}
};
/**
* Create a clone of this sprite.
* @returns {!Clone} Newly created clone.
*/
Sprite.prototype.createClone = function () {
var newClone = new Clone(this, this.runtime);
newClone.isOriginal = this.clones.length == 0;
var newClone = new RenderedTarget(this, this.runtime);
newClone.isOriginal = this.clones.length === 0;
this.clones.push(newClone);
if (newClone.isOriginal) {
newClone.initDrawable();

View file

@ -1,6 +1,6 @@
var Color = require('../util/color');
function Cast () {}
var Cast = function () {};
/**
* @fileoverview
@ -44,9 +44,9 @@ Cast.toBoolean = function (value) {
}
if (typeof value === 'string') {
// These specific strings are treated as false in Scratch.
if ((value == '') ||
(value == '0') ||
(value.toLowerCase() == 'false')) {
if ((value === '') ||
(value === '0') ||
(value.toLowerCase() === 'false')) {
return false;
}
// All other strings treated as true.
@ -72,7 +72,7 @@ Cast.toString = function (value) {
*/
Cast.toRgbColorList = function (value) {
var color;
if (typeof value == 'string' && value.substring(0, 1) == '#') {
if (typeof value === 'string' && value.substring(0, 1) === '#') {
color = Color.hexToRgb(value);
} else {
color = Color.decimalToRgb(Cast.toNumber(value));
@ -114,7 +114,7 @@ Cast.isInt = function (val) {
return true;
}
// True if it's "round" (e.g., 2.0 and 2).
return val == parseInt(val);
return val === parseInt(val, 10);
} else if (typeof val === 'boolean') {
// `True` and `false` always represent integer after Scratch cast.
return true;
@ -138,15 +138,15 @@ Cast.LIST_ALL = 'ALL';
*/
Cast.toListIndex = function (index, length) {
if (typeof index !== 'number') {
if (index == 'all') {
if (index === 'all') {
return Cast.LIST_ALL;
}
if (index == 'last') {
if (index === 'last') {
if (length > 0) {
return length;
}
return Cast.LIST_INVALID;
} else if (index == 'random' || index == 'any') {
} else if (index === 'random' || index === 'any') {
if (length > 0) {
return 1 + Math.floor(Math.random() * length);
}

View file

@ -1,4 +1,4 @@
function Color () {}
var Color = function () {};
/**
* Convert a Scratch decimal color to a hex string, #RRGGBB.
@ -35,7 +35,7 @@ Color.decimalToRgb = function (decimal) {
*/
Color.hexToRgb = function (hex) {
var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
hex = hex.replace(shorthandRegex, function(m, r, g, b) {
hex = hex.replace(shorthandRegex, function (m, r, g, b) {
return r + r + g + g + b + b;
});
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);

4
src/util/log.js Normal file
View file

@ -0,0 +1,4 @@
var minilog = require('minilog');
minilog.enable();
module.exports = minilog('vm');

View file

@ -1,4 +1,4 @@
function MathUtil () {}
var MathUtil = function () {};
/**
* Convert a value from degrees to radians.
@ -42,7 +42,7 @@ MathUtil.clamp = function (n, min, max) {
*/
MathUtil.wrapClamp = function (n, min, max) {
var range = (max - min) + 1;
return n - Math.floor((n - min) / range) * range;
return n - (Math.floor((n - min) / range) * range);
};
module.exports = MathUtil;

View file

@ -15,7 +15,7 @@
/**
* @constructor
*/
function Timer () {}
var Timer = function () {};
/**
* Used to store the start time of a timer action.

5
test/.eslintrc.js Normal file
View file

@ -0,0 +1,5 @@
module.exports = {
rules: {
'no-undefined': [0]
}
};

130
test/unit/blocks_control.js Normal file
View file

@ -0,0 +1,130 @@
var test = require('tap').test;
var Control = require('../../src/blocks/scratch3_control');
var Runtime = require('../../src/engine/runtime');
test('getPrimitives', function (t) {
var rt = new Runtime();
var c = new Control(rt);
t.type(c.getPrimitives(), 'object');
t.end();
});
test('repeat', function (t) {
var rt = new Runtime();
var c = new Control(rt);
// Test harness (mocks `util`)
var i = 0;
var repeat = 10;
var util = {
stackFrame: Object.create(null),
startBranch: function () {
i++;
c.repeat({TIMES: repeat}, util);
}
};
// Execute test
c.repeat({TIMES: 10}, util);
t.strictEqual(util.stackFrame.loopCounter, -1);
t.strictEqual(i, repeat);
t.end();
});
test('repeatUntil', function (t) {
var rt = new Runtime();
var c = new Control(rt);
// Test harness (mocks `util`)
var i = 0;
var repeat = 10;
var util = {
stackFrame: Object.create(null),
startBranch: function () {
i++;
c.repeatUntil({CONDITION: (i === repeat)}, util);
}
};
// Execute test
c.repeatUntil({CONDITION: (i === repeat)}, util);
t.strictEqual(i, repeat);
t.end();
});
test('forever', function (t) {
var rt = new Runtime();
var c = new Control(rt);
// Test harness (mocks `util`)
var i = 0;
var util = {
startBranch: function (branchNum, isLoop) {
i++;
t.strictEqual(branchNum, 1);
t.strictEqual(isLoop, true);
}
};
// Execute test
c.forever(null, util);
t.strictEqual(i, 1);
t.end();
});
test('if / ifElse', function (t) {
var rt = new Runtime();
var c = new Control(rt);
// Test harness (mocks `util`)
var i = 0;
var util = {
startBranch: function (branchNum) {
i += branchNum;
}
};
// Execute test
c.if({CONDITION: true}, util);
t.strictEqual(i, 1);
c.if({CONDITION: false}, util);
t.strictEqual(i, 1);
c.ifElse({CONDITION: true}, util);
t.strictEqual(i, 2);
c.ifElse({CONDITION: false}, util);
t.strictEqual(i, 4);
t.end();
});
test('stop', function (t) {
var rt = new Runtime();
var c = new Control(rt);
// Test harness (mocks `util`)
var state = {
stopAll: 0,
stopOtherTargetThreads: 0,
stopThread: 0
};
var util = {
stopAll: function () {
state.stopAll++;
},
stopOtherTargetThreads: function () {
state.stopOtherTargetThreads++;
},
stopThread: function () {
state.stopThread++;
}
};
// Execute test
c.stop({STOP_OPTION: 'all'}, util);
c.stop({STOP_OPTION: 'other scripts in sprite'}, util);
c.stop({STOP_OPTION: 'other scripts in stage'}, util);
c.stop({STOP_OPTION: 'this script'}, util);
t.strictEqual(state.stopAll, 1);
t.strictEqual(state.stopOtherTargetThreads, 2);
t.strictEqual(state.stopThread, 1);
t.end();
});

View file

@ -9,75 +9,75 @@ test('getPrimitives', function (t) {
});
test('add', function (t) {
t.strictEqual(blocks.add({NUM1:'1', NUM2:'1'}), 2);
t.strictEqual(blocks.add({NUM1:'foo', NUM2:'bar'}), 0);
t.strictEqual(blocks.add({NUM1: '1', NUM2: '1'}), 2);
t.strictEqual(blocks.add({NUM1: 'foo', NUM2: 'bar'}), 0);
t.end();
});
test('subtract', function (t) {
t.strictEqual(blocks.subtract({NUM1:'1', NUM2:'1'}), 0);
t.strictEqual(blocks.subtract({NUM1:'foo', NUM2:'bar'}), 0);
t.strictEqual(blocks.subtract({NUM1: '1', NUM2: '1'}), 0);
t.strictEqual(blocks.subtract({NUM1: 'foo', NUM2: 'bar'}), 0);
t.end();
});
test('multiply', function (t) {
t.strictEqual(blocks.multiply({NUM1:'2', NUM2:'2'}), 4);
t.strictEqual(blocks.multiply({NUM1:'foo', NUM2:'bar'}), 0);
t.strictEqual(blocks.multiply({NUM1: '2', NUM2: '2'}), 4);
t.strictEqual(blocks.multiply({NUM1: 'foo', NUM2: 'bar'}), 0);
t.end();
});
test('divide', function (t) {
t.strictEqual(blocks.divide({NUM1:'2', NUM2:'2'}), 1);
t.strictEqual(blocks.divide({NUM1:'1', NUM2:'0'}), Infinity); // @todo
t.ok(isNaN(blocks.divide({NUM1:'foo', NUM2:'bar'}))); // @todo
t.strictEqual(blocks.divide({NUM1: '2', NUM2: '2'}), 1);
t.strictEqual(blocks.divide({NUM1: '1', NUM2: '0'}), Infinity); // @todo
t.ok(isNaN(blocks.divide({NUM1: 'foo', NUM2: 'bar'}))); // @todo
t.end();
});
test('lt', function (t) {
t.strictEqual(blocks.lt({OPERAND1:'1', OPERAND2:'2'}), true);
t.strictEqual(blocks.lt({OPERAND1:'2', OPERAND2:'1'}), false);
t.strictEqual(blocks.lt({OPERAND1:'1', OPERAND2:'1'}), false);
t.strictEqual(blocks.lt({OPERAND1: '1', OPERAND2: '2'}), true);
t.strictEqual(blocks.lt({OPERAND1: '2', OPERAND2: '1'}), false);
t.strictEqual(blocks.lt({OPERAND1: '1', OPERAND2: '1'}), false);
t.end();
});
test('equals', function (t) {
t.strictEqual(blocks.equals({OPERAND1:'1', OPERAND2:'2'}), false);
t.strictEqual(blocks.equals({OPERAND1:'2', OPERAND2:'1'}), false);
t.strictEqual(blocks.equals({OPERAND1:'1', OPERAND2:'1'}), true);
t.strictEqual(blocks.equals({OPERAND1: '1', OPERAND2: '2'}), false);
t.strictEqual(blocks.equals({OPERAND1: '2', OPERAND2: '1'}), false);
t.strictEqual(blocks.equals({OPERAND1: '1', OPERAND2: '1'}), true);
t.end();
});
test('gt', function (t) {
t.strictEqual(blocks.gt({OPERAND1:'1', OPERAND2:'2'}), false);
t.strictEqual(blocks.gt({OPERAND1:'2', OPERAND2:'1'}), true);
t.strictEqual(blocks.gt({OPERAND1:'1', OPERAND2:'1'}), false);
t.strictEqual(blocks.gt({OPERAND1: '1', OPERAND2: '2'}), false);
t.strictEqual(blocks.gt({OPERAND1: '2', OPERAND2: '1'}), true);
t.strictEqual(blocks.gt({OPERAND1: '1', OPERAND2: '1'}), false);
t.end();
});
test('and', function (t) {
t.strictEqual(blocks.and({OPERAND1:true, OPERAND2:true}), true);
t.strictEqual(blocks.and({OPERAND1:true, OPERAND2:false}), false);
t.strictEqual(blocks.and({OPERAND1:false, OPERAND2:false}), false);
t.strictEqual(blocks.and({OPERAND1: true, OPERAND2: true}), true);
t.strictEqual(blocks.and({OPERAND1: true, OPERAND2: false}), false);
t.strictEqual(blocks.and({OPERAND1: false, OPERAND2: false}), false);
t.end();
});
test('or', function (t) {
t.strictEqual(blocks.or({OPERAND1:true, OPERAND2:true}), true);
t.strictEqual(blocks.or({OPERAND1:true, OPERAND2:false}), true);
t.strictEqual(blocks.or({OPERAND1:false, OPERAND2:false}), false);
t.strictEqual(blocks.or({OPERAND1: true, OPERAND2: true}), true);
t.strictEqual(blocks.or({OPERAND1: true, OPERAND2: false}), true);
t.strictEqual(blocks.or({OPERAND1: false, OPERAND2: false}), false);
t.end();
});
test('not', function (t) {
t.strictEqual(blocks.not({OPERAND:true}), false);
t.strictEqual(blocks.not({OPERAND:false}), true);
t.strictEqual(blocks.not({OPERAND: true}), false);
t.strictEqual(blocks.not({OPERAND: false}), true);
t.end();
});
test('random', function (t) {
var min = 0;
var max = 100;
var result = blocks.random({FROM:min, TO:max});
var result = blocks.random({FROM: min, TO: max});
t.ok(result >= min);
t.ok(result <= max);
t.end();
@ -86,14 +86,14 @@ test('random', function (t) {
test('random - equal', function (t) {
var min = 1;
var max = 1;
t.strictEqual(blocks.random({FROM:min, TO:max}), min);
t.strictEqual(blocks.random({FROM: min, TO: max}), min);
t.end();
});
test('random - decimal', function (t) {
var min = 0.1;
var max = 10;
var result = blocks.random({FROM:min, TO:max});
var result = blocks.random({FROM: min, TO: max});
t.ok(result >= min);
t.ok(result <= max);
t.end();
@ -102,7 +102,7 @@ test('random - decimal', function (t) {
test('random - int', function (t) {
var min = 0;
var max = 10;
var result = blocks.random({FROM:min, TO:max});
var result = blocks.random({FROM: min, TO: max});
t.ok(result >= min);
t.ok(result <= max);
t.end();
@ -111,65 +111,65 @@ test('random - int', function (t) {
test('random - reverse', function (t) {
var min = 0;
var max = 10;
var result = blocks.random({FROM:max, TO:min});
var result = blocks.random({FROM: max, TO: min});
t.ok(result >= min);
t.ok(result <= max);
t.end();
});
test('join', function (t) {
t.strictEqual(blocks.join({STRING1:'foo', STRING2:'bar'}), 'foobar');
t.strictEqual(blocks.join({STRING1:'1', STRING2:'2'}), '12');
t.strictEqual(blocks.join({STRING1: 'foo', STRING2: 'bar'}), 'foobar');
t.strictEqual(blocks.join({STRING1: '1', STRING2: '2'}), '12');
t.end();
});
test('letterOf', function (t) {
t.strictEqual(blocks.letterOf({STRING:'foo', LETTER:0}), '');
t.strictEqual(blocks.letterOf({STRING:'foo', LETTER:1}), 'f');
t.strictEqual(blocks.letterOf({STRING:'foo', LETTER:2}), 'o');
t.strictEqual(blocks.letterOf({STRING:'foo', LETTER:3}), 'o');
t.strictEqual(blocks.letterOf({STRING:'foo', LETTER:4}), '');
t.strictEqual(blocks.letterOf({STRING:'foo', LETTER:'bar'}), '');
t.strictEqual(blocks.letterOf({STRING: 'foo', LETTER: 0}), '');
t.strictEqual(blocks.letterOf({STRING: 'foo', LETTER: 1}), 'f');
t.strictEqual(blocks.letterOf({STRING: 'foo', LETTER: 2}), 'o');
t.strictEqual(blocks.letterOf({STRING: 'foo', LETTER: 3}), 'o');
t.strictEqual(blocks.letterOf({STRING: 'foo', LETTER: 4}), '');
t.strictEqual(blocks.letterOf({STRING: 'foo', LETTER: 'bar'}), '');
t.end();
});
test('length', function (t) {
t.strictEqual(blocks.length({STRING:''}), 0);
t.strictEqual(blocks.length({STRING:'foo'}), 3);
t.strictEqual(blocks.length({STRING:'1'}), 1);
t.strictEqual(blocks.length({STRING:'100'}), 3);
t.strictEqual(blocks.length({STRING: ''}), 0);
t.strictEqual(blocks.length({STRING: 'foo'}), 3);
t.strictEqual(blocks.length({STRING: '1'}), 1);
t.strictEqual(blocks.length({STRING: '100'}), 3);
t.end();
});
test('mod', function (t) {
t.strictEqual(blocks.mod({NUM1:1, NUM2:1}), 0);
t.strictEqual(blocks.mod({NUM1:3, NUM2:6}), 3);
t.strictEqual(blocks.mod({NUM1:-3, NUM2:6}), 3);
t.strictEqual(blocks.mod({NUM1: 1, NUM2: 1}), 0);
t.strictEqual(blocks.mod({NUM1: 3, NUM2: 6}), 3);
t.strictEqual(blocks.mod({NUM1: -3, NUM2: 6}), 3);
t.end();
});
test('round', function (t) {
t.strictEqual(blocks.round({NUM:1}), 1);
t.strictEqual(blocks.round({NUM:1.1}), 1);
t.strictEqual(blocks.round({NUM:1.5}), 2);
t.strictEqual(blocks.round({NUM: 1}), 1);
t.strictEqual(blocks.round({NUM: 1.1}), 1);
t.strictEqual(blocks.round({NUM: 1.5}), 2);
t.end();
});
test('mathop', function (t) {
t.strictEqual(blocks.mathop({OPERATOR:'abs', NUM:-1}), 1);
t.strictEqual(blocks.mathop({OPERATOR:'floor', NUM:1.5}), 1);
t.strictEqual(blocks.mathop({OPERATOR:'ceiling', NUM:0.1}), 1);
t.strictEqual(blocks.mathop({OPERATOR:'sqrt', NUM:1}), 1);
t.strictEqual(blocks.mathop({OPERATOR:'sin', NUM:1}), 0.01745240643728351);
t.strictEqual(blocks.mathop({OPERATOR:'cos', NUM:1}), 0.9998476951563913);
t.strictEqual(blocks.mathop({OPERATOR:'tan', NUM:1}), 0.017455064928217585);
t.strictEqual(blocks.mathop({OPERATOR:'asin', NUM:1}), 90);
t.strictEqual(blocks.mathop({OPERATOR:'acos', NUM:1}), 0);
t.strictEqual(blocks.mathop({OPERATOR:'atan', NUM:1}), 45);
t.strictEqual(blocks.mathop({OPERATOR:'ln', NUM:1}), 0);
t.strictEqual(blocks.mathop({OPERATOR:'log', NUM:1}), 0);
t.strictEqual(blocks.mathop({OPERATOR:'e ^', NUM:1}), 2.718281828459045);
t.strictEqual(blocks.mathop({OPERATOR:'10 ^', NUM:1}), 10);
t.strictEqual(blocks.mathop({OPERATOR:'undefined', NUM:1}), 0);
t.strictEqual(blocks.mathop({OPERATOR: 'abs', NUM: -1}), 1);
t.strictEqual(blocks.mathop({OPERATOR: 'floor', NUM: 1.5}), 1);
t.strictEqual(blocks.mathop({OPERATOR: 'ceiling', NUM: 0.1}), 1);
t.strictEqual(blocks.mathop({OPERATOR: 'sqrt', NUM: 1}), 1);
t.strictEqual(blocks.mathop({OPERATOR: 'sin', NUM: 1}), 0.01745240643728351);
t.strictEqual(blocks.mathop({OPERATOR: 'cos', NUM: 1}), 0.9998476951563913);
t.strictEqual(blocks.mathop({OPERATOR: 'tan', NUM: 1}), 0.017455064928217585);
t.strictEqual(blocks.mathop({OPERATOR: 'asin', NUM: 1}), 90);
t.strictEqual(blocks.mathop({OPERATOR: 'acos', NUM: 1}), 0);
t.strictEqual(blocks.mathop({OPERATOR: 'atan', NUM: 1}), 45);
t.strictEqual(blocks.mathop({OPERATOR: 'ln', NUM: 1}), 0);
t.strictEqual(blocks.mathop({OPERATOR: 'log', NUM: 1}), 0);
t.strictEqual(blocks.mathop({OPERATOR: 'e ^', NUM: 1}), 2.718281828459045);
t.strictEqual(blocks.mathop({OPERATOR: '10 ^', NUM: 1}), 10);
t.strictEqual(blocks.mathop({OPERATOR: 'undefined', NUM: 1}), 0);
t.end();
});

View file

@ -7,10 +7,10 @@ test('spec', function (t) {
t.end();
});
test('invalid inputs', function(t) {
test('invalid inputs', function (t) {
var nothing = adapter('not an object');
t.type(nothing, 'undefined');
nothing = adapter({noxmlproperty:true});
nothing = adapter({noxmlproperty: true});
t.type(nothing, 'undefined');
t.end();
});
@ -26,7 +26,7 @@ test('create event', function (t) {
t.type(result[0].opcode, 'string');
t.type(result[0].fields, 'object');
t.type(result[0].inputs, 'object');
t.type(result[0].inputs['DURATION'], 'object');
t.type(result[0].inputs.DURATION, 'object');
t.type(result[0].topLevel, 'boolean');
t.equal(result[0].topLevel, true);
@ -35,8 +35,8 @@ test('create event', function (t) {
t.type(result[1].opcode, 'string');
t.type(result[1].fields, 'object');
t.type(result[1].inputs, 'object');
t.type(result[1].fields['NUM'], 'object');
t.type(result[1].fields['NUM'].value, '10');
t.type(result[1].fields.NUM, 'object');
t.type(result[1].fields.NUM.value, '10');
t.type(result[1].topLevel, 'boolean');
t.equal(result[1].topLevel, false);
@ -50,18 +50,18 @@ test('create with branch', function (t) {
t.type(result[0].opcode, 'string');
t.type(result[0].fields, 'object');
t.type(result[0].inputs, 'object');
t.type(result[0].inputs['SUBSTACK'], 'object');
t.type(result[0].inputs.SUBSTACK, 'object');
t.type(result[0].topLevel, 'boolean');
t.equal(result[0].topLevel, true);
// In branch
var branchBlockId = result[0].inputs['SUBSTACK']['block'];
var branchShadowId = result[0].inputs['SUBSTACK']['shadow'];
var branchBlockId = result[0].inputs.SUBSTACK.block;
var branchShadowId = result[0].inputs.SUBSTACK.shadow;
t.type(branchBlockId, 'string');
t.equal(branchShadowId, null);
// Find actual branch block
var branchBlock = null;
for (var i = 0; i < result.length; i++) {
if (result[i].id == branchBlockId) {
if (result[i].id === branchBlockId) {
branchBlock = result[i];
}
}
@ -76,27 +76,27 @@ test('create with two branches', function (t) {
t.type(result[0].opcode, 'string');
t.type(result[0].fields, 'object');
t.type(result[0].inputs, 'object');
t.type(result[0].inputs['SUBSTACK'], 'object');
t.type(result[0].inputs['SUBSTACK2'], 'object');
t.type(result[0].inputs.SUBSTACK, 'object');
t.type(result[0].inputs.SUBSTACK2, 'object');
t.type(result[0].topLevel, 'boolean');
t.equal(result[0].topLevel, true);
// In branchs
var firstBranchBlockId = result[0].inputs['SUBSTACK']['block'];
var secondBranchBlockId = result[0].inputs['SUBSTACK2']['block'];
var firstBranchBlockId = result[0].inputs.SUBSTACK.block;
var secondBranchBlockId = result[0].inputs.SUBSTACK2.block;
t.type(firstBranchBlockId, 'string');
t.type(secondBranchBlockId, 'string');
var firstBranchShadowBlockId = result[0].inputs['SUBSTACK']['shadow'];
var secondBranchShadowBlockId = result[0].inputs['SUBSTACK2']['shadow'];
var firstBranchShadowBlockId = result[0].inputs.SUBSTACK.shadow;
var secondBranchShadowBlockId = result[0].inputs.SUBSTACK2.shadow;
t.equal(firstBranchShadowBlockId, null);
t.equal(secondBranchShadowBlockId, null);
// Find actual branch blocks
var firstBranchBlock = null;
var secondBranchBlock = null;
for (var i = 0; i < result.length; i++) {
if (result[i].id == firstBranchBlockId) {
if (result[i].id === firstBranchBlockId) {
firstBranchBlock = result[i];
}
if (result[i].id == secondBranchBlockId) {
if (result[i].id === secondBranchBlockId) {
secondBranchBlock = result[i];
}
}

View file

@ -245,8 +245,8 @@ test('create', function (t) {
topLevel: true
});
t.type(b._blocks['foo'], 'object');
t.equal(b._blocks['foo'].opcode, 'TEST_BLOCK');
t.type(b._blocks.foo, 'object');
t.equal(b._blocks.foo.opcode, 'TEST_BLOCK');
t.notEqual(b._scripts.indexOf('foo'), -1);
t.end();
});
@ -277,7 +277,7 @@ test('move', function (t) {
});
t.equal(b._scripts.length, 1);
t.equal(Object.keys(b._blocks).length, 2);
t.equal(b._blocks['foo'].next, 'bar');
t.equal(b._blocks.foo.next, 'bar');
// Detach 'bar' from 'foo'
b.moveBlock({
@ -286,7 +286,7 @@ test('move', function (t) {
});
t.equal(b._scripts.length, 2);
t.equal(Object.keys(b._blocks).length, 2);
t.equal(b._blocks['foo'].next, null);
t.equal(b._blocks.foo.next, null);
t.end();
});
@ -314,7 +314,7 @@ test('move into empty', function (t) {
newInput: 'fooInput',
newParent: 'foo'
});
t.equal(b._blocks['foo'].inputs['fooInput'].block, 'bar');
t.equal(b._blocks.foo.inputs.fooInput.block, 'bar');
t.end();
});
@ -326,7 +326,7 @@ test('move no obscure shadow', function (t) {
next: null,
fields: {},
inputs: {
'fooInput': {
fooInput: {
name: 'fooInput',
block: 'x',
shadow: 'y'
@ -347,8 +347,8 @@ test('move no obscure shadow', function (t) {
newInput: 'fooInput',
newParent: 'foo'
});
t.equal(b._blocks['foo'].inputs['fooInput'].block, 'bar');
t.equal(b._blocks['foo'].inputs['fooInput'].shadow, 'y');
t.equal(b._blocks.foo.inputs.fooInput.block, 'bar');
t.equal(b._blocks.foo.inputs.fooInput.shadow, 'y');
t.end();
});
@ -369,7 +369,7 @@ test('change', function (t) {
});
// Test that the field is updated
t.equal(b._blocks['foo'].fields.someField.value, 'initial-value');
t.equal(b._blocks.foo.fields.someField.value, 'initial-value');
b.changeBlock({
element: 'field',
@ -378,7 +378,7 @@ test('change', function (t) {
value: 'final-value'
});
t.equal(b._blocks['foo'].fields.someField.value, 'final-value');
t.equal(b._blocks.foo.fields.someField.value, 'final-value');
// Invalid cases
// No `element`
@ -387,7 +387,7 @@ test('change', function (t) {
name: 'someField',
value: 'invalid-value'
});
t.equal(b._blocks['foo'].fields.someField.value, 'final-value');
t.equal(b._blocks.foo.fields.someField.value, 'final-value');
// No block ID
b.changeBlock({
@ -395,7 +395,7 @@ test('change', function (t) {
name: 'someField',
value: 'invalid-value'
});
t.equal(b._blocks['foo'].fields.someField.value, 'final-value');
t.equal(b._blocks.foo.fields.someField.value, 'final-value');
// No such field
b.changeBlock({
@ -404,7 +404,7 @@ test('change', function (t) {
name: 'someWrongField',
value: 'final-value'
});
t.equal(b._blocks['foo'].fields.someField.value, 'final-value');
t.equal(b._blocks.foo.fields.someField.value, 'final-value');
t.end();
});
@ -423,7 +423,7 @@ test('delete', function (t) {
id: 'foo'
});
t.type(b._blocks['foo'], 'undefined');
t.type(b._blocks.foo, 'undefined');
t.equal(b._scripts.indexOf('foo'), -1);
t.end();
});
@ -459,9 +459,9 @@ test('delete chain', function (t) {
b.deleteBlock({
id: 'foo'
});
t.type(b._blocks['foo'], 'undefined');
t.type(b._blocks['foo2'], 'undefined');
t.type(b._blocks['foo3'], 'undefined');
t.type(b._blocks.foo, 'undefined');
t.type(b._blocks.foo2, 'undefined');
t.type(b._blocks.foo3, 'undefined');
t.equal(b._scripts.indexOf('foo'), -1);
t.equal(Object.keys(b._blocks).length, 0);
t.equal(b._scripts.length, 0);
@ -532,11 +532,11 @@ test('delete inputs', function (t) {
b.deleteBlock({
id: 'foo'
});
t.type(b._blocks['foo'], 'undefined');
t.type(b._blocks['foo2'], 'undefined');
t.type(b._blocks['foo3'], 'undefined');
t.type(b._blocks['foo4'], 'undefined');
t.type(b._blocks['foo5'], 'undefined');
t.type(b._blocks.foo, 'undefined');
t.type(b._blocks.foo2, 'undefined');
t.type(b._blocks.foo3, 'undefined');
t.type(b._blocks.foo4, 'undefined');
t.type(b._blocks.foo5, 'undefined');
t.equal(b._scripts.indexOf('foo'), -1);
t.equal(Object.keys(b._blocks).length, 0);
t.equal(b._scripts.length, 0);

View file

@ -2,7 +2,7 @@ var fs = require('fs');
var path = require('path');
var test = require('tap').test;
var clone = require('../../src/sprites/clone');
var renderedTarget = require('../../src/sprites/rendered-target');
var runtime = require('../../src/engine/runtime');
var sb2 = require('../../src/import/sb2import');
@ -25,7 +25,7 @@ test('default', function (t) {
t.type(rt, 'object');
t.type(rt.targets, 'object');
t.ok(rt.targets[0] instanceof clone);
t.ok(rt.targets[0] instanceof renderedTarget);
t.type(rt.targets[0].id, 'string');
t.type(rt.targets[0].blocks, 'object');
t.type(rt.targets[0].variables, 'object');
@ -36,7 +36,7 @@ test('default', function (t) {
t.equal(rt.targets[0].isOriginal, true);
t.equal(rt.targets[0].isStage, true);
t.ok(rt.targets[1] instanceof clone);
t.ok(rt.targets[1] instanceof renderedTarget);
t.type(rt.targets[1].id, 'string');
t.type(rt.targets[1].blocks, 'object');
t.type(rt.targets[1].variables, 'object');
@ -63,7 +63,7 @@ test('demo', function (t) {
t.type(rt, 'object');
t.type(rt.targets, 'object');
t.ok(rt.targets[0] instanceof clone);
t.ok(rt.targets[0] instanceof renderedTarget);
t.type(rt.targets[0].id, 'string');
t.type(rt.targets[0].blocks, 'object');
t.type(rt.targets[0].variables, 'object');
@ -74,7 +74,7 @@ test('demo', function (t) {
t.equal(rt.targets[0].isOriginal, true);
t.equal(rt.targets[0].isStage, true);
t.ok(rt.targets[1] instanceof clone);
t.ok(rt.targets[1] instanceof renderedTarget);
t.type(rt.targets[1].id, 'string');
t.type(rt.targets[1].blocks, 'object');
t.type(rt.targets[1].variables, 'object');

34
test/unit/io_clock.js Normal file
View file

@ -0,0 +1,34 @@
var test = require('tap').test;
var Clock = require('../../src/io/clock');
var Runtime = require('../../src/engine/runtime');
test('spec', function (t) {
var rt = new Runtime();
var c = new Clock(rt);
t.type(Clock, 'function');
t.type(c, 'object');
t.type(c.projectTimer, 'function');
t.type(c.pause, 'function');
t.type(c.resume, 'function');
t.type(c.resetProjectTimer, 'function');
t.end();
});
test('cycle', function (t) {
var rt = new Runtime();
var c = new Clock(rt);
t.ok(c.projectTimer() <= 0.1);
setTimeout(function () {
c.resetProjectTimer();
setTimeout(function () {
t.ok(c.projectTimer() > 0);
c.pause();
t.ok(c.projectTimer() > 0);
c.resume();
t.ok(c.projectTimer() > 0);
t.end();
}, 100);
}, 100);
});

73
test/unit/io_keyboard.js Normal file
View file

@ -0,0 +1,73 @@
var test = require('tap').test;
var Keyboard = require('../../src/io/keyboard');
var Runtime = require('../../src/engine/runtime');
test('spec', function (t) {
var rt = new Runtime();
var k = new Keyboard(rt);
t.type(k, 'object');
t.type(k.postData, 'function');
t.type(k.getKeyIsDown, 'function');
t.end();
});
test('space', function (t) {
var rt = new Runtime();
var k = new Keyboard(rt);
k.postData({
keyCode: 32,
isDown: true
});
t.strictDeepEquals(k._keysPressed, [32]);
t.strictEquals(k.getKeyIsDown('space'), true);
t.strictEquals(k.getKeyIsDown('any'), true);
t.end();
});
test('letter', function (t) {
var rt = new Runtime();
var k = new Keyboard(rt);
k.postData({
keyCode: 65,
isDown: true
});
t.strictDeepEquals(k._keysPressed, [65]);
t.strictEquals(k.getKeyIsDown('a'), true);
t.strictEquals(k.getKeyIsDown('any'), true);
t.end();
});
test('number', function (t) {
var rt = new Runtime();
var k = new Keyboard(rt);
k.postData({
keyCode: 49,
isDown: true
});
t.strictDeepEquals(k._keysPressed, [49]);
t.strictEquals(k.getKeyIsDown(49), true);
t.strictEquals(k.getKeyIsDown('any'), true);
t.end();
});
test('keyup', function (t) {
var rt = new Runtime();
var k = new Keyboard(rt);
k.postData({
keyCode: 37,
isDown: true
});
k.postData({
keyCode: 37,
isDown: false
});
t.strictDeepEquals(k._keysPressed, []);
t.strictEquals(k.getKeyIsDown(37), false);
t.strictEquals(k.getKeyIsDown('any'), false);
t.end();
});

49
test/unit/io_mouse.js Normal file
View file

@ -0,0 +1,49 @@
var test = require('tap').test;
var Mouse = require('../../src/io/mouse');
var Runtime = require('../../src/engine/runtime');
test('spec', function (t) {
var rt = new Runtime();
var m = new Mouse(rt);
t.type(m, 'object');
t.type(m.postData, 'function');
t.type(m.getX, 'function');
t.type(m.getY, 'function');
t.type(m.getIsDown, 'function');
t.end();
});
test('mouseUp', function (t) {
var rt = new Runtime();
var m = new Mouse(rt);
m.postData({
x: 1,
y: 10,
isDown: false,
canvasWidth: 480,
canvasHeight: 360
});
t.strictEquals(m.getX(), -239);
t.strictEquals(m.getY(), 170);
t.strictEquals(m.getIsDown(), false);
t.end();
});
test('mouseDown', function (t) {
var rt = new Runtime();
var m = new Mouse(rt);
m.postData({
x: 10,
y: 100,
isDown: true,
canvasWidth: 480,
canvasHeight: 360
});
t.strictEquals(m.getX(), -230);
t.strictEquals(m.getY(), 80);
t.strictEquals(m.getIsDown(), true);
t.end();
});

View file

@ -1,13 +1,13 @@
var test = require('tap').test;
var Clone = require('../../src/sprites/clone');
var RenderedTarget = require('../../src/sprites/rendered-target');
var Sprite = require('../../src/sprites/sprite');
test('clone effects', function (t) {
// Create two clones and ensure they have different graphic effect objects.
// Regression test for Github issue #224
var spr = new Sprite();
var a = new Clone(spr, null);
var b = new Clone(spr, null);
var a = new RenderedTarget(spr, null);
var b = new RenderedTarget(spr, null);
t.ok(a.effects !== b.effects);
t.end();
});

View file

@ -75,19 +75,19 @@ test('toString', function (t) {
test('toRbgColorList', function (t) {
// Hex (minimal, see "color" util tests)
t.deepEqual(cast.toRgbColorList('#000'), [0,0,0]);
t.deepEqual(cast.toRgbColorList('#000000'), [0,0,0]);
t.deepEqual(cast.toRgbColorList('#fff'), [255,255,255]);
t.deepEqual(cast.toRgbColorList('#ffffff'), [255,255,255]);
t.deepEqual(cast.toRgbColorList('#000'), [0, 0, 0]);
t.deepEqual(cast.toRgbColorList('#000000'), [0, 0, 0]);
t.deepEqual(cast.toRgbColorList('#fff'), [255, 255, 255]);
t.deepEqual(cast.toRgbColorList('#ffffff'), [255, 255, 255]);
// Decimal (minimal, see "color" util tests)
t.deepEqual(cast.toRgbColorList(0), [0,0,0]);
t.deepEqual(cast.toRgbColorList(1), [0,0,1]);
t.deepEqual(cast.toRgbColorList(16777215), [255,255,255]);
t.deepEqual(cast.toRgbColorList(0), [0, 0, 0]);
t.deepEqual(cast.toRgbColorList(1), [0, 0, 1]);
t.deepEqual(cast.toRgbColorList(16777215), [255, 255, 255]);
// Malformed
t.deepEqual(cast.toRgbColorList('ffffff'), [0,0,0]);
t.deepEqual(cast.toRgbColorList('foobar'), [0,0,0]);
t.deepEqual(cast.toRgbColorList('ffffff'), [0, 0, 0]);
t.deepEqual(cast.toRgbColorList('foobar'), [0, 0, 0]);
t.end();
});
@ -144,7 +144,7 @@ test('isInt', function (t) {
});
test('toListIndex', function (t) {
var list = [0,1,2,3,4,5];
var list = [0, 1, 2, 3, 4, 5];
var empty = [];
// Valid

View file

@ -11,25 +11,25 @@ test('decimalToHex', function (t) {
});
test('decimalToRgb', function (t) {
t.deepEqual(color.decimalToRgb(0), {r:0,g:0,b:0});
t.deepEqual(color.decimalToRgb(1), {r:0,g:0,b:1});
t.deepEqual(color.decimalToRgb(16777215), {r:255,g:255,b:255});
t.deepEqual(color.decimalToRgb(-16777215), {r:0,g:0,b:1});
t.deepEqual(color.decimalToRgb(99999999), {r:245,g:224,b:255});
t.deepEqual(color.decimalToRgb(0), {r: 0, g: 0, b: 0});
t.deepEqual(color.decimalToRgb(1), {r: 0, g: 0, b: 1});
t.deepEqual(color.decimalToRgb(16777215), {r: 255, g: 255, b: 255});
t.deepEqual(color.decimalToRgb(-16777215), {r: 0, g: 0, b: 1});
t.deepEqual(color.decimalToRgb(99999999), {r: 245, g: 224, b: 255});
t.end();
});
test('hexToRgb', function (t) {
t.deepEqual(color.hexToRgb('#000'), {r:0,g:0,b:0});
t.deepEqual(color.hexToRgb('#000000'), {r:0,g:0,b:0});
t.deepEqual(color.hexToRgb('#fff'), {r:255,g:255,b:255});
t.deepEqual(color.hexToRgb('#ffffff'), {r:255,g:255,b:255});
t.deepEqual(color.hexToRgb('#0fa'), {r:0,g:255,b:170});
t.deepEqual(color.hexToRgb('#00ffaa'), {r:0,g:255,b:170});
t.deepEqual(color.hexToRgb('#000'), {r: 0, g: 0, b: 0});
t.deepEqual(color.hexToRgb('#000000'), {r: 0, g: 0, b: 0});
t.deepEqual(color.hexToRgb('#fff'), {r: 255, g: 255, b: 255});
t.deepEqual(color.hexToRgb('#ffffff'), {r: 255, g: 255, b: 255});
t.deepEqual(color.hexToRgb('#0fa'), {r: 0, g: 255, b: 170});
t.deepEqual(color.hexToRgb('#00ffaa'), {r: 0, g: 255, b: 170});
t.deepEqual(color.hexToRgb('000'), {r:0,g:0,b:0});
t.deepEqual(color.hexToRgb('fff'), {r:255,g:255,b:255});
t.deepEqual(color.hexToRgb('00ffaa'), {r:0,g:255,b:170});
t.deepEqual(color.hexToRgb('000'), {r: 0, g: 0, b: 0});
t.deepEqual(color.hexToRgb('fff'), {r: 255, g: 255, b: 255});
t.deepEqual(color.hexToRgb('00ffaa'), {r: 0, g: 255, b: 170});
t.deepEqual(color.hexToRgb('0'), null);
t.deepEqual(color.hexToRgb('hello world'), null);
@ -38,16 +38,16 @@ test('hexToRgb', function (t) {
});
test('rgbToHex', function (t) {
t.strictEqual(color.rgbToHex({r:0,g:0,b:0}), '#000000');
t.strictEqual(color.rgbToHex({r:255,g:255,b:255}), '#ffffff');
t.strictEqual(color.rgbToHex({r:0,g:255,b:170}), '#00ffaa');
t.strictEqual(color.rgbToHex({r: 0, g: 0, b: 0}), '#000000');
t.strictEqual(color.rgbToHex({r: 255, g: 255, b: 255}), '#ffffff');
t.strictEqual(color.rgbToHex({r: 0, g: 255, b: 170}), '#00ffaa');
t.end();
});
test('rgbToDecimal', function (t) {
t.strictEqual(color.rgbToDecimal({r:0,g:0,b:0}), 0);
t.strictEqual(color.rgbToDecimal({r:255,g:255,b:255}), 16777215);
t.strictEqual(color.rgbToDecimal({r:0,g:255,b:170}), 65450);
t.strictEqual(color.rgbToDecimal({r: 0, g: 0, b: 0}), 0);
t.strictEqual(color.rgbToDecimal({r: 255, g: 255, b: 255}), 16777215);
t.strictEqual(color.rgbToDecimal({r: 0, g: 255, b: 170}), 65450);
t.end();
});

View file

@ -6,7 +6,8 @@ var webpack = require('webpack');
var base = {
devServer: {
contentBase: path.resolve(__dirname, 'playground'),
host: '0.0.0.0'
host: '0.0.0.0',
port: process.env.PORT || 8073
},
module: {
loaders: [
@ -50,7 +51,7 @@ module.exports = [
// Webpack-compatible
defaultsDeep({}, base, {
entry: {
'dist': './src/index.js'
dist: './src/index.js'
},
output: {
@ -63,8 +64,8 @@ module.exports = [
// Playground
defaultsDeep({}, base, {
entry: {
'vm': './src/index.js',
'vendor': [
vm: './src/index.js',
vendor: [
// FPS counter
'stats.js/build/stats.min.js',
// Syntax highlighter
@ -109,9 +110,6 @@ module.exports = [
to: 'media'
}, {
from: 'node_modules/highlightjs/styles/zenburn.css'
}, {
from: 'assets',
to: 'assets'
}])
])
})