2017-04-17 15:10:04 -04:00
|
|
|
const Cast = require('../util/cast');
|
2017-10-05 10:24:20 -04:00
|
|
|
const Clone = require('../util/clone');
|
|
|
|
const RenderedTarget = require('../sprites/rendered-target');
|
2018-04-26 11:29:24 -04:00
|
|
|
const uid = require('../util/uid');
|
2018-05-15 22:22:44 -04:00
|
|
|
const StageLayering = require('../engine/stage-layering');
|
2017-10-05 10:24:20 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @typedef {object} BubbleState - the bubble state associated with a particular target.
|
|
|
|
* @property {Boolean} onSpriteRight - tracks whether the bubble is right or left of the sprite.
|
|
|
|
* @property {?int} drawableId - the ID of the associated bubble Drawable, null if none.
|
|
|
|
* @property {string} text - the text of the bubble.
|
|
|
|
* @property {string} type - the type of the bubble, "say" or "think"
|
2018-04-27 09:21:38 -04:00
|
|
|
* @property {?string} usageId - ID indicating the most recent usage of the say/think bubble.
|
|
|
|
* Used for comparison when determining whether to clear a say/think bubble.
|
2017-10-05 10:24:20 -04:00
|
|
|
*/
|
2016-09-08 09:40:27 -04:00
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
class Scratch3LooksBlocks {
|
|
|
|
constructor (runtime) {
|
|
|
|
/**
|
|
|
|
* The runtime instantiating this block package.
|
|
|
|
* @type {Runtime}
|
|
|
|
*/
|
|
|
|
this.runtime = runtime;
|
2017-10-05 10:24:20 -04:00
|
|
|
|
2018-04-20 10:44:55 -04:00
|
|
|
this._onTargetChanged = this._onTargetChanged.bind(this);
|
2017-10-05 17:03:30 -04:00
|
|
|
this._onResetBubbles = this._onResetBubbles.bind(this);
|
2017-10-06 13:43:07 -04:00
|
|
|
this._onTargetWillExit = this._onTargetWillExit.bind(this);
|
2017-11-08 10:37:38 -05:00
|
|
|
this._updateBubble = this._updateBubble.bind(this);
|
2017-10-05 17:03:30 -04:00
|
|
|
|
|
|
|
// Reset all bubbles on start/stop
|
2017-10-06 13:43:07 -04:00
|
|
|
this.runtime.on('PROJECT_STOP_ALL', this._onResetBubbles);
|
|
|
|
this.runtime.on('targetWasRemoved', this._onTargetWillExit);
|
2017-11-08 10:37:38 -05:00
|
|
|
|
|
|
|
// Enable other blocks to use bubbles like ask/answer
|
|
|
|
this.runtime.on('SAY', this._updateBubble);
|
2017-10-05 10:24:20 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The default bubble state, to be used when a target has no existing bubble state.
|
|
|
|
* @type {BubbleState}
|
|
|
|
*/
|
|
|
|
static get DEFAULT_BUBBLE_STATE () {
|
|
|
|
return {
|
|
|
|
drawableId: null,
|
|
|
|
onSpriteRight: true,
|
|
|
|
skinId: null,
|
|
|
|
text: '',
|
2018-04-26 11:29:24 -04:00
|
|
|
type: 'say',
|
2018-04-27 09:21:38 -04:00
|
|
|
usageId: null
|
2017-10-05 10:24:20 -04:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The key to load & store a target's bubble-related state.
|
|
|
|
* @type {string}
|
|
|
|
*/
|
|
|
|
static get STATE_KEY () {
|
|
|
|
return 'Scratch.looks';
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {Target} target - collect bubble state for this target. Probably, but not necessarily, a RenderedTarget.
|
|
|
|
* @returns {BubbleState} the mutable bubble state associated with that target. This will be created if necessary.
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
_getBubbleState (target) {
|
|
|
|
let bubbleState = target.getCustomState(Scratch3LooksBlocks.STATE_KEY);
|
|
|
|
if (!bubbleState) {
|
|
|
|
bubbleState = Clone.simple(Scratch3LooksBlocks.DEFAULT_BUBBLE_STATE);
|
|
|
|
target.setCustomState(Scratch3LooksBlocks.STATE_KEY, bubbleState);
|
|
|
|
}
|
|
|
|
return bubbleState;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-10-06 13:43:07 -04:00
|
|
|
* Handle a target which has moved.
|
2017-10-05 10:24:20 -04:00
|
|
|
* @param {RenderedTarget} target - the target which has moved.
|
|
|
|
* @private
|
|
|
|
*/
|
2018-04-20 10:44:55 -04:00
|
|
|
_onTargetChanged (target) {
|
2017-10-05 10:24:20 -04:00
|
|
|
const bubbleState = this._getBubbleState(target);
|
2017-10-23 13:03:10 -04:00
|
|
|
if (bubbleState.drawableId) {
|
2017-10-05 10:24:20 -04:00
|
|
|
this._positionBubble(target);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-10-06 13:43:07 -04:00
|
|
|
/**
|
2017-10-06 15:24:29 -04:00
|
|
|
* Handle a target which is exiting.
|
|
|
|
* @param {RenderedTarget} target - the target.
|
2017-10-06 13:43:07 -04:00
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
_onTargetWillExit (target) {
|
|
|
|
const bubbleState = this._getBubbleState(target);
|
2017-10-23 13:03:10 -04:00
|
|
|
if (bubbleState.drawableId && bubbleState.skinId) {
|
2018-05-20 19:49:42 -04:00
|
|
|
this.runtime.renderer.destroyDrawable(bubbleState.drawableId, StageLayering.SPRITE_LAYER);
|
2017-10-06 13:43:07 -04:00
|
|
|
this.runtime.renderer.destroySkin(bubbleState.skinId);
|
2017-10-23 13:03:10 -04:00
|
|
|
bubbleState.drawableId = null;
|
2017-10-06 15:24:29 -04:00
|
|
|
bubbleState.skinId = null;
|
2017-10-23 13:03:10 -04:00
|
|
|
this.runtime.requestRedraw();
|
2017-10-06 13:43:07 -04:00
|
|
|
}
|
2018-04-20 10:44:55 -04:00
|
|
|
target.removeListener(RenderedTarget.EVENT_TARGET_VISUAL_CHANGE, this._onTargetChanged);
|
2017-10-06 13:43:07 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle project start/stop by clearing all visible bubbles.
|
|
|
|
* @private
|
|
|
|
*/
|
2017-10-05 17:03:30 -04:00
|
|
|
_onResetBubbles () {
|
2017-10-06 13:43:07 -04:00
|
|
|
for (let n = 0; n < this.runtime.targets.length; n++) {
|
2018-06-28 16:44:33 -04:00
|
|
|
const bubbleState = this._getBubbleState(this.runtime.targets[n]);
|
|
|
|
bubbleState.text = '';
|
2017-10-06 15:24:29 -04:00
|
|
|
this._onTargetWillExit(this.runtime.targets[n]);
|
2017-10-06 13:43:07 -04:00
|
|
|
}
|
2017-10-23 13:03:10 -04:00
|
|
|
clearTimeout(this._bubbleTimeout);
|
2017-10-05 17:03:30 -04:00
|
|
|
}
|
|
|
|
|
2017-10-06 13:43:07 -04:00
|
|
|
/**
|
2017-10-06 15:24:29 -04:00
|
|
|
* Position the bubble of a target. If it doesn't fit on the specified side, flip and rerender.
|
2017-10-06 13:43:07 -04:00
|
|
|
* @param {!Target} target Target whose bubble needs positioning.
|
|
|
|
* @private
|
|
|
|
*/
|
2017-10-05 10:24:20 -04:00
|
|
|
_positionBubble (target) {
|
2018-04-20 10:45:34 -04:00
|
|
|
if (!target.visible) return;
|
2017-10-05 10:24:20 -04:00
|
|
|
const bubbleState = this._getBubbleState(target);
|
2018-04-10 09:50:18 -04:00
|
|
|
const [bubbleWidth, bubbleHeight] = this.runtime.renderer.getCurrentSkinSize(bubbleState.drawableId);
|
2018-04-20 10:45:54 -04:00
|
|
|
let targetBounds;
|
|
|
|
try {
|
|
|
|
targetBounds = target.getBoundsForBubble();
|
|
|
|
} catch (error_) {
|
|
|
|
// Bounds calculation could fail (e.g. on empty costumes), in that case
|
|
|
|
// use the x/y position of the target.
|
|
|
|
targetBounds = {
|
|
|
|
left: target.x,
|
|
|
|
right: target.x,
|
|
|
|
top: target.y,
|
|
|
|
bottom: target.y
|
|
|
|
};
|
|
|
|
}
|
2018-04-30 14:26:25 -04:00
|
|
|
const stageSize = this.runtime.renderer.getNativeSize();
|
|
|
|
const stageBounds = {
|
|
|
|
left: -stageSize[0] / 2,
|
|
|
|
right: stageSize[0] / 2,
|
|
|
|
top: stageSize[1] / 2,
|
|
|
|
bottom: -stageSize[1] / 2
|
|
|
|
};
|
2017-10-11 15:58:06 -04:00
|
|
|
if (bubbleState.onSpriteRight && bubbleWidth + targetBounds.right > stageBounds.right &&
|
|
|
|
(targetBounds.left - bubbleWidth > stageBounds.left)) { // Only flip if it would fit
|
2017-10-05 10:24:20 -04:00
|
|
|
bubbleState.onSpriteRight = false;
|
|
|
|
this._renderBubble(target);
|
2017-10-11 15:58:06 -04:00
|
|
|
} else if (!bubbleState.onSpriteRight && targetBounds.left - bubbleWidth < stageBounds.left &&
|
|
|
|
(bubbleWidth + targetBounds.right < stageBounds.right)) { // Only flip if it would fit
|
2017-10-05 10:24:20 -04:00
|
|
|
bubbleState.onSpriteRight = true;
|
|
|
|
this._renderBubble(target);
|
2017-10-06 15:24:29 -04:00
|
|
|
} else {
|
|
|
|
this.runtime.renderer.updateDrawableProperties(bubbleState.drawableId, {
|
|
|
|
position: [
|
2017-10-11 16:34:50 -04:00
|
|
|
bubbleState.onSpriteRight ? (
|
2018-06-05 15:58:03 -04:00
|
|
|
Math.max(
|
|
|
|
stageBounds.left, // Bubble should not extend past left edge of stage
|
|
|
|
Math.min(stageBounds.right - bubbleWidth, targetBounds.right)
|
|
|
|
)
|
2017-10-11 16:34:50 -04:00
|
|
|
) : (
|
2018-06-05 15:58:03 -04:00
|
|
|
Math.min(
|
|
|
|
stageBounds.right - bubbleWidth, // Bubble should not extend past right edge of stage
|
|
|
|
Math.max(stageBounds.left, targetBounds.left - bubbleWidth)
|
|
|
|
)
|
2017-10-11 16:34:50 -04:00
|
|
|
),
|
2018-06-05 15:58:03 -04:00
|
|
|
// Bubble should not extend past the top of the stage
|
2018-03-14 11:33:36 -04:00
|
|
|
Math.min(stageBounds.top, targetBounds.bottom + bubbleHeight)
|
2017-10-06 15:24:29 -04:00
|
|
|
]
|
|
|
|
});
|
|
|
|
this.runtime.requestRedraw();
|
2017-10-05 10:24:20 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-10-06 13:43:07 -04:00
|
|
|
/**
|
|
|
|
* Create a visible bubble for a target. If a bubble exists for the target,
|
|
|
|
* just set it to visible and update the type/text. Otherwise create a new
|
|
|
|
* bubble and update the relevant custom state.
|
|
|
|
* @param {!Target} target Target who needs a bubble.
|
2017-10-06 15:24:29 -04:00
|
|
|
* @return {undefined} Early return if text is empty string.
|
2017-10-06 13:43:07 -04:00
|
|
|
* @private
|
|
|
|
*/
|
2017-10-05 10:24:20 -04:00
|
|
|
_renderBubble (target) {
|
2018-03-14 19:07:05 -04:00
|
|
|
if (!this.runtime.renderer) return;
|
|
|
|
|
2017-10-05 10:24:20 -04:00
|
|
|
const bubbleState = this._getBubbleState(target);
|
2018-04-30 13:40:36 -04:00
|
|
|
const {type, text, onSpriteRight} = bubbleState;
|
2017-10-23 14:24:37 -04:00
|
|
|
|
2018-04-30 13:40:36 -04:00
|
|
|
// Remove the bubble if target is not visible, or text is being set to blank.
|
|
|
|
if (!target.visible || text === '') {
|
2018-03-14 19:07:05 -04:00
|
|
|
this._onTargetWillExit(target);
|
|
|
|
return;
|
2017-10-06 15:24:29 -04:00
|
|
|
}
|
2017-10-05 10:24:20 -04:00
|
|
|
|
|
|
|
if (bubbleState.skinId) {
|
2018-04-30 13:40:36 -04:00
|
|
|
this.runtime.renderer.updateTextSkin(bubbleState.skinId, type, text, onSpriteRight, [0, 0]);
|
2017-10-05 10:24:20 -04:00
|
|
|
} else {
|
2018-04-20 10:44:55 -04:00
|
|
|
target.addListener(RenderedTarget.EVENT_TARGET_VISUAL_CHANGE, this._onTargetChanged);
|
2017-10-05 10:24:20 -04:00
|
|
|
|
2017-10-05 17:03:30 -04:00
|
|
|
// TODO is there a way to figure out before rendering whether to default left or right?
|
|
|
|
const targetBounds = target.getBounds();
|
2018-06-28 16:45:07 -04:00
|
|
|
const stageSize = this.runtime.renderer.getNativeSize();
|
|
|
|
const stageBounds = {
|
|
|
|
left: -stageSize[0] / 2,
|
|
|
|
right: stageSize[0] / 2,
|
|
|
|
top: stageSize[1] / 2,
|
|
|
|
bottom: -stageSize[1] / 2
|
|
|
|
};
|
2017-10-05 17:03:30 -04:00
|
|
|
if (targetBounds.right + 170 > stageBounds.right) {
|
|
|
|
bubbleState.onSpriteRight = false;
|
|
|
|
}
|
|
|
|
|
2018-05-20 19:49:42 -04:00
|
|
|
bubbleState.drawableId = this.runtime.renderer.createDrawable(StageLayering.SPRITE_LAYER);
|
2017-10-05 10:24:20 -04:00
|
|
|
bubbleState.skinId = this.runtime.renderer.createTextSkin(type, text, bubbleState.onSpriteRight, [0, 0]);
|
|
|
|
|
|
|
|
this.runtime.renderer.updateDrawableProperties(bubbleState.drawableId, {
|
|
|
|
skinId: bubbleState.skinId
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
this._positionBubble(target);
|
|
|
|
}
|
2017-10-05 17:03:30 -04:00
|
|
|
|
2017-10-06 13:43:07 -04:00
|
|
|
/**
|
|
|
|
* The entry point for say/think blocks. Clears existing bubble if the text is empty.
|
|
|
|
* Set the bubble custom state and then call _renderBubble.
|
|
|
|
* @param {!Target} target Target that say/think blocks are being called on.
|
|
|
|
* @param {!string} type Either "say" or "think"
|
|
|
|
* @param {!string} text The text for the bubble, empty string clears the bubble.
|
|
|
|
* @private
|
|
|
|
*/
|
2017-10-05 10:24:20 -04:00
|
|
|
_updateBubble (target, type, text) {
|
2017-10-06 15:24:29 -04:00
|
|
|
const bubbleState = this._getBubbleState(target);
|
|
|
|
bubbleState.type = type;
|
|
|
|
bubbleState.text = text;
|
2018-04-26 11:29:24 -04:00
|
|
|
bubbleState.usageId = uid();
|
2017-10-06 15:24:29 -04:00
|
|
|
this._renderBubble(target);
|
2017-10-05 10:24:20 -04:00
|
|
|
}
|
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
/**
|
|
|
|
* Retrieve the block primitives implemented by this package.
|
|
|
|
* @return {object.<string, Function>} Mapping of opcode to Function.
|
|
|
|
*/
|
|
|
|
getPrimitives () {
|
|
|
|
return {
|
|
|
|
looks_say: this.say,
|
|
|
|
looks_sayforsecs: this.sayforsecs,
|
|
|
|
looks_think: this.think,
|
2017-10-05 10:24:20 -04:00
|
|
|
looks_thinkforsecs: this.thinkforsecs,
|
2017-04-17 19:42:48 -04:00
|
|
|
looks_show: this.show,
|
|
|
|
looks_hide: this.hide,
|
2018-05-01 12:58:09 -04:00
|
|
|
looks_hideallsprites: () => {}, // legacy no-op block
|
2017-04-17 19:42:48 -04:00
|
|
|
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,
|
2018-05-01 12:58:09 -04:00
|
|
|
looks_changestretchby: () => {}, // legacy no-op blocks
|
|
|
|
looks_setstretchto: () => {},
|
2017-12-28 13:14:00 -05:00
|
|
|
looks_gotofrontback: this.goToFrontBack,
|
|
|
|
looks_goforwardbackwardlayers: this.goForwardBackwardLayers,
|
2017-04-17 19:42:48 -04:00
|
|
|
looks_size: this.getSize,
|
2018-01-04 10:04:54 -05:00
|
|
|
looks_costumenumbername: this.getCostumeNumberName,
|
|
|
|
looks_backdropnumbername: this.getBackdropNumberName
|
2017-04-17 19:42:48 -04:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2017-11-15 17:53:43 -05:00
|
|
|
getMonitored () {
|
|
|
|
return {
|
2018-05-08 14:09:18 -04:00
|
|
|
looks_size: {
|
|
|
|
isSpriteSpecific: true,
|
|
|
|
getId: targetId => `${targetId}_size`
|
|
|
|
},
|
|
|
|
looks_costumenumbername: {
|
|
|
|
isSpriteSpecific: true,
|
|
|
|
getId: targetId => `${targetId}_costumenumbername`
|
|
|
|
},
|
|
|
|
looks_backdropnumbername: {
|
|
|
|
getId: () => 'backdropnumbername'
|
|
|
|
}
|
2017-11-15 17:53:43 -05:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
say (args, util) {
|
2017-10-05 10:24:20 -04:00
|
|
|
// @TODO in 2.0 calling say/think resets the right/left bias of the bubble
|
2018-01-31 09:23:08 -05:00
|
|
|
let message = args.MESSAGE;
|
|
|
|
if (typeof message === 'number') {
|
2018-03-05 20:15:49 -05:00
|
|
|
message = parseFloat(message.toFixed(2));
|
2018-01-31 09:23:08 -05:00
|
|
|
}
|
2018-02-07 18:49:18 -05:00
|
|
|
message = String(message);
|
|
|
|
this.runtime.emit('SAY', util.target, 'say', message);
|
2017-04-17 19:42:48 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
sayforsecs (args, util) {
|
2017-10-05 10:24:20 -04:00
|
|
|
this.say(args, util);
|
2018-04-26 11:29:24 -04:00
|
|
|
const target = util.target;
|
|
|
|
const usageId = this._getBubbleState(target).usageId;
|
2017-04-17 19:42:48 -04:00
|
|
|
return new Promise(resolve => {
|
2017-10-23 11:19:39 -04:00
|
|
|
this._bubbleTimeout = setTimeout(() => {
|
|
|
|
this._bubbleTimeout = null;
|
2018-04-26 11:29:24 -04:00
|
|
|
// Clear say bubble if it hasn't been changed and proceed.
|
|
|
|
if (this._getBubbleState(target).usageId === usageId) {
|
2018-05-15 14:37:06 -04:00
|
|
|
this._updateBubble(target, 'say', '');
|
2018-04-26 11:29:24 -04:00
|
|
|
}
|
2017-04-17 19:42:48 -04:00
|
|
|
resolve();
|
|
|
|
}, 1000 * args.SECS);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
think (args, util) {
|
2017-10-30 15:15:38 -04:00
|
|
|
this._updateBubble(util.target, 'think', String(args.MESSAGE));
|
2017-04-17 19:42:48 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
thinkforsecs (args, util) {
|
2017-10-05 10:24:20 -04:00
|
|
|
this.think(args, util);
|
2018-04-26 11:29:24 -04:00
|
|
|
const target = util.target;
|
|
|
|
const usageId = this._getBubbleState(target).usageId;
|
2017-04-17 19:42:48 -04:00
|
|
|
return new Promise(resolve => {
|
2017-10-23 11:19:39 -04:00
|
|
|
this._bubbleTimeout = setTimeout(() => {
|
|
|
|
this._bubbleTimeout = null;
|
2018-04-26 11:29:24 -04:00
|
|
|
// Clear think bubble if it hasn't been changed and proceed.
|
|
|
|
if (this._getBubbleState(target).usageId === usageId) {
|
2018-05-15 14:37:06 -04:00
|
|
|
this._updateBubble(target, 'think', '');
|
2018-04-26 11:29:24 -04:00
|
|
|
}
|
2017-04-17 19:42:48 -04:00
|
|
|
resolve();
|
|
|
|
}, 1000 * args.SECS);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
show (args, util) {
|
|
|
|
util.target.setVisible(true);
|
2017-10-06 15:24:29 -04:00
|
|
|
this._renderBubble(util.target);
|
2017-04-17 19:42:48 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
hide (args, util) {
|
|
|
|
util.target.setVisible(false);
|
2017-10-23 13:03:10 -04:00
|
|
|
this._renderBubble(util.target);
|
2017-04-17 19:42:48 -04:00
|
|
|
}
|
|
|
|
|
2016-06-30 00:11:47 -04:00
|
|
|
/**
|
2018-08-22 13:39:15 -04:00
|
|
|
* Utility function to set the costume of a target.
|
2017-04-17 19:42:48 -04:00
|
|
|
* Matches the behavior of Scratch 2.0 for different types of arguments.
|
2018-08-22 13:39:15 -04:00
|
|
|
* @param {!Target} target Target to set costume to.
|
2017-04-17 19:42:48 -04:00
|
|
|
* @param {Any} requestedCostume Costume requested, e.g., 0, 'name', etc.
|
|
|
|
* @param {boolean=} optZeroIndex Set to zero-index the requestedCostume.
|
|
|
|
* @return {Array.<!Thread>} Any threads started by this switch.
|
2016-06-30 00:11:47 -04:00
|
|
|
*/
|
2018-08-22 13:39:15 -04:00
|
|
|
_setCostume (target, requestedCostume, optZeroIndex) {
|
2017-04-17 19:42:48 -04:00
|
|
|
if (typeof requestedCostume === 'number') {
|
2018-08-22 13:39:15 -04:00
|
|
|
target.setCostume(optZeroIndex ? requestedCostume : requestedCostume - 1);
|
2016-09-08 09:40:27 -04:00
|
|
|
} else {
|
2018-08-22 13:39:15 -04:00
|
|
|
const costumeIndex = target.getCostumeIndexByName(requestedCostume.toString());
|
|
|
|
|
|
|
|
if (costumeIndex !== -1) {
|
2017-04-17 19:42:48 -04:00
|
|
|
target.setCostume(costumeIndex);
|
2018-08-22 13:39:15 -04:00
|
|
|
} else if (requestedCostume === 'next costume') {
|
2017-04-17 19:42:48 -04:00
|
|
|
target.setCostume(target.currentCostume + 1);
|
2018-08-22 13:39:15 -04:00
|
|
|
} else if (requestedCostume === 'previous costume') {
|
|
|
|
target.setCostume(target.currentCostume - 1);
|
|
|
|
} else if (!isNaN(requestedCostume) &&
|
|
|
|
(typeof requestedCostume !== 'string' || /\d/g.test(requestedCostume))) {
|
|
|
|
target.setCostume(optZeroIndex ? Number(requestedCostume) : Number(requestedCostume) - 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Per 2.0, 'switch costume' can't start threads even in the Stage.
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Utility function to set the backdrop of a target.
|
|
|
|
* Matches the behavior of Scratch 2.0 for different types of arguments.
|
|
|
|
* @param {!Target} stage Target to set backdrop to.
|
|
|
|
* @param {Any} requestedBackdrop Backdrop requested, e.g., 0, 'name', etc.
|
|
|
|
* @param {boolean=} optZeroIndex Set to zero-index the requestedBackdrop.
|
|
|
|
* @return {Array.<!Thread>} Any threads started by this switch.
|
|
|
|
*/
|
|
|
|
_setBackdrop (stage, requestedBackdrop, optZeroIndex) {
|
|
|
|
if (typeof requestedBackdrop === 'number') {
|
|
|
|
stage.setCostume(optZeroIndex ? requestedBackdrop : requestedBackdrop - 1);
|
|
|
|
} else if (requestedBackdrop === 'next backdrop') {
|
|
|
|
stage.setCostume(stage.currentCostume + 1);
|
|
|
|
} else if (requestedBackdrop === 'previous backdrop') {
|
|
|
|
stage.setCostume(stage.currentCostume - 1);
|
|
|
|
} else {
|
|
|
|
const costumeIndex = stage.getCostumeIndexByName(requestedBackdrop.toString());
|
|
|
|
|
|
|
|
if (costumeIndex !== -1) {
|
|
|
|
stage.setCostume(costumeIndex);
|
|
|
|
} else if (requestedBackdrop === 'random backdrop') {
|
|
|
|
const numCostumes = stage.getCostumes().length;
|
2018-05-07 20:27:51 -04:00
|
|
|
if (numCostumes > 1) {
|
2018-05-08 20:13:04 -04:00
|
|
|
let selectedIndex = Math.floor(Math.random() * (numCostumes - 1));
|
2018-08-22 13:39:15 -04:00
|
|
|
if (selectedIndex === stage.currentCostume) selectedIndex += 1;
|
|
|
|
stage.setCostume(selectedIndex);
|
2017-04-17 19:42:48 -04:00
|
|
|
}
|
2018-08-22 13:39:15 -04:00
|
|
|
} else if (!isNaN(requestedBackdrop) &&
|
|
|
|
(typeof requestedBackdrop !== 'string' || /\d/g.test(requestedBackdrop))) {
|
|
|
|
stage.setCostume(optZeroIndex ? Number(requestedBackdrop) : Number(requestedBackdrop) - 1);
|
2016-09-08 09:40:27 -04:00
|
|
|
}
|
|
|
|
}
|
2018-08-22 13:39:15 -04:00
|
|
|
|
|
|
|
const newName = stage.getCostumes()[stage.currentCostume].name;
|
|
|
|
return this.runtime.startHats('event_whenbackdropswitchesto', {
|
|
|
|
BACKDROP: newName
|
|
|
|
});
|
2016-09-08 09:40:27 -04:00
|
|
|
}
|
2017-04-17 19:42:48 -04:00
|
|
|
|
|
|
|
switchCostume (args, util) {
|
2018-08-22 13:39:15 -04:00
|
|
|
this._setCostume(util.target, args.COSTUME);
|
2016-09-08 09:40:27 -04:00
|
|
|
}
|
2017-04-17 19:42:48 -04:00
|
|
|
|
|
|
|
nextCostume (args, util) {
|
2018-08-22 13:39:15 -04:00
|
|
|
this._setCostume(
|
2017-04-17 19:42:48 -04:00
|
|
|
util.target, util.target.currentCostume + 1, true
|
2016-09-08 09:40:27 -04:00
|
|
|
);
|
2017-04-17 19:42:48 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
switchBackdrop (args) {
|
2018-08-22 13:39:15 -04:00
|
|
|
this._setBackdrop(this.runtime.getTargetForStage(), args.BACKDROP);
|
2017-04-17 19:42:48 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
switchBackdropAndWait (args, util) {
|
|
|
|
// Have we run before, starting threads?
|
|
|
|
if (!util.stackFrame.startedThreads) {
|
|
|
|
// No - switch the backdrop.
|
|
|
|
util.stackFrame.startedThreads = (
|
2018-08-22 13:39:15 -04:00
|
|
|
this._setBackdrop(
|
2017-04-17 19:42:48 -04:00
|
|
|
this.runtime.getTargetForStage(),
|
|
|
|
args.BACKDROP
|
|
|
|
)
|
|
|
|
);
|
|
|
|
if (util.stackFrame.startedThreads.length === 0) {
|
|
|
|
// Nothing was started.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// We've run before; check if the wait is still going on.
|
|
|
|
const instance = this;
|
|
|
|
const waiting = util.stackFrame.startedThreads.some(thread => instance.runtime.isActiveThread(thread));
|
|
|
|
if (waiting) {
|
2018-06-06 09:59:38 -04:00
|
|
|
// If all threads are waiting for the next tick or later yield
|
|
|
|
// for a tick as well. Otherwise yield until the next loop of
|
|
|
|
// the threads.
|
|
|
|
if (
|
|
|
|
util.stackFrame.startedThreads
|
|
|
|
.every(thread => instance.runtime.isWaitingThread(thread))
|
|
|
|
) {
|
|
|
|
util.yieldTick();
|
|
|
|
} else {
|
|
|
|
util.yield();
|
|
|
|
}
|
2016-09-08 09:40:27 -04:00
|
|
|
}
|
|
|
|
}
|
2017-04-17 19:42:48 -04:00
|
|
|
|
|
|
|
nextBackdrop () {
|
|
|
|
const stage = this.runtime.getTargetForStage();
|
2018-08-22 13:39:15 -04:00
|
|
|
this._setBackdrop(
|
2017-04-17 19:42:48 -04:00
|
|
|
stage, stage.currentCostume + 1, true
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
changeEffect (args, util) {
|
|
|
|
const effect = Cast.toString(args.EFFECT).toLowerCase();
|
|
|
|
const change = Cast.toNumber(args.CHANGE);
|
|
|
|
if (!util.target.effects.hasOwnProperty(effect)) return;
|
|
|
|
const newValue = change + util.target.effects[effect];
|
|
|
|
util.target.setEffect(effect, newValue);
|
|
|
|
}
|
|
|
|
|
|
|
|
setEffect (args, util) {
|
|
|
|
const effect = Cast.toString(args.EFFECT).toLowerCase();
|
|
|
|
const value = Cast.toNumber(args.VALUE);
|
|
|
|
util.target.setEffect(effect, value);
|
|
|
|
}
|
|
|
|
|
|
|
|
clearEffects (args, util) {
|
|
|
|
util.target.clearEffects();
|
|
|
|
}
|
|
|
|
|
|
|
|
changeSize (args, util) {
|
|
|
|
const change = Cast.toNumber(args.CHANGE);
|
|
|
|
util.target.setSize(util.target.size + change);
|
|
|
|
}
|
|
|
|
|
|
|
|
setSize (args, util) {
|
|
|
|
const size = Cast.toNumber(args.SIZE);
|
|
|
|
util.target.setSize(size);
|
|
|
|
}
|
|
|
|
|
2017-12-28 13:14:00 -05:00
|
|
|
goToFrontBack (args, util) {
|
2017-12-02 08:33:32 -05:00
|
|
|
if (!util.target.isStage) {
|
2017-12-28 13:14:00 -05:00
|
|
|
if (args.FRONT_BACK === 'front') {
|
|
|
|
util.target.goToFront();
|
|
|
|
} else {
|
|
|
|
util.target.goToBack();
|
|
|
|
}
|
2017-12-02 08:33:32 -05:00
|
|
|
}
|
2017-04-17 19:42:48 -04:00
|
|
|
}
|
|
|
|
|
2017-12-28 13:14:00 -05:00
|
|
|
goForwardBackwardLayers (args, util) {
|
|
|
|
if (!util.target.isStage) {
|
|
|
|
if (args.FORWARD_BACKWARD === 'forward') {
|
|
|
|
util.target.goForwardLayers(Cast.toNumber(args.NUM));
|
|
|
|
} else {
|
|
|
|
util.target.goBackwardLayers(Cast.toNumber(args.NUM));
|
|
|
|
}
|
|
|
|
}
|
2017-04-17 19:42:48 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
getSize (args, util) {
|
|
|
|
return Math.round(util.target.size);
|
|
|
|
}
|
|
|
|
|
2018-01-04 10:04:54 -05:00
|
|
|
getBackdropNumberName (args) {
|
2017-04-17 19:42:48 -04:00
|
|
|
const stage = this.runtime.getTargetForStage();
|
2018-01-04 10:04:54 -05:00
|
|
|
if (args.NUMBER_NAME === 'number') {
|
|
|
|
return stage.currentCostume + 1;
|
|
|
|
}
|
|
|
|
// Else return name
|
2018-02-21 19:59:35 -05:00
|
|
|
return stage.getCostumes()[stage.currentCostume].name;
|
2017-04-17 19:42:48 -04:00
|
|
|
}
|
|
|
|
|
2018-01-04 10:04:54 -05:00
|
|
|
getCostumeNumberName (args, util) {
|
|
|
|
if (args.NUMBER_NAME === 'number') {
|
|
|
|
return util.target.currentCostume + 1;
|
|
|
|
}
|
|
|
|
// Else return name
|
2018-02-21 19:59:35 -05:00
|
|
|
return util.target.getCostumes()[util.target.currentCostume].name;
|
2017-04-17 19:42:48 -04:00
|
|
|
}
|
|
|
|
}
|
2016-09-08 09:40:27 -04:00
|
|
|
|
2016-06-30 00:11:47 -04:00
|
|
|
module.exports = Scratch3LooksBlocks;
|