Merge remote-tracking branch 'upstream/develop' into develop
This commit is contained in:
9 files changed
+762
-235
No files matched your search
Generated
+618
-205
File diff suppressed because it is too large.
Load diff
@@ -13,6 +13,8 @@ class Scratch3ControlBlocks {
|
||||
* @type {number}
|
||||
*/
|
||||
this._counter = 0;
|
||||
|
||||
this.runtime.on('RUNTIME_DISPOSED', this.clearCounter.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -34,7 +34,7 @@ class Scratch3LooksBlocks {
|
||||
this.runtime.on('targetWasRemoved', this._onTargetWillExit);
|
||||
|
||||
// Enable other blocks to use bubbles like ask/answer
|
||||
this.runtime.on('SAY', this._updateBubble);
|
||||
this.runtime.on(Scratch3LooksBlocks.SAY_OR_THINK, this._updateBubble);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,6 +60,16 @@ class Scratch3LooksBlocks {
|
||||
return 'Scratch.looks';
|
||||
}
|
||||
|
||||
/**
|
||||
* Event name for a text bubble being created or updated.
|
||||
* @const {string}
|
||||
*/
|
||||
static get SAY_OR_THINK () {
|
||||
// There are currently many places in the codebase which explicitly refer to this event by the string 'SAY',
|
||||
// so keep this as the string 'SAY' for now rather than changing it to 'SAY_OR_THINK' and breaking things.
|
||||
return 'SAY';
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit for say bubble string.
|
||||
* @const {string}
|
||||
@@ -233,6 +243,29 @@ class Scratch3LooksBlocks {
|
||||
this._positionBubble(target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Properly format text for a text bubble.
|
||||
* @param {string} text The text to be formatted
|
||||
* @return {string} The formatted text
|
||||
* @private
|
||||
*/
|
||||
_formatBubbleText (text) {
|
||||
if (text === '') return text;
|
||||
|
||||
// Non-integers should be rounded to 2 decimal places (no more, no less), unless they're small enough that
|
||||
// rounding would display them as 0.00. This matches 2.0's behavior:
|
||||
// https://github.com/LLK/scratch-flash/blob/2e4a402ceb205a042887f54b26eebe1c2e6da6c0/src/scratch/ScratchSprite.as#L579-L585
|
||||
if (typeof text === 'number' &&
|
||||
Math.abs(text) >= 0.01 && text % 1 !== 0) {
|
||||
text = text.toFixed(2);
|
||||
}
|
||||
|
||||
// Limit the length of the string.
|
||||
text = String(text).substr(0, Scratch3LooksBlocks.SAY_BUBBLE_LIMIT);
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* The entry point for say/think blocks. Clears existing bubble if the text is empty.
|
||||
* Set the bubble custom state and then call _renderBubble.
|
||||
@@ -244,7 +277,7 @@ class Scratch3LooksBlocks {
|
||||
_updateBubble (target, type, text) {
|
||||
const bubbleState = this._getBubbleState(target);
|
||||
bubbleState.type = type;
|
||||
bubbleState.text = text;
|
||||
bubbleState.text = this._formatBubbleText(text);
|
||||
bubbleState.usageId = uid();
|
||||
this._renderBubble(target);
|
||||
}
|
||||
@@ -304,11 +337,7 @@ class Scratch3LooksBlocks {
|
||||
this._say(message, util.target);
|
||||
}
|
||||
_say (message, target) { // used by compiler
|
||||
if (typeof message === 'number') {
|
||||
message = parseFloat(message.toFixed(2));
|
||||
}
|
||||
message = String(message).substr(0, Scratch3LooksBlocks.SAY_BUBBLE_LIMIT);
|
||||
this.runtime.emit('SAY', target, 'say', message);
|
||||
this.runtime.emit(Scratch3LooksBlocks.SAY_OR_THINK, target, 'say', message);
|
||||
}
|
||||
|
||||
sayforsecs (args, util) {
|
||||
@@ -328,7 +357,7 @@ class Scratch3LooksBlocks {
|
||||
}
|
||||
|
||||
think (args, util) {
|
||||
this._updateBubble(util.target, 'think', String(args.MESSAGE).substr(0, Scratch3LooksBlocks.SAY_BUBBLE_LIMIT));
|
||||
this.runtime.emit(Scratch3LooksBlocks.SAY_OR_THINK, util.target, 'think', args.MESSAGE);
|
||||
}
|
||||
|
||||
thinkforsecs (args, util) {
|
||||
@@ -421,7 +450,7 @@ class Scratch3LooksBlocks {
|
||||
const lowerBound = 0;
|
||||
const upperBound = numCostumes - 1;
|
||||
const costumeToExclude = stage.currentCostume;
|
||||
|
||||
|
||||
const nextCostume = MathUtil.inclusiveRandIntWithout(lowerBound, upperBound, costumeToExclude);
|
||||
|
||||
stage.setCostume(nextCostume);
|
||||
|
||||
@@ -44,6 +44,7 @@ class Scratch3SensingBlocks {
|
||||
this.runtime.on('PROJECT_START', this._resetAnswer.bind(this));
|
||||
this.runtime.on('PROJECT_STOP_ALL', this._clearAllQuestions.bind(this));
|
||||
this.runtime.on('STOP_FOR_TARGET', this._clearTargetQuestions.bind(this));
|
||||
this.runtime.on('RUNTIME_DISPOSED', this._resetAnswer.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+18
-2
@@ -1375,13 +1375,28 @@ class Runtime extends EventEmitter {
|
||||
|
||||
/**
|
||||
* @returns {Array.<object>} scratch-blocks XML for each category of extension blocks, in category order.
|
||||
* @param {?Target} [target] - the active editing target (optional)
|
||||
* @property {string} id - the category / extension ID
|
||||
* @property {string} xml - the XML text for this category, starting with `<category>` and ending with `</category>`
|
||||
*/
|
||||
getBlocksXML () {
|
||||
getBlocksXML (target) {
|
||||
return this._blockInfo.map(categoryInfo => {
|
||||
const {name, color1, color2} = categoryInfo;
|
||||
const paletteBlocks = categoryInfo.blocks.filter(block => !block.info.hideFromPalette);
|
||||
// Filter out blocks that aren't supposed to be shown on this target, as determined by the block info's
|
||||
// `hideFromPalette` and `filter` properties.
|
||||
const paletteBlocks = categoryInfo.blocks.filter(block => {
|
||||
let blockFilterIncludesTarget = true;
|
||||
// If an editing target is not passed, include all blocks
|
||||
// If the block info doesn't include a `filter` property, always include it
|
||||
if (target && block.info.filter) {
|
||||
blockFilterIncludesTarget = block.info.filter.includes(
|
||||
target.isStage ? TargetType.STAGE : TargetType.SPRITE
|
||||
);
|
||||
}
|
||||
// If the block info's `hideFromPalette` is true, then filter out this block
|
||||
return blockFilterIncludesTarget && !block.info.hideFromPalette;
|
||||
});
|
||||
|
||||
const colorXML = `colour="${color1}" secondaryColour="${color2}"`;
|
||||
|
||||
// Use a menu icon if there is one. Otherwise, use the block icon. If there's no icon,
|
||||
@@ -1858,6 +1873,7 @@ class Runtime extends EventEmitter {
|
||||
this.targets.map(this.disposeTarget, this);
|
||||
this._monitorState = OrderedMap({});
|
||||
this.emit(Runtime.RUNTIME_DISPOSED);
|
||||
this.ioDevices.clock.resetProjectTimer();
|
||||
// @todo clear out extensions? turboMode? etc.
|
||||
|
||||
// *********** Cloud *******************
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const ArgumentType = require('../../extension-support/argument-type');
|
||||
const BlockType = require('../../extension-support/block-type');
|
||||
const TargetType = require('../../extension-support/target-type');
|
||||
const Cast = require('../../util/cast');
|
||||
const Clone = require('../../util/clone');
|
||||
const Color = require('../../util/color');
|
||||
@@ -306,7 +307,8 @@ class Scratch3PenBlocks {
|
||||
id: 'pen.stamp',
|
||||
default: 'stamp',
|
||||
description: 'render current costume on the background'
|
||||
})
|
||||
}),
|
||||
filter: [TargetType.SPRITE]
|
||||
},
|
||||
{
|
||||
opcode: 'penDown',
|
||||
@@ -315,7 +317,8 @@ class Scratch3PenBlocks {
|
||||
id: 'pen.penDown',
|
||||
default: 'pen down',
|
||||
description: 'start leaving a trail when the sprite moves'
|
||||
})
|
||||
}),
|
||||
filter: [TargetType.SPRITE]
|
||||
},
|
||||
{
|
||||
opcode: 'penUp',
|
||||
@@ -324,7 +327,8 @@ class Scratch3PenBlocks {
|
||||
id: 'pen.penUp',
|
||||
default: 'pen up',
|
||||
description: 'stop leaving a trail behind the sprite'
|
||||
})
|
||||
}),
|
||||
filter: [TargetType.SPRITE]
|
||||
},
|
||||
{
|
||||
opcode: 'setPenColorToColor',
|
||||
@@ -338,7 +342,8 @@ class Scratch3PenBlocks {
|
||||
COLOR: {
|
||||
type: ArgumentType.COLOR
|
||||
}
|
||||
}
|
||||
},
|
||||
filter: [TargetType.SPRITE]
|
||||
},
|
||||
{
|
||||
opcode: 'changePenColorParamBy',
|
||||
@@ -358,7 +363,8 @@ class Scratch3PenBlocks {
|
||||
type: ArgumentType.NUMBER,
|
||||
defaultValue: 10
|
||||
}
|
||||
}
|
||||
},
|
||||
filter: [TargetType.SPRITE]
|
||||
},
|
||||
{
|
||||
opcode: 'setPenColorParamTo',
|
||||
@@ -378,7 +384,8 @@ class Scratch3PenBlocks {
|
||||
type: ArgumentType.NUMBER,
|
||||
defaultValue: 50
|
||||
}
|
||||
}
|
||||
},
|
||||
filter: [TargetType.SPRITE]
|
||||
},
|
||||
{
|
||||
opcode: 'changePenSizeBy',
|
||||
@@ -393,7 +400,8 @@ class Scratch3PenBlocks {
|
||||
type: ArgumentType.NUMBER,
|
||||
defaultValue: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
filter: [TargetType.SPRITE]
|
||||
},
|
||||
{
|
||||
opcode: 'setPenSizeTo',
|
||||
@@ -408,7 +416,8 @@ class Scratch3PenBlocks {
|
||||
type: ArgumentType.NUMBER,
|
||||
defaultValue: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
filter: [TargetType.SPRITE]
|
||||
},
|
||||
/* Legacy blocks, should not be shown in flyout */
|
||||
{
|
||||
|
||||
+30
-11
@@ -15,7 +15,9 @@ const util = {
|
||||
{name: 'second name'},
|
||||
{name: 'third name'}
|
||||
]
|
||||
}
|
||||
},
|
||||
_customState: {},
|
||||
getCustomState: () => util.target._customState
|
||||
}
|
||||
};
|
||||
|
||||
@@ -159,7 +161,7 @@ test('switch backdrop block runs correctly', t => {
|
||||
t.strictEqual(testBackdrop(['a', 'b', 'c', 'd'], -1), 3);
|
||||
t.strictEqual(testBackdrop(['a', 'b', 'c', 'd'], -4), 4);
|
||||
t.strictEqual(testBackdrop(['a', 'b', 'c', 'd'], 10), 2);
|
||||
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
@@ -195,21 +197,38 @@ test('getBackdropNumberName can return costume name', t => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('numbers should be rounded to two decimals in say', t => {
|
||||
test('numbers should be rounded properly in say/think', t => {
|
||||
const rt = new Runtime();
|
||||
const looks = new Looks(rt);
|
||||
|
||||
const args = {MESSAGE: 3.14159};
|
||||
const expectedSayString = '3.14';
|
||||
let expectedSayString;
|
||||
|
||||
rt.removeAllListeners('SAY'); // Prevent say blocks from executing
|
||||
|
||||
rt.addListener('SAY', (target, type, sayString) => {
|
||||
t.strictEqual(sayString, expectedSayString);
|
||||
t.end();
|
||||
rt.addListener('SAY', () => {
|
||||
const bubbleState = util.target.getCustomState(Looks.STATE_KEY);
|
||||
t.strictEqual(bubbleState.text, expectedSayString);
|
||||
});
|
||||
|
||||
looks.say(args, util);
|
||||
expectedSayString = '3.14';
|
||||
looks.say({MESSAGE: 3.14159}, util, 'say bubble should round to 2 decimal places');
|
||||
looks.think({MESSAGE: 3.14159}, util, 'think bubble should round to 2 decimal places');
|
||||
|
||||
expectedSayString = '3';
|
||||
looks.say({MESSAGE: 3}, util, 'say bubble should not add decimal places to integers');
|
||||
looks.think({MESSAGE: 3}, util, 'think bubble should not add decimal places to integers');
|
||||
|
||||
expectedSayString = '3.10';
|
||||
looks.say({MESSAGE: 3.1}, util, 'say bubble should round to 2 decimal places, even if only 1 is needed');
|
||||
looks.think({MESSAGE: 3.1}, util, 'think bubble should round to 2 decimal places, even if only 1 is needed');
|
||||
|
||||
expectedSayString = '0.00125';
|
||||
looks.say({MESSAGE: 0.00125}, util, 'say bubble should not round if it would display small numbers as 0');
|
||||
looks.think({MESSAGE: 0.00125}, util, 'think bubble should not round if it would display small numbers as 0');
|
||||
|
||||
expectedSayString = '1.99999';
|
||||
looks.say({MESSAGE: '1.99999'}, util, 'say bubble should not round strings');
|
||||
looks.think({MESSAGE: '1.99999'}, util, 'think bubble should not round strings');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('clamp graphic effects', t => {
|
||||
|
||||
@@ -153,6 +153,23 @@ test('ask and answer with a visible target', t => {
|
||||
s.askAndWait({QUESTION: expectedQuestion}, util);
|
||||
});
|
||||
|
||||
test('answer gets reset when runtime is disposed', t => {
|
||||
const rt = new Runtime();
|
||||
const s = new Sensing(rt);
|
||||
const util = {target: {visible: false}};
|
||||
const expectedAnswer = 'the answer';
|
||||
|
||||
rt.addListener('QUESTION', () => rt.emit('ANSWER', expectedAnswer));
|
||||
const promise = s.askAndWait({QUESTION: ''}, util);
|
||||
|
||||
promise.then(() => t.strictEqual(s.getAnswer(), expectedAnswer))
|
||||
.then(() => rt.dispose())
|
||||
.then(() => {
|
||||
t.strictEqual(s.getAnswer(), '');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('set drag mode', t => {
|
||||
const runtime = new Runtime();
|
||||
runtime.requestTargetsUpdate = () => {}; // noop for testing
|
||||
|
||||
@@ -250,3 +250,24 @@ test('Disposing the runtime emits an event', t => {
|
||||
t.equal(disposed, true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Clock is reset on runtime dispose', t => {
|
||||
const rt = new Runtime();
|
||||
const c = rt.ioDevices.clock;
|
||||
let simulatedTime = 0;
|
||||
|
||||
c._projectTimer = {
|
||||
timeElapsed: () => simulatedTime,
|
||||
start: () => {
|
||||
simulatedTime = 0;
|
||||
}
|
||||
};
|
||||
|
||||
t.ok(c.projectTimer() === 0);
|
||||
simulatedTime += 1000;
|
||||
t.ok(c.projectTimer() === 1);
|
||||
rt.dispose();
|
||||
// When the runtime is disposed, the clock should be reset
|
||||
t.ok(c.projectTimer() === 0);
|
||||
t.end();
|
||||
});
|
||||
Reference in new issue
Block a user