soiz1's picture
Upload 811 files
30c32c8 verified
raw
history blame
37.6 kB
const Cast = require('../util/cast');
const Color = require('../util/color');
const Clone = require('../util/clone');
const uid = require('../util/uid');
const StageLayering = require('../engine/stage-layering');
const getMonitorIdForBlockWithArgs = require('../util/get-monitor-id');
const MathUtil = require('../util/math-util');
/**
* @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"
* @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.
*/
class Scratch3LooksBlocks {
constructor (runtime) {
/**
* The runtime instantiating this block package.
* @type {Runtime}
*/
this.runtime = runtime;
this._onTargetChanged = this._onTargetChanged.bind(this);
this._onResetBubbles = this._onResetBubbles.bind(this);
this._onTargetWillExit = this._onTargetWillExit.bind(this);
this._updateBubble = this._updateBubble.bind(this);
this.SAY_BUBBLE_LIMITdefault = 330;
this.SAY_BUBBLE_LIMIT = this.SAY_BUBBLE_LIMITdefault;
this.defaultBubble = {
MAX_LINE_WIDTH: 170, // Maximum width, in Scratch pixels, of a single line of text
MIN_WIDTH: 50, // Minimum width, in Scratch pixels, of a text bubble
STROKE_WIDTH: 4, // Thickness of the stroke around the bubble.
// Only half's visible because it's drawn under the fill
PADDING: 10, // Padding around the text area
CORNER_RADIUS: 16, // Radius of the rounded corners
TAIL_HEIGHT: 12, // Height of the speech bubble's "tail". Probably should be a constant.
FONT: 'Helvetica', // Font to render the text with
FONT_SIZE: 14, // Font size, in Scratch pixels
FONT_HEIGHT_RATIO: 0.9, // Height, in Scratch pixels, of the text, as a proportion of the font's size
LINE_HEIGHT: 16, // Spacing between each line of text
COLORS: {
BUBBLE_FILL: 'white',
BUBBLE_STROKE: 'rgba(0, 0, 0, 0.15)',
TEXT_FILL: '#575E75'
}
};
// Reset all bubbles on start/stop
this.runtime.on('PROJECT_STOP_ALL', this._onResetBubbles);
this.runtime.on('targetWasRemoved', this._onTargetWillExit);
// Enable other blocks to use bubbles like ask/answer
this.runtime.on(Scratch3LooksBlocks.SAY_OR_THINK, this._updateBubble);
}
/**
* 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: '',
type: 'say',
usageId: null,
// @todo make this read from renderer
props: this.defaultBubble
};
}
/**
* The key to load & store a target's bubble-related state.
* @type {string}
*/
static get STATE_KEY () {
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 ghost effect
* @const {object}
*/
static get EFFECT_GHOST_LIMIT (){
return {min: 0, max: 100};
}
/**
* Limit for brightness effect
* @const {object}
*/
static get EFFECT_BRIGHTNESS_LIMIT (){
return {min: -100, max: 100};
}
/**
* @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;
}
/**
* resets the text bubble of a sprite
* @param {Target} target the target to reset
*/
_resetBubbles (target) {
const state = this._getBubbleState(target);
this.SAY_BUBBLE_LIMIT = this.SAY_BUBBLE_LIMITdefault;
state.props = this.defaultBubble;
}
/**
* set any property of the text bubble of any given target
* @param {Target} target the target to modify
* @param {array} props the property names to change
* @param {array} value the values the set the properties to
*/
_setBubbleProperty (target, props, value) {
const object = this._getBubbleState(target);
if (!object.props) object.props = this.defaultBubble;
props.forEach((prop, index) => {
if (prop.startsWith('COLORS')) {
object.props.COLORS[prop.split('.')[1]] = value[index];
} else {
object.props[prop] = value[index];
}
});
target.setCustomState(Scratch3LooksBlocks.STATE_KEY, object);
}
/**
* Handle a target which has moved.
* @param {RenderedTarget} target - the target which has moved.
* @private
*/
_onTargetChanged (target) {
const bubbleState = this._getBubbleState(target);
if (bubbleState.drawableId) {
this._positionBubble(target);
}
}
/**
* Handle a target which is exiting.
* @param {RenderedTarget} target - the target.
* @private
*/
_onTargetWillExit (target) {
const bubbleState = this._getBubbleState(target);
if (bubbleState.drawableId && bubbleState.skinId) {
this.runtime.renderer.destroyDrawable(bubbleState.drawableId, StageLayering.SPRITE_LAYER);
this.runtime.renderer.destroySkin(bubbleState.skinId);
bubbleState.drawableId = null;
bubbleState.skinId = null;
this.runtime.requestRedraw();
}
target.onTargetVisualChange = null;
}
/**
* Handle project start/stop by clearing all visible bubbles.
* @private
*/
_onResetBubbles () {
for (let n = 0; n < this.runtime.targets.length; n++) {
const bubbleState = this._getBubbleState(this.runtime.targets[n]);
bubbleState.text = '';
this._onTargetWillExit(this.runtime.targets[n]);
}
clearTimeout(this._bubbleTimeout);
}
/**
* Position the bubble of a target. If it doesn't fit on the specified side, flip and rerender.
* @param {!Target} target Target whose bubble needs positioning.
* @private
*/
_positionBubble (target) {
if (!target.visible) return;
const bubbleState = this._getBubbleState(target);
const [bubbleWidth, bubbleHeight] = this.runtime.renderer.getCurrentSkinSize(bubbleState.drawableId);
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
};
}
const stageSize = this.runtime.renderer.getNativeSize();
const stageBounds = {
left: -stageSize[0] / 2,
right: stageSize[0] / 2,
top: stageSize[1] / 2,
bottom: -stageSize[1] / 2
};
if (bubbleState.onSpriteRight && bubbleWidth + targetBounds.right > stageBounds.right &&
(targetBounds.left - bubbleWidth > stageBounds.left)) { // Only flip if it would fit
bubbleState.onSpriteRight = false;
this._renderBubble(target);
} else if (!bubbleState.onSpriteRight && targetBounds.left - bubbleWidth < stageBounds.left &&
(bubbleWidth + targetBounds.right < stageBounds.right)) { // Only flip if it would fit
bubbleState.onSpriteRight = true;
this._renderBubble(target);
} else {
this.runtime.renderer.updateDrawablePosition(bubbleState.drawableId, [
bubbleState.onSpriteRight ? (
Math.max(
stageBounds.left, // Bubble should not extend past left edge of stage
Math.min(stageBounds.right - bubbleWidth, targetBounds.right)
)
) : (
Math.min(
stageBounds.right - bubbleWidth, // Bubble should not extend past right edge of stage
Math.max(stageBounds.left, targetBounds.left - bubbleWidth)
)
),
// Bubble should not extend past the top of the stage
Math.min(stageBounds.top, targetBounds.bottom + bubbleHeight)
]);
this.runtime.requestRedraw();
}
}
/**
* 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.
* @return {undefined} Early return if text is empty string.
* @private
*/
_renderBubble (target) { // used by compiler
if (!this.runtime.renderer) return;
const bubbleState = this._getBubbleState(target);
const {type, text, onSpriteRight} = bubbleState;
// Remove the bubble if target is not visible, or text is being set to blank.
if (!target.visible || text === '') {
this._onTargetWillExit(target);
return;
}
if (bubbleState.skinId) {
this.runtime.renderer.updateTextSkin(bubbleState.skinId, type, text, onSpriteRight, bubbleState.props);
} else {
target.onTargetVisualChange = this._onTargetChanged;
bubbleState.drawableId = this.runtime.renderer.createDrawable(StageLayering.SPRITE_LAYER);
bubbleState.skinId = this.runtime.renderer.createTextSkin(type, text,
bubbleState.onSpriteRight, bubbleState.props);
this.runtime.renderer.updateDrawableSkinId(bubbleState.drawableId, bubbleState.skinId);
}
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).slice(0, this.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.
* @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
*/
_updateBubble (target, type, text) {
const bubbleState = this._getBubbleState(target);
bubbleState.type = type;
bubbleState.text = this._formatBubbleText(text);
bubbleState.usageId = uid();
this._renderBubble(target);
}
_percentToRatio (percent) {
return percent / 100;
}
_doesFontSuport (size, font) {
const check = size + 'px ' + font;
return document.fonts.check(check);
}
/**
* 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,
looks_thinkforsecs: this.thinkforsecs,
looks_setFont: this.setFont,
looks_setColor: this.setColor,
looks_setShape: this.setShape,
looks_show: this.show,
looks_hide: this.hide,
looks_getSpriteVisible: this.getSpriteVisible,
looks_getOtherSpriteVisible: this.getOtherSpriteVisible,
looks_hideallsprites: () => {}, // legacy no-op block
looks_switchcostumeto: this.switchCostume,
looks_switchbackdropto: this.switchBackdrop,
looks_switchbackdroptoandwait: this.switchBackdropAndWait,
looks_nextcostume: this.nextCostume,
looks_nextbackdrop: this.nextBackdrop,
looks_previouscostume: this.previousCostume,
looks_previousbackdrop: this.previousBackdrop,
looks_changeeffectby: this.changeEffect,
looks_seteffectto: this.setEffect,
looks_cleargraphiceffects: this.clearEffects,
looks_getEffectValue: this.getEffectValue,
looks_changesizeby: this.changeSize,
looks_setsizeto: this.setSize,
looks_changestretchby: () => {},
looks_setstretchto: this.stretchSet,
looks_gotofrontback: this.goToFrontBack,
looks_goforwardbackwardlayers: this.goForwardBackwardLayers,
looks_goTargetLayer: this.goTargetLayer,
looks_layersSetLayer: this.setSpriteLayer,
looks_layersGetLayer: this.getSpriteLayer,
looks_size: this.getSize,
looks_costumenumbername: this.getCostumeNumberName,
looks_backdropnumbername: this.getBackdropNumberName,
looks_setStretch: this.stretchSet,
looks_changeStretch: this.changeStretch,
looks_stretchGetX: this.getStretchX,
looks_stretchGetY: this.getStretchY,
looks_sayWidth: this.getBubbleWidth,
looks_sayHeight: this.getBubbleHeight,
looks_changeVisibilityOfSprite: this.showOrHideSprite,
looks_changeVisibilityOfSpriteShow: this.showSprite,
looks_changeVisibilityOfSpriteHide: this.hideSprite,
looks_stoptalking: this.stopTalking,
looks_getinputofcostume: this.getCostumeValue,
looks_tintColor: this.getTintColor,
looks_setTintColor: this.setTintColor
};
}
getSpriteLayer (_, util) {
const target = util.target;
return target.getLayerOrder();
}
setSpriteLayer (args, util) {
const target = util.target;
const targetLayer = Cast.toNumber(args.NUM);
const currentLayer = target.getLayerOrder();
// i dont know how to set layer lol
target.goForwardLayers(targetLayer - currentLayer);
}
_getBubbleSize (target) {
const bubbleState = this._getBubbleState(target);
return this.runtime.renderer.getSkinSize(bubbleState.skinId);
}
getBubbleWidth (_, util) {
const target = util.target;
let val = 0;
try {
val = this._getBubbleSize(target)[0];
} catch {
val = 0;
}
return val;
}
getBubbleHeight (_, util) {
const target = util.target;
let val = 0;
try {
val = this._getBubbleSize(target)[1];
} catch {
val = 0;
}
return val;
}
getStretchY (args, util) {
return util.target._getRenderedDirectionAndScale().stretch[1];
}
getStretchX (args, util) {
return util.target._getRenderedDirectionAndScale().stretch[0];
}
stretchSet (args, util) {
util.target.setStretch(args.X, args.Y);
}
changeStretch(args, util) {
let [x, y] = util.target._getRenderedDirectionAndScale().stretch;
let new_x = x + Cast.toNumber(args.X)
let new_y = y + Cast.toNumber(args.Y)
util.target.setStretch(new_x, new_y)
}
setFont (args, util) {
this._setBubbleProperty(
util.target,
['FONT', 'FONT_SIZE'],
[args.font, args.size]
);
}
setColor (args, util) {
const numColor = Number(args.color);
if (!isNaN(numColor)) {
args.color = Color.decimalToHex(numColor);
}
this._setBubbleProperty(
util.target,
['COLORS.' + args.prop],
[args.color]
);
}
setShape (args, util) {
if (args.prop === 'texlim') {
this.SAY_BUBBLE_LIMIT = Math.max(args.color, 1);
return;
}
this._setBubbleProperty(
util.target,
[args.prop],
[args.color]
);
}
getMonitored () {
return {
looks_size: {
isSpriteSpecific: true,
getId: targetId => `${targetId}_size`
},
looks_stretchGetX: {
isSpriteSpecific: true,
getId: targetId => `${targetId}_stretchX`
},
looks_stretchGetY: {
isSpriteSpecific: true,
getId: targetId => `${targetId}_stretchY`
},
looks_sayWidth: {
isSpriteSpecific: true,
getId: targetId => `${targetId}_sayWidth`
},
looks_sayHeight: {
isSpriteSpecific: true,
getId: targetId => `${targetId}_sayHeight`
},
looks_getEffectValue: {
isSpriteSpecific: true,
getId: (targetId, fields) => getMonitorIdForBlockWithArgs(`${targetId}_getEffectValue`, fields)
},
looks_tintColor: {
isSpriteSpecific: true,
getId: targetId => `${targetId}_tintColor`
},
looks_getSpriteVisible: {
isSpriteSpecific: true,
getId: targetId => `${targetId}_getSpriteVisible`
},
looks_layersGetLayer: {
isSpriteSpecific: true,
getId: targetId => `${targetId}_layersGetLayer`
},
looks_costumenumbername: {
isSpriteSpecific: true,
getId: (targetId, fields) => getMonitorIdForBlockWithArgs(`${targetId}_costumenumbername`, fields)
},
looks_backdropnumbername: {
getId: (_, fields) => getMonitorIdForBlockWithArgs('backdropnumbername', fields)
}
};
}
say (args, util) {
// @TODO in 2.0 calling say/think resets the right/left bias of the bubble
const message = args.MESSAGE;
this._say(message, util.target);
}
_say (message, target) { // used by compiler
this.runtime.emit(Scratch3LooksBlocks.SAY_OR_THINK, target, 'say', message);
}
stopTalking (_, util) {
this.say({ MESSAGE: '' }, util);
}
sayforsecs (args, util) {
this.say(args, util);
const target = util.target;
const usageId = this._getBubbleState(target).usageId;
return new Promise(resolve => {
this._bubbleTimeout = setTimeout(() => {
this._bubbleTimeout = null;
// Clear say bubble if it hasn't been changed and proceed.
if (this._getBubbleState(target).usageId === usageId) {
this._updateBubble(target, 'say', '');
}
resolve();
}, 1000 * args.SECS);
});
}
think (args, util) {
this.runtime.emit(Scratch3LooksBlocks.SAY_OR_THINK, util.target, 'think', args.MESSAGE);
}
thinkforsecs (args, util) {
this.think(args, util);
const target = util.target;
const usageId = this._getBubbleState(target).usageId;
return new Promise(resolve => {
this._bubbleTimeout = setTimeout(() => {
this._bubbleTimeout = null;
// Clear think bubble if it hasn't been changed and proceed.
if (this._getBubbleState(target).usageId === usageId) {
this._updateBubble(target, 'think', '');
}
resolve();
}, 1000 * args.SECS);
});
}
show (args, util) {
util.target.setVisible(true);
this._renderBubble(util.target);
}
hide (args, util) {
util.target.setVisible(false);
this._renderBubble(util.target);
}
showOrHideSprite (args, util) {
const option = args.VISIBLE_OPTION;
const visibleOption = Cast.toString(args.VISIBLE_TYPE).toLowerCase();
// Set target
let target;
if (option === '_myself_') {
target = util.target;
} else if (option === '_stage_') {
target = this.runtime.getTargetForStage();
} else {
target = this.runtime.getSpriteTargetByName(option);
}
if (!target) return;
target.setVisible(visibleOption === 'show');
this._renderBubble(target);
}
showSprite (args, util) {
this.showOrHideSprite({ VISIBLE_OPTION: args.VISIBLE_OPTION, VISIBLE_TYPE: "show" }, util);
}
hideSprite (args, util) {
this.showOrHideSprite({ VISIBLE_OPTION: args.VISIBLE_OPTION, VISIBLE_TYPE: "hide" }, util);
}
getSpriteVisible (args, util) {
return util.target.visible;
}
getOtherSpriteVisible (args, util) {
const option = args.VISIBLE_OPTION;
// Set target
let target;
if (option === '_myself_') {
target = util.target;
} else if (option === '_stage_') {
target = this.runtime.getTargetForStage();
} else {
target = this.runtime.getSpriteTargetByName(option);
}
if (!target) return;
return target.visible;
}
getEffectValue (args, util) {
const effect = Cast.toString(args.EFFECT).toLowerCase();
const effects = util.target.effects;
if (!effects.hasOwnProperty(effect)) return 0;
const value = Cast.toNumber(effects[effect]);
return value;
}
getTintColor (_, util) {
const effects = util.target.effects;
if (typeof effects.tintColor !== 'number') return '#ffffff';
return Color.decimalToHex(effects.tintColor - 1);
}
setTintColor (args, util) { // used by compiler
const rgb = Cast.toRgbColorObject(args.color);
const decimal = Color.rgbToDecimal(rgb);
util.target.setEffect("tintColor", decimal + 1);
}
/**
* Utility function to set the costume of a target.
* Matches the behavior of Scratch 2.0 for different types of arguments.
* @param {!Target} target Target to set costume to.
* @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.
*/
_setCostume (target, requestedCostume, optZeroIndex) { // used by compiler
if (typeof requestedCostume === 'number') {
// Numbers should be treated as costume indices, always
target.setCostume(optZeroIndex ? requestedCostume : requestedCostume - 1);
} else {
// Strings should be treated as costume names, where possible
const costumeIndex = target.getCostumeIndexByName(requestedCostume.toString());
if (costumeIndex !== -1) {
target.setCostume(costumeIndex);
} else if (requestedCostume === 'next costume') {
target.setCostume(target.currentCostume + 1);
} else if (requestedCostume === 'previous costume') {
target.setCostume(target.currentCostume - 1);
// Try to cast the string to a number (and treat it as a costume index)
// Pure whitespace should not be treated as a number
// Note: isNaN will cast the string to a number before checking if it's NaN
} else if (requestedCostume === 'random costume') {
let randomIndex = MathUtil.inclusiveRandIntWithout(
0,
target.sprite.costumes_.length - 1,
target.currentCostume
)
if (randomIndex >= target.sprite.costumes_.length) {
randomIndex = 0;
// This really only accounts for if there's only 1
// costume.
}
target.setCostume(randomIndex);
} else if (!(isNaN(requestedCostume) || Cast.isWhiteSpace(requestedCostume))) {
target.setCostume(optZeroIndex ? Number(requestedCostume) : Number(requestedCostume) - 1);
}
}
// Per 2.0, 'switch costume' can't start threads even in the Stage.
return [];
}
costumeValueToDefaultNone (value) {
switch (value) {
case 'width':
case 'height':
case 'rotation center x':
case 'rotation center y':
return 0;
default:
return '';
}
}
getCostumeValue (args, util) {
let costumeIndex = 0;
const target = util.target
const requestedCostume = args.COSTUME;
const requestedValue = Cast.toString(args.INPUT);
if (typeof requestedCostume === 'number') {
// Numbers should be treated as costume indices, always
costumeIndex = (requestedCostume === 0) ? 0 : requestedCostume - 1;
} else {
let noun = target.isStage ? "backdrop" : "costume";
switch (Cast.toString(requestedCostume)) {
case "next " + noun:
costumeIndex = target.currentCostume + 1;
if (costumeIndex >= target.sprite.costumes_.length) {
costumeIndex = 0
// loop around to front
}
break;
case "previous " + noun:
costumeIndex = target.currentCostume - 1;
if (costumeIndex < 0) {
costumeIndex = target.sprite.costumes_.length - 1;
// Loop around to back
}
break;
case "random " + noun:
costumeIndex = MathUtil.inclusiveRandIntWithout(
0,
target.sprite.costumes_.length - 1,
target.currentCostume
)
if (costumeIndex >= target.sprite.costumes_.length) {
costumeIndex = 0;
// This really only accounts for if there's only 1
// costume.
}
break;
default:
costumeIndex = target.getCostumeIndexByName(Cast.toString(requestedCostume));
}
}
if (costumeIndex < 0) return this.costumeValueToDefaultNone(requestedValue);
if (!target.sprite) return this.costumeValueToDefaultNone(requestedValue);
if (!target.sprite.costumes_) return this.costumeValueToDefaultNone(requestedValue);
const costume = target.sprite.costumes_[costumeIndex];
if (!costume) return this.costumeValueToDefaultNone(requestedValue);
switch (requestedValue) {
case 'width':
return costume.size[0];
case 'height':
return costume.size[1];
case 'rotation center x':
return costume.rotationCenterX;
case 'rotation center y':
return costume.rotationCenterY;
case 'drawing mode':
return ((costume.dataFormat === "svg") ? "Vector" : "Bitmap");
default:
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) { // used by compiler
if (typeof requestedBackdrop === 'number') {
// Numbers should be treated as backdrop indices, always
stage.setCostume(optZeroIndex ? requestedBackdrop : requestedBackdrop - 1);
} else {
// Strings should be treated as backdrop names where possible
const costumeIndex = stage.getCostumeIndexByName(requestedBackdrop.toString());
if (costumeIndex !== -1) {
stage.setCostume(costumeIndex);
} else if (requestedBackdrop === 'next backdrop') {
stage.setCostume(stage.currentCostume + 1);
} else if (requestedBackdrop === 'previous backdrop') {
stage.setCostume(stage.currentCostume - 1);
} else if (requestedBackdrop === 'random backdrop') {
const numCostumes = stage.getCostumes().length;
if (numCostumes > 1) {
// Don't pick the current backdrop, so that the block
// will always have an observable effect.
const lowerBound = 0;
const upperBound = numCostumes - 1;
const costumeToExclude = stage.currentCostume;
const nextCostume = MathUtil.inclusiveRandIntWithout(lowerBound, upperBound, costumeToExclude);
stage.setCostume(nextCostume);
}
// Try to cast the string to a number (and treat it as a costume index)
// Pure whitespace should not be treated as a number
// Note: isNaN will cast the string to a number before checking if it's NaN
} else if (!(isNaN(requestedBackdrop) || Cast.isWhiteSpace(requestedBackdrop))) {
stage.setCostume(optZeroIndex ? Number(requestedBackdrop) : Number(requestedBackdrop) - 1);
}
}
const newName = stage.getCostumes()[stage.currentCostume].name;
return this.runtime.startHats('event_whenbackdropswitchesto', {
BACKDROP: newName
});
}
switchCostume (args, util) {
this._setCostume(util.target, args.COSTUME); // used by compiler
}
nextCostume (args, util) {
this._setCostume(
util.target, util.target.currentCostume + 1, true
);
}
previousCostume (args, util) {
this._setCostume(
util.target, util.target.currentCostume - 1, true
);
}
switchBackdrop (args) {
this._setBackdrop(this.runtime.getTargetForStage(), args.BACKDROP);
}
switchBackdropAndWait (args, util) {
// Have we run before, starting threads?
if (!util.stackFrame.startedThreads) {
// No - switch the backdrop.
util.stackFrame.startedThreads = (
this._setBackdrop(
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;
// Scratch 2 considers threads to be waiting if they are still in
// runtime.threads. Threads that have run all their blocks, or are
// marked done but still in runtime.threads are still considered to
// be waiting.
const waiting = util.stackFrame.startedThreads
.some(thread => instance.runtime.threads.indexOf(thread) !== -1);
if (waiting) {
// 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();
}
}
}
nextBackdrop () {
const stage = this.runtime.getTargetForStage();
this._setBackdrop(
stage, stage.currentCostume + 1, true
);
}
previousBackdrop() {
const stage = this.runtime.getTargetForStage();
this._setBackdrop(
stage, stage.currentCostume - 1, true
);
}
clampEffect (effect, value) { // used by compiler
let clampedValue = value;
switch (effect) {
case 'ghost':
clampedValue = MathUtil.clamp(value,
Scratch3LooksBlocks.EFFECT_GHOST_LIMIT.min,
Scratch3LooksBlocks.EFFECT_GHOST_LIMIT.max);
break;
case 'brightness':
clampedValue = MathUtil.clamp(value,
Scratch3LooksBlocks.EFFECT_BRIGHTNESS_LIMIT.min,
Scratch3LooksBlocks.EFFECT_BRIGHTNESS_LIMIT.max);
break;
}
return clampedValue;
}
changeEffect (args, util) {
const effect = Cast.toString(args.EFFECT).toLowerCase();
const change = Cast.toNumber(args.CHANGE);
if (!util.target.effects.hasOwnProperty(effect)) return;
let newValue = change + util.target.effects[effect];
newValue = this.clampEffect(effect, newValue);
util.target.setEffect(effect, newValue);
}
setEffect (args, util) {
const effect = Cast.toString(args.EFFECT).toLowerCase();
let value = Cast.toNumber(args.VALUE);
value = this.clampEffect(effect, value);
util.target.setEffect(effect, value);
}
clearEffects (args, util) {
util.target.clearEffects();
this._resetBubbles(util.target);
}
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);
}
goToFrontBack (args, util) {
if (!util.target.isStage) {
if (args.FRONT_BACK === 'front') {
util.target.goToFront();
} else {
util.target.goToBack();
}
}
}
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));
}
}
}
goTargetLayer (args, util) {
let target;
const option = args.VISIBLE_OPTION;
if (option === '_stage_') target = this.runtime.getTargetForStage();
else target = this.runtime.getSpriteTargetByName(option);
if (!util.target.isStage && target) {
if (args.FORWARD_BACKWARD === 'infront') {
util.target.goBehindOther(target);
util.target.goForwardLayers(1);
} else {
util.target.goBehindOther(target);
}
}
}
getSize (args, util) {
return Math.round(util.target.size);
}
getBackdropNumberName (args) {
const stage = this.runtime.getTargetForStage();
if (args.NUMBER_NAME === 'number') {
return stage.currentCostume + 1;
}
// Else return name
return stage.getCostumes()[stage.currentCostume].name;
}
getCostumeNumberName (args, util) {
if (args.NUMBER_NAME === 'number') {
return util.target.currentCostume + 1;
}
// Else return name
return util.target.getCostumes()[util.target.currentCostume].name;
}
}
module.exports = Scratch3LooksBlocks;