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) { say (args, util) {
// @TODO in 2.0 calling say/think resets the right/left bias of the bubble // @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); this.runtime.emit(Scratch3LooksBlocks.SAY_OR_THINK, util.target, 'say', args.MESSAGE);
} }
sayforsecs (args, util) { sayforsecs (args, util) {
this.say(args, util); this._bubbleForSecs(args, util, 'say');
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) { think (args, util) {
@ -353,19 +368,7 @@ class Scratch3LooksBlocks {
} }
thinkforsecs (args, util) { thinkforsecs (args, util) {
this.think(args, util); this._bubbleForSecs(args, util, 'think');
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) { show (args, util) {

View file

@ -1,6 +1,5 @@
const Cast = require('../util/cast'); const Cast = require('../util/cast');
const MathUtil = require('../util/math-util'); const MathUtil = require('../util/math-util');
const Timer = require('../util/timer');
class Scratch3MotionBlocks { class Scratch3MotionBlocks {
constructor (runtime) { constructor (runtime) {
@ -142,37 +141,34 @@ class Scratch3MotionBlocks {
} }
glide (args, util) { glide (args, util) {
if (util.stackFrame.timer) { if (util.stackTimerNeedsInit()) {
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 {
// First time: save data for future use. // First time: save data for future use.
util.stackFrame.timer = new Timer(); const duration = Cast.toNumber(args.SECS) * 1000;
util.stackFrame.timer.start(); util.startStackTimer(duration);
util.stackFrame.duration = Cast.toNumber(args.SECS);
util.stackFrame.startX = util.target.x; util.stackFrame.startX = util.target.x;
util.stackFrame.startY = util.target.y; util.stackFrame.startY = util.target.y;
util.stackFrame.endX = Cast.toNumber(args.X); util.stackFrame.endX = Cast.toNumber(args.X);
util.stackFrame.endY = Cast.toNumber(args.Y); util.stackFrame.endY = Cast.toNumber(args.Y);
if (util.stackFrame.duration <= 0) { if (duration <= 0) {
// Duration too short to glide. // Duration too short to glide.
util.target.setXY(util.stackFrame.endX, util.stackFrame.endY); util.target.setXY(util.stackFrame.endX, util.stackFrame.endY);
return; return;
} }
util.yield(); 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 Cast = require('../../util/cast');
const formatMessage = require('format-message'); const formatMessage = require('format-message');
const MathUtil = require('../../util/math-util'); const MathUtil = require('../../util/math-util');
const Timer = require('../../util/timer');
/** /**
* The instrument and drum sounds, loaded as static assets. * The instrument and drum sounds, loaded as static assets.
@ -961,7 +960,7 @@ class Scratch3MusicBlocks {
* @param {object} util - utility object provided by the runtime. * @param {object} util - utility object provided by the runtime.
*/ */
_playDrumForBeats (drumNum, beats, util) { _playDrumForBeats (drumNum, beats, util) {
if (this._stackTimerNeedsInit(util)) { if (util.stackTimerNeedsInit()) {
drumNum = Cast.toNumber(drumNum); drumNum = Cast.toNumber(drumNum);
drumNum = Math.round(drumNum); drumNum = Math.round(drumNum);
drumNum -= 1; // drums are one-indexed drumNum -= 1; // drums are one-indexed
@ -969,9 +968,10 @@ class Scratch3MusicBlocks {
beats = Cast.toNumber(beats); beats = Cast.toNumber(beats);
beats = this._clampBeats(beats); beats = this._clampBeats(beats);
this._playDrumNum(util, drumNum); this._playDrumNum(util, drumNum);
this._startStackTimer(util, this._beatsToSec(beats)); util.startStackTimer(this._beatsToMSec(beats));
} else { util.yield();
this._checkStackTimer(util); } else if (!util.stackTimerFinished()) {
util.yield();
} }
} }
@ -1025,12 +1025,13 @@ class Scratch3MusicBlocks {
* @property {number} BEATS - the duration in beats of the rest. * @property {number} BEATS - the duration in beats of the rest.
*/ */
restForBeats (args, util) { restForBeats (args, util) {
if (this._stackTimerNeedsInit(util)) { if (util.stackTimerNeedsInit()) {
let beats = Cast.toNumber(args.BEATS); let beats = Cast.toNumber(args.BEATS);
beats = this._clampBeats(beats); beats = this._clampBeats(beats);
this._startStackTimer(util, this._beatsToSec(beats)); util.startStackTimer(this._beatsToMSec(beats));
} else { util.yield();
this._checkStackTimer(util); } else if (!util.stackTimerFinished()) {
util.yield();
} }
} }
@ -1043,7 +1044,7 @@ class Scratch3MusicBlocks {
* @property {number} BEATS - the duration in beats of the note. * @property {number} BEATS - the duration in beats of the note.
*/ */
playNoteForBeats (args, util) { playNoteForBeats (args, util) {
if (this._stackTimerNeedsInit(util)) { if (util.stackTimerNeedsInit()) {
let note = Cast.toNumber(args.NOTE); let note = Cast.toNumber(args.NOTE);
note = MathUtil.clamp(note, note = MathUtil.clamp(note,
Scratch3MusicBlocks.MIDI_NOTE_RANGE.min, Scratch3MusicBlocks.MIDI_NOTE_RANGE.max); Scratch3MusicBlocks.MIDI_NOTE_RANGE.min, Scratch3MusicBlocks.MIDI_NOTE_RANGE.max);
@ -1053,13 +1054,14 @@ class Scratch3MusicBlocks {
// but "play note for 0 beats" is silent. // but "play note for 0 beats" is silent.
if (beats === 0) return; 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); util.startStackTimer(durationMSec);
} else { util.yield();
this._checkStackTimer(util); } 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. * @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 * @private
*/ */
_beatsToSec (beats) { _beatsToMSec (beats) {
return (60 / this.getTempo()) * beats; return (60 / this.getTempo()) * beats * 1000;
}
/**
* 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();
}
} }
/** /**

View file

@ -1,20 +1,23 @@
const test = require('tap').test; const test = require('tap').test;
const Music = require('../../src/extensions/scratch3_music/index.js'); 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 = { const rt = new Runtime();
getTargetForStage: () => ({tempo: 60}), const util = new BlockUtility();
on: () => {} // Stub out listener methods used in constructor. 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 = { const blocks = new Music(rt);
stackFrame: Object.create(null),
target: {
audioPlayer: null
},
yield: () => null
};
test('playDrum uses 1-indexing and wrap clamps', t => { test('playDrum uses 1-indexing and wrap clamps', t => {
// Stub playDrumNum // Stub playDrumNum