This commit is contained in:
adroitwhiz 2025-05-08 12:29:22 -07:00 committed by GitHub
commit ce6b29940c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 83 additions and 114 deletions
src
blocks
extensions/scratch3_music
test/unit

View file

@ -327,25 +327,40 @@ class Scratch3LooksBlocks {
};
}
/**
* Create a text bubble and yield on this thread until it's done. Used for "say/think for () secs".
* @param {number} args Block arguments
* @param {BlockUtility} util Utility object provided by the runtime.
* @param {string} type The type of bubble (say/think)
* @private
*/
_bubbleForSecs (args, util, type) {
if (util.stackTimerNeedsInit()) {
this.runtime.emit(Scratch3LooksBlocks.SAY_OR_THINK, util.target, type, args.MESSAGE);
const duration = Math.max(0, 1000 * Cast.toNumber(args.SECS));
util.stackFrame.bubbleId = this._getBubbleState(util.target).usageId;
util.startStackTimer(duration);
util.yield();
} else if (util.stackTimerFinished()) {
// Make sure the bubble we're removing is the same bubble we created.
// We don't want to cancel a bubble started from another script.
if (util.stackFrame.bubbleId === this._getBubbleState(util.target).usageId) {
this._updateBubble(util.target, type, '');
}
} else {
util.yield();
}
}
say (args, util) {
// @TODO in 2.0 calling say/think resets the right/left bias of the bubble
this.runtime.emit(Scratch3LooksBlocks.SAY_OR_THINK, util.target, 'say', args.MESSAGE);
}
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);
});
this._bubbleForSecs(args, util, 'say');
}
think (args, util) {
@ -353,19 +368,7 @@ class Scratch3LooksBlocks {
}
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);
});
this._bubbleForSecs(args, util, 'think');
}
show (args, util) {

View file

@ -1,6 +1,5 @@
const Cast = require('../util/cast');
const MathUtil = require('../util/math-util');
const Timer = require('../util/timer');
class Scratch3MotionBlocks {
constructor (runtime) {
@ -142,37 +141,34 @@ class Scratch3MotionBlocks {
}
glide (args, util) {
if (util.stackFrame.timer) {
const timeElapsed = util.stackFrame.timer.timeElapsed();
if (timeElapsed < util.stackFrame.duration * 1000) {
// In progress: move to intermediate position.
const frac = timeElapsed / (util.stackFrame.duration * 1000);
const dx = frac * (util.stackFrame.endX - util.stackFrame.startX);
const dy = frac * (util.stackFrame.endY - util.stackFrame.startY);
util.target.setXY(
util.stackFrame.startX + dx,
util.stackFrame.startY + dy
);
util.yield();
} else {
// Finished: move to final position.
util.target.setXY(util.stackFrame.endX, util.stackFrame.endY);
}
} else {
if (util.stackTimerNeedsInit()) {
// First time: save data for future use.
util.stackFrame.timer = new Timer();
util.stackFrame.timer.start();
util.stackFrame.duration = Cast.toNumber(args.SECS);
const duration = Cast.toNumber(args.SECS) * 1000;
util.startStackTimer(duration);
util.stackFrame.startX = util.target.x;
util.stackFrame.startY = util.target.y;
util.stackFrame.endX = Cast.toNumber(args.X);
util.stackFrame.endY = Cast.toNumber(args.Y);
if (util.stackFrame.duration <= 0) {
if (duration <= 0) {
// Duration too short to glide.
util.target.setXY(util.stackFrame.endX, util.stackFrame.endY);
return;
}
util.yield();
} else if (util.stackTimerFinished()) {
// Finished: move to final position.
util.target.setXY(util.stackFrame.endX, util.stackFrame.endY);
} else {
// In progress: move to intermediate position.
const timeElapsed = util.stackFrame.timer.timeElapsed();
const frac = timeElapsed / util.stackFrame.duration;
const dx = frac * (util.stackFrame.endX - util.stackFrame.startX);
const dy = frac * (util.stackFrame.endY - util.stackFrame.startY);
util.target.setXY(
util.stackFrame.startX + dx,
util.stackFrame.startY + dy
);
util.yield();
}
}

View file

@ -4,7 +4,6 @@ const Clone = require('../../util/clone');
const Cast = require('../../util/cast');
const formatMessage = require('format-message');
const MathUtil = require('../../util/math-util');
const Timer = require('../../util/timer');
/**
* The instrument and drum sounds, loaded as static assets.
@ -961,7 +960,7 @@ class Scratch3MusicBlocks {
* @param {object} util - utility object provided by the runtime.
*/
_playDrumForBeats (drumNum, beats, util) {
if (this._stackTimerNeedsInit(util)) {
if (util.stackTimerNeedsInit()) {
drumNum = Cast.toNumber(drumNum);
drumNum = Math.round(drumNum);
drumNum -= 1; // drums are one-indexed
@ -969,9 +968,10 @@ class Scratch3MusicBlocks {
beats = Cast.toNumber(beats);
beats = this._clampBeats(beats);
this._playDrumNum(util, drumNum);
this._startStackTimer(util, this._beatsToSec(beats));
} else {
this._checkStackTimer(util);
util.startStackTimer(this._beatsToMSec(beats));
util.yield();
} else if (!util.stackTimerFinished()) {
util.yield();
}
}
@ -1025,12 +1025,13 @@ class Scratch3MusicBlocks {
* @property {number} BEATS - the duration in beats of the rest.
*/
restForBeats (args, util) {
if (this._stackTimerNeedsInit(util)) {
if (util.stackTimerNeedsInit()) {
let beats = Cast.toNumber(args.BEATS);
beats = this._clampBeats(beats);
this._startStackTimer(util, this._beatsToSec(beats));
} else {
this._checkStackTimer(util);
util.startStackTimer(this._beatsToMSec(beats));
util.yield();
} else if (!util.stackTimerFinished()) {
util.yield();
}
}
@ -1043,7 +1044,7 @@ class Scratch3MusicBlocks {
* @property {number} BEATS - the duration in beats of the note.
*/
playNoteForBeats (args, util) {
if (this._stackTimerNeedsInit(util)) {
if (util.stackTimerNeedsInit()) {
let note = Cast.toNumber(args.NOTE);
note = MathUtil.clamp(note,
Scratch3MusicBlocks.MIDI_NOTE_RANGE.min, Scratch3MusicBlocks.MIDI_NOTE_RANGE.max);
@ -1053,13 +1054,14 @@ class Scratch3MusicBlocks {
// but "play note for 0 beats" is silent.
if (beats === 0) return;
const durationSec = this._beatsToSec(beats);
const durationMSec = this._beatsToMSec(beats);
this._playNote(util, note, durationSec);
this._playNote(util, note, durationMSec * 0.001);
this._startStackTimer(util, durationSec);
} else {
this._checkStackTimer(util);
util.startStackTimer(durationMSec);
util.yield();
} else if (!util.stackTimerFinished()) {
util.yield();
}
}
@ -1199,48 +1201,13 @@ class Scratch3MusicBlocks {
}
/**
* Convert a number of beats to a number of seconds, using the current tempo.
* Convert a number of beats to a number of milliseconds, using the current tempo.
* @param {number} beats - number of beats to convert to secs.
* @return {number} seconds - number of seconds `beats` will last.
* @return {number} number of milliseconds `beats` will last.
* @private
*/
_beatsToSec (beats) {
return (60 / this.getTempo()) * beats;
}
/**
* Check if the stack timer needs initialization.
* @param {object} util - utility object provided by the runtime.
* @return {boolean} - true if the stack timer needs to be initialized.
* @private
*/
_stackTimerNeedsInit (util) {
return !util.stackFrame.timer;
}
/**
* Start the stack timer and the yield the thread if necessary.
* @param {object} util - utility object provided by the runtime.
* @param {number} duration - a duration in seconds to set the timer for.
* @private
*/
_startStackTimer (util, duration) {
util.stackFrame.timer = new Timer();
util.stackFrame.timer.start();
util.stackFrame.duration = duration;
util.yield();
}
/**
* Check the stack timer, and if its time is not up yet, yield the thread.
* @param {object} util - utility object provided by the runtime.
* @private
*/
_checkStackTimer (util) {
const timeElapsed = util.stackFrame.timer.timeElapsed();
if (timeElapsed < util.stackFrame.duration * 1000) {
util.yield();
}
_beatsToMSec (beats) {
return (60 / this.getTempo()) * beats * 1000;
}
/**

View file

@ -1,20 +1,23 @@
const test = require('tap').test;
const Music = require('../../src/extensions/scratch3_music/index.js');
const Blocks = require('../../src/engine/blocks');
const BlockUtility = require('../../src/engine/block-utility');
const Runtime = require('../../src/engine/runtime');
const Target = require('../../src/engine/target');
const Thread = require('../../src/engine/thread');
const fakeRuntime = {
getTargetForStage: () => ({tempo: 60}),
on: () => {} // Stub out listener methods used in constructor.
};
const rt = new Runtime();
const util = new BlockUtility();
util.sequencer = rt.sequencer;
util.runtime = rt;
util.thread = new Thread(null);
util.thread.pushStack();
const blocks = new Music(fakeRuntime);
const b = new Blocks(rt);
const tgt = new Target(rt, b);
tgt.isStage = true;
const util = {
stackFrame: Object.create(null),
target: {
audioPlayer: null
},
yield: () => null
};
const blocks = new Music(rt);
test('playDrum uses 1-indexing and wrap clamps', t => {
// Stub playDrumNum