tweaks and improvements
This commit is contained in:
+98
-15
@@ -26,12 +26,15 @@
|
||||
const propSources = new Map();
|
||||
let propPreviewSignature = null, propPreviewGeneration = 0;
|
||||
const SOUND_SOURCE_KEY = 'animationdirector_sound_source';
|
||||
const SOUND_SELECTION_BLACKLIST = new Set(['needsofnature:a', 'needsofnature:funkytown']);
|
||||
const SOUND_PREVIEW_PROPERTY = 'animationdirector_audio_preview';
|
||||
const SOUND_EVENTS = ['select_animation', 'load_project', 'select_project', 'update_keyframe_selection'];
|
||||
let soundLibraryAction, soundLibraryDialog, soundLibraryListener, soundPreviewProperty, soundAudition;
|
||||
let soundLibrary, soundLibraryGeneration = 0, soundPreviewMode = 'dry';
|
||||
let soundLibrary, soundLibraryGeneration = 0;
|
||||
const soundAudioUrls = new Map(), soundMappedPoints = new WeakMap();
|
||||
let soundAudioBytes = 0;
|
||||
const figuraSoundPatches = [], figuraSoundMedia = new Set();
|
||||
let figuraSoundCompatEnabled = true, figuraSoundPlayListener;
|
||||
const SYSTEM_TAG_OPTIONS = {
|
||||
'': 'None (normal animation)',
|
||||
manualpeak: 'Manual peak',
|
||||
@@ -5410,11 +5413,11 @@
|
||||
return soundLibrary;
|
||||
}
|
||||
|
||||
function soundCueId(cue, mode = soundPreviewMode) {
|
||||
function soundCueId(cue) {
|
||||
const text = String(cue || '').trim();
|
||||
const bare = text.replace(/^(?:needsofnature|minecraft):/, '');
|
||||
if (bare.toLowerCase() === 'reactiveimpact_silent') return null;
|
||||
const semantic = { reactiveimpact: { dry: 'impactdry01', wet: 'impactwet01', full: 'wet04', peak: 'shot_in01' }[mode] || 'impactdry01',
|
||||
const semantic = { reactiveimpact: 'impactdry01',
|
||||
impactdry: 'impactdry01', impactwet: 'impactwet01', birth: 'pop' };
|
||||
if (semantic[bare.toLowerCase()]) return `needsofnature:${semantic[bare.toLowerCase()]}`;
|
||||
const id = text.includes(':') ? text : `needsofnature:${text}`;
|
||||
@@ -5468,22 +5471,17 @@
|
||||
for (const keyframe of animation?.animators?.effects?.sound || []) {
|
||||
for (const point of keyframe.data_points || []) {
|
||||
if (point.file && point.file !== point[SOUND_PREVIEW_PROPERTY]) continue;
|
||||
const signature = `${soundLibraryGeneration}:${soundPreviewMode}:${point.effect}:${point.sound}:${point.file}`;
|
||||
const signature = `${soundLibraryGeneration}:${point.effect}:${point.sound}:${point.file}`;
|
||||
if (soundMappedPoints.get(point) === signature) continue;
|
||||
const audio = resolveSoundAudio(point.effect) || (point.sound ? resolveSoundAudio(point.sound) : null);
|
||||
point.file = audio || '';
|
||||
point[SOUND_PREVIEW_PROPERTY] = point.file;
|
||||
soundMappedPoints.set(point, `${soundLibraryGeneration}:${soundPreviewMode}:${point.effect}:${point.sound}:${point.file}`);
|
||||
soundMappedPoints.set(point, `${soundLibraryGeneration}:${point.effect}:${point.sound}:${point.file}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setSoundPreviewMode(mode) {
|
||||
soundPreviewMode = ['dry', 'wet', 'full', 'peak'].includes(mode) ? mode : 'dry';
|
||||
mapAnimationSounds(typeof Animation === 'undefined' ? [] : Animation.all);
|
||||
}
|
||||
|
||||
function applyLibrarySound(id, insert = false) {
|
||||
const animation = typeof Animation === 'undefined' ? null : Animation.selected;
|
||||
if (!animation) throw new Error('Select an animation first.');
|
||||
@@ -5521,13 +5519,14 @@
|
||||
soundLibraryDialog = new Dialog({ id: 'animationdirector_sound_library', title: 'Sound Library', width: 700,
|
||||
buttons: ['Close'], onConfirm: stopSoundAudition, onCancel: stopSoundAudition,
|
||||
component: {
|
||||
data() { return { query: '', category: 'all', selected: '', mode: soundPreviewMode, page: 0,
|
||||
data() { return { query: '', category: 'all', selected: '', page: 0,
|
||||
ids: [...soundLibrary.events.keys()].sort(), warnings: soundLibrary.warnings.join('\n'), error: '' }; },
|
||||
computed: {
|
||||
rows() {
|
||||
const cues = ['reactiveimpact', 'reactiveimpact_silent', 'impactdry', 'impactwet', 'birth'];
|
||||
return [...cues.map((id) => ({ id, category: 'Gameplay cues' })), ...this.ids.map((id) => ({ id, category: 'Sound events' }))]
|
||||
.filter((entry) => (this.category === 'all' || entry.category === this.category) && entry.id.toLowerCase().includes(this.query.toLowerCase()));
|
||||
.filter((entry) => !SOUND_SELECTION_BLACKLIST.has(entry.id)
|
||||
&& (this.category === 'all' || entry.category === this.category) && entry.id.toLowerCase().includes(this.query.toLowerCase()));
|
||||
},
|
||||
visibleRows() { return this.rows.slice(this.page * 100, (this.page + 1) * 100); },
|
||||
},
|
||||
@@ -5558,16 +5557,14 @@
|
||||
},
|
||||
stop: stopSoundAudition,
|
||||
apply(insert) { try { applyLibrarySound(this.selected, insert); this.error = ''; } catch (error) { this.error = error.message; } },
|
||||
modeChanged() { stopSoundAudition(); setSoundPreviewMode(this.mode); },
|
||||
},
|
||||
template: `<div style="padding:12px">
|
||||
<div class="bar flex" style="gap:6px"><button @click="jar">Select NoN JAR / ZIP</button><button @click="folder">Select Assets Folder</button><button @click="reload">Refresh</button></div>
|
||||
<p class="subtle">Uses the selected sound source and remembered Pack Folders. Previews never change exported cue IDs.</p>
|
||||
<div class="bar flex" style="gap:8px;margin:10px 0"><input v-model="query" placeholder="Search sound IDs" style="flex:1" aria-label="Search sound IDs"><select v-model="category"><option value="all">All sounds</option><option>Sound events</option><option>Gameplay cues</option></select></div>
|
||||
<label>Reactive preview <select v-model="mode" @change="modeChanged"><option value="dry">Dry</option><option value="wet">Wet</option><option value="full">Full</option><option value="peak">Peak</option></select></label>
|
||||
<p class="subtle">Gameplay cues and random events use representative audio, not a gameplay simulation.</p>
|
||||
<div style="height:260px;overflow-y:auto;margin:10px 0;border:1px solid var(--color-border)">
|
||||
<div v-for="row in visibleRows" :key="row.category+row.id" @click="selected=row.id;stop()" @dblclick="selected=row.id;preview()" :style="{background:selected===row.id?'var(--color-accent)':'',padding:'6px 10px',cursor:'pointer'}"><span>{{row.id}}</span><small style="float:right;opacity:.6">{{row.category}}</small></div>
|
||||
<div v-for="row in visibleRows" :key="row.category+row.id" @click="selected=row.id;preview()" :style="{background:selected===row.id?'var(--color-accent)':'',padding:'6px 10px',cursor:'pointer'}"><span>{{row.id}}</span><small style="float:right;opacity:.6">{{row.category}}</small></div>
|
||||
</div>
|
||||
<div class="bar flex" style="gap:6px"><button @click="page--" :disabled="page===0">Previous</button><span>{{rows.length}} results</span><button @click="page++" :disabled="(page+1)*100>=rows.length">Next</button></div>
|
||||
<div class="bar flex" style="gap:6px;margin-top:12px"><button @click="preview" :disabled="!selected">Play</button><button @click="stop">Stop</button><button @click="apply(false)" :disabled="!selected">Apply to Selected / Insert</button><button @click="apply(true)" :disabled="!selected">Insert at Playhead</button></div>
|
||||
@@ -5578,7 +5575,88 @@
|
||||
soundLibraryDialog.show();
|
||||
}
|
||||
|
||||
function isBrokenFiguraSoundWrapper(callback) {
|
||||
// Figura Format 0.1.5 discards in_loop. Do not patch native or corrected implementations.
|
||||
return typeof callback === 'function' && /^function\s*\(\s*\)\s*\{\s*if\s*\(\s*Format\s*===\s*format\s*\)\s*return\s*;?\s*displayFrame\.call\(\s*this\s*\)\s*;?\s*\}$/.test(Function.prototype.toString.call(callback));
|
||||
}
|
||||
|
||||
function removeFiguraSoundMedia(media) {
|
||||
for (const list of [Timeline.playing_sounds, Timeline.paused_sounds]) {
|
||||
const index = list?.indexOf(media) ?? -1;
|
||||
if (index >= 0) list.splice(index, 1);
|
||||
}
|
||||
figuraSoundMedia.delete(media);
|
||||
}
|
||||
|
||||
function playFiguraCompatSounds(animator, inLoop, states) {
|
||||
const time = animator.animation?.time;
|
||||
if (!Number.isFinite(time)) return;
|
||||
const previous = states.get(animator);
|
||||
states.set(animator, { time, includeStart: false });
|
||||
if (!inLoop || animator.muted?.sound || !Timeline.playing) return;
|
||||
const wrapped = previous && time < previous.time;
|
||||
const rangeStart = Timeline.custom_range?.[0] || 0;
|
||||
const rangeEnd = Math.min(animator.animation.length || Infinity, Timeline.custom_range?.[1] || Infinity);
|
||||
for (const frame of animator.sound || []) {
|
||||
const crossed = !previous ? frame.time === time : wrapped
|
||||
? (frame.time > previous.time && frame.time <= rangeEnd) || (frame.time >= rangeStart && frame.time <= time)
|
||||
: frame.time <= time && (frame.time > previous.time || (previous.includeStart && frame.time === previous.time));
|
||||
const file = frame.data_points?.[0]?.file;
|
||||
if (!crossed || !file) continue;
|
||||
const existing = Timeline.playing_sounds.filter((media) => media.keyframe_id === frame.uuid);
|
||||
// A sound resumed by Blockbench's startPreviousSounds must not be played twice.
|
||||
if (existing.length && !wrapped) continue;
|
||||
for (const media of existing) { media.pause(); removeFiguraSoundMedia(media); }
|
||||
try {
|
||||
const media = new Audio(file);
|
||||
media.keyframe_id = frame.uuid;
|
||||
media.playbackRate = Math.max(0.1, Math.min(4, (Timeline.playback_speed || 100) / 100));
|
||||
media.volume = Math.max(0, Math.min(1, typeof settings === 'undefined' ? 1 : (settings.volume?.value ?? 100) / 100));
|
||||
media.onended = () => removeFiguraSoundMedia(media);
|
||||
Timeline.playing_sounds.push(media); figuraSoundMedia.add(media);
|
||||
media.play().catch((error) => { removeFiguraSoundMedia(media); console.warn('AnimationDirector sound preview failed:', error); });
|
||||
} catch (error) { console.warn('AnimationDirector sound preview failed:', error); }
|
||||
}
|
||||
}
|
||||
|
||||
function installFiguraSoundCompat() {
|
||||
if (typeof EffectAnimator === 'undefined') return;
|
||||
const original = EffectAnimator.prototype.displayFrame;
|
||||
if (!isBrokenFiguraSoundWrapper(original)) return;
|
||||
const states = new WeakMap();
|
||||
const wrapper = function (...args) {
|
||||
const result = original.apply(this, args);
|
||||
if (figuraSoundCompatEnabled && typeof Format !== 'undefined' && Format.id === 'geckolib_model') {
|
||||
playFiguraCompatSounds(this, args[0], states);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
figuraSoundPatches.push({ original, wrapper, states });
|
||||
EffectAnimator.prototype.displayFrame = wrapper;
|
||||
}
|
||||
|
||||
function beginFiguraSoundPlayback() {
|
||||
installFiguraSoundCompat();
|
||||
if (typeof Animation === 'undefined') return;
|
||||
for (const animation of Animation.all) {
|
||||
const effects = animation.animators?.effects;
|
||||
if (effects) for (const patch of figuraSoundPatches) {
|
||||
patch.states.set(effects, { time: animation.time, includeStart: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeFiguraSoundCompat() {
|
||||
figuraSoundCompatEnabled = false;
|
||||
for (const media of [...figuraSoundMedia]) { media.pause(); removeFiguraSoundMedia(media); }
|
||||
for (const patch of figuraSoundPatches.slice().reverse()) {
|
||||
if (EffectAnimator.prototype.displayFrame === patch.wrapper) EffectAnimator.prototype.displayFrame = patch.original;
|
||||
}
|
||||
figuraSoundPatches.length = 0;
|
||||
}
|
||||
|
||||
function refreshSoundLibraryPanel() {
|
||||
installFiguraSoundCompat();
|
||||
if (typeof document !== 'undefined') {
|
||||
const panel = document.getElementById('panel_keyframe');
|
||||
if (panel && !document.getElementById('animationdirector_sound_library_button')) {
|
||||
@@ -5616,6 +5694,9 @@ You can import or export complete AFW animation IDs as one file containing all s
|
||||
});
|
||||
soundLibraryListener = refreshSoundLibraryPanel;
|
||||
Blockbench.on(SOUND_EVENTS.join(' '), soundLibraryListener);
|
||||
figuraSoundCompatEnabled = true;
|
||||
figuraSoundPlayListener = beginFiguraSoundPlayback;
|
||||
Blockbench.on('timeline_play', figuraSoundPlayListener);
|
||||
refreshSoundLibraryPanel();
|
||||
if (typeof Property === 'function' && typeof ModelProject !== 'undefined' && ModelProject?.properties) {
|
||||
if (!ModelProject.properties[ACTOR_NAMES_PROPERTY]) {
|
||||
@@ -5820,6 +5901,8 @@ You can import or export complete AFW animation IDs as one file containing all s
|
||||
MenuBar.update();
|
||||
},
|
||||
onunload() {
|
||||
Blockbench.removeListener('timeline_play', figuraSoundPlayListener);
|
||||
removeFiguraSoundCompat();
|
||||
for (const event of SOUND_EVENTS) Blockbench.removeListener(event, soundLibraryListener);
|
||||
if (typeof document !== 'undefined') document.getElementById('animationdirector_sound_library_button')?.remove();
|
||||
soundLibraryAction?.delete(); soundLibraryDialog?.delete(); soundPreviewProperty?.delete();
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
# AnimationDirector
|
||||
|
||||
## Sound Library
|
||||
|
||||
Open **Sound Library...** in the keyframe panel or **Tools > AnimationDirector Anims**.
|
||||
Select a NoN mod JAR/ZIP or a folder containing `assets` once. The plugin remembers
|
||||
this source and also reads sound registries from your configured **Pack Folders**.
|
||||
Pack folders use their configured priority, followed by the selected sound source.
|
||||
No sound assets are downloaded or bundled into the plugin.
|
||||
|
||||
Search sound IDs, select a result and press **Play** (or double-click it) to audition.
|
||||
**Apply to Selected / Insert** changes the first cue of selected sound keyframes,
|
||||
or inserts at the playhead when no sound keyframe is selected. **Insert at Playhead**
|
||||
always inserts; existing cues at that time are retained.
|
||||
|
||||
Recognized sound cues are automatically linked to timeline audio when importing
|
||||
animations, opening projects or selecting animations. Select the sound source first,
|
||||
or select it afterwards to map already imported animations. **Refresh** reloads changed
|
||||
files. Manually assigned audio previews and unknown gameplay cues are preserved.
|
||||
|
||||
**Reactive preview** selects representative Dry/Wet/Full/Peak audio for
|
||||
`reactiveimpact`. `reactiveimpact_silent` has no audio. Random sound events/pools use
|
||||
the first available sample. These previews do not simulate gameplay; exported
|
||||
animation cue IDs remain unchanged. Blockbench's native timeline player previews
|
||||
the first cue at a sound keyframe; additional simultaneous cues still export normally.
|
||||
|
||||
Sound browsing requires Blockbench Desktop. Preview audio is loaded on demand,
|
||||
cached with a 64 MiB limit, and kept out of exported animation JSON.
|
||||
|
||||
+141
-7
@@ -9,8 +9,9 @@ function harness() {
|
||||
const storage = new Map(), blobs = [], revoked = [], undo = [];
|
||||
const source = fs.readFileSync(path.join(__dirname, '..', 'AnimationDirector.js'), 'utf8')
|
||||
.replace(' Plugin.register(PLUGIN_ID, {', `
|
||||
globalThis.api = { loadSoundLibrary, resolveSoundAudio, mapAnimationSounds, setSoundPreviewMode,
|
||||
applyLibrarySound, compileConjoinedStage, showSoundLibrary, SOUND_SOURCE_KEY, SOUND_PREVIEW_PROPERTY };
|
||||
globalThis.api = { loadSoundLibrary, resolveSoundAudio, mapAnimationSounds,
|
||||
applyLibrarySound, compileConjoinedStage, showSoundLibrary, SOUND_SOURCE_KEY, SOUND_PREVIEW_PROPERTY,
|
||||
installFiguraSoundCompat, beginFiguraSoundPlayback, removeFiguraSoundCompat, isBrokenFiguraSoundWrapper };
|
||||
Plugin.register(PLUGIN_ID, {`);
|
||||
class EffectAnimator {
|
||||
constructor(animation) { this.animation = animation; this.sound = []; }
|
||||
@@ -28,11 +29,37 @@ function harness() {
|
||||
Animation: { selected: { animators: {} }, all: [] }, EffectAnimator, Timeline: { time: 2, selected: [] },
|
||||
Undo: { initEdit(aspects) { undo.push(aspects.keyframes.slice()); }, finishEdit() {} },
|
||||
Dialog: class { constructor(options) { context.dialog = options; } show() {} delete() {} },
|
||||
Format: { id: 'geckolib_model' }, settings: { volume: { value: 80 } }, console,
|
||||
played: [], coreCalls: [],
|
||||
Audio: class {
|
||||
constructor(file) { this.file = file; }
|
||||
play() { context.played.push(this); return Promise.resolve(); }
|
||||
pause() { this.paused = true; }
|
||||
},
|
||||
};
|
||||
vm.runInNewContext(source, context);
|
||||
return { ...context.api, context, storage, blobs, revoked, undo };
|
||||
}
|
||||
|
||||
function brokenFigura(h) {
|
||||
vm.runInNewContext(`(() => {
|
||||
const format = { id: 'figura' };
|
||||
const displayFrame = function (in_loop) { coreCalls.push(in_loop); return 'original-result'; };
|
||||
EffectAnimator.prototype.displayFrame = function () {
|
||||
if (Format === format) return;
|
||||
displayFrame.call(this);
|
||||
};
|
||||
})()`, h.context);
|
||||
const animation = { time: 0, length: 2, animators: {}, playing: true };
|
||||
const effects = new h.context.EffectAnimator(animation);
|
||||
effects.muted = { sound: false };
|
||||
effects.sound = [{ uuid: 'cue', time: 0.5, data_points: [{ file: 'manual.ogg' }] }];
|
||||
animation.animators.effects = effects;
|
||||
h.context.Animation.all = [animation];
|
||||
Object.assign(h.context.Timeline, { playing: true, playback_speed: 100, custom_range: [0, 0], playing_sounds: [] });
|
||||
return { animation, effects, original: h.context.EffectAnimator.prototype.displayFrame };
|
||||
}
|
||||
|
||||
function assets(t, json, files = {}) {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ad-sounds-'));
|
||||
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
||||
@@ -54,8 +81,6 @@ test('sound library reads the real NoN sound registry, lazily maps semantic cues
|
||||
assert.equal(h.blobs.length, 1);
|
||||
assert.equal(h.resolveSoundAudio('reactiveimpact_silent'), null);
|
||||
assert.equal(h.resolveSoundAudio('not_a_sound'), null);
|
||||
h.setSoundPreviewMode('peak');
|
||||
assert.equal(h.resolveSoundAudio('reactiveimpact'), h.resolveSoundAudio('shot_in01'));
|
||||
h.loadSoundLibrary();
|
||||
assert(h.revoked.includes(audio));
|
||||
});
|
||||
@@ -102,7 +127,8 @@ test('mapping retains cue IDs and manual previews, updates managed files and han
|
||||
assert.equal(points[1].file, 'custom.ogg');
|
||||
assert.equal(points[2].file, '');
|
||||
assert.equal(points[3].file, '');
|
||||
h.setSoundPreviewMode('peak');
|
||||
h.loadSoundLibrary();
|
||||
h.mapAnimationSounds([animation]);
|
||||
assert.notEqual(points[0].file, original);
|
||||
points[0].effect = 'reactiveimpact_silent';
|
||||
h.mapAnimationSounds([animation]);
|
||||
@@ -140,13 +166,121 @@ test('editor preview fields never appear in exported sound cues', () => {
|
||||
assert.equal(clip.sound_effects['0.0'][0].file, 'blob:test');
|
||||
});
|
||||
|
||||
test('library UI exposes search, semantic preview modes, audition and insertion controls', () => {
|
||||
test('library UI auditions on a single click without reactive preview controls', () => {
|
||||
const h = harness();
|
||||
h.showSoundLibrary();
|
||||
const ui = h.context.dialog.component;
|
||||
assert.match(ui.template, /Search sound IDs/);
|
||||
assert.match(ui.template, /Insert at Playhead/);
|
||||
assert.match(ui.template, /Reactive preview/);
|
||||
assert.doesNotMatch(ui.template, /Reactive preview|@dblclick/);
|
||||
assert.match(ui.template, /@click="selected=row.id;preview\(\)"/);
|
||||
assert.equal(typeof ui.methods.preview, 'function');
|
||||
assert.equal(typeof ui.methods.reload, 'function');
|
||||
});
|
||||
|
||||
test('sound selection hides blacklisted events without disabling existing cue previews', () => {
|
||||
const h = harness();
|
||||
h.storage.set(h.SOUND_SOURCE_KEY, path.resolve(__dirname, '../../NON/src/main/resources'));
|
||||
h.showSoundLibrary();
|
||||
const ui = h.context.dialog.component;
|
||||
const state = ui.data();
|
||||
assert(state.ids.includes('needsofnature:a'));
|
||||
assert(state.ids.includes('needsofnature:funkytown'));
|
||||
for (const query of ['', 'needsofnature:a', 'funkytown']) {
|
||||
const rows = ui.computed.rows.call({ ...state, query });
|
||||
assert(!rows.some((row) => row.id === 'needsofnature:a' || row.id === 'needsofnature:funkytown'));
|
||||
}
|
||||
assert(h.resolveSoundAudio('needsofnature:a'));
|
||||
assert(h.resolveSoundAudio('needsofnature:funkytown'));
|
||||
});
|
||||
|
||||
test('Figura workaround detects only the known faulty wrapper and restores it on unload', () => {
|
||||
const h = harness(), f = brokenFigura(h);
|
||||
assert(h.isBrokenFiguraSoundWrapper(f.original));
|
||||
h.installFiguraSoundCompat();
|
||||
const patched = h.context.EffectAnimator.prototype.displayFrame;
|
||||
assert.notEqual(patched, f.original);
|
||||
h.installFiguraSoundCompat();
|
||||
assert.equal(h.context.EffectAnimator.prototype.displayFrame, patched);
|
||||
f.animation.time = 0.5;
|
||||
f.effects.displayFrame(true);
|
||||
h.removeFiguraSoundCompat();
|
||||
assert.equal(h.context.EffectAnimator.prototype.displayFrame, f.original);
|
||||
for (const corrected of [function (in_loop) {}, function (...args) { return args; }]) {
|
||||
h.context.EffectAnimator.prototype.displayFrame = corrected;
|
||||
h.installFiguraSoundCompat();
|
||||
assert.equal(h.context.EffectAnimator.prototype.displayFrame, corrected);
|
||||
}
|
||||
});
|
||||
|
||||
test('Figura sound workaround triggers crossed keyframes even beyond the native 33ms window', () => {
|
||||
const h = harness(), f = brokenFigura(h);
|
||||
h.beginFiguraSoundPlayback();
|
||||
f.animation.time = 0.45; f.effects.displayFrame(true);
|
||||
f.animation.time = 0.6; f.effects.displayFrame(true);
|
||||
assert.equal(h.context.played.length, 1);
|
||||
assert.equal(h.context.played[0].file, 'manual.ogg');
|
||||
assert.equal(h.context.played[0].volume, 0.8);
|
||||
assert.equal(h.context.coreCalls.length, 2);
|
||||
assert.equal(h.context.coreCalls[0], undefined);
|
||||
f.animation.time = 0.7; f.effects.displayFrame(true);
|
||||
assert.equal(h.context.played.length, 1);
|
||||
});
|
||||
|
||||
test('Figura workaround plays start cues and restarts long sounds on subsequent loops', () => {
|
||||
const h = harness(), f = brokenFigura(h);
|
||||
f.effects.sound[0].time = 0;
|
||||
h.beginFiguraSoundPlayback();
|
||||
f.animation.time = 0.04; f.effects.displayFrame(true);
|
||||
assert.equal(h.context.played.length, 1);
|
||||
f.animation.time = 1.9; f.effects.displayFrame(true);
|
||||
f.animation.time = 0; f.effects.displayFrame(true);
|
||||
assert.equal(h.context.played.length, 2);
|
||||
assert(h.context.played[0].paused);
|
||||
assert.equal(h.context.Timeline.playing_sounds.length, 1);
|
||||
h.removeFiguraSoundCompat();
|
||||
assert(h.context.played[1].paused);
|
||||
assert.equal(h.context.Timeline.playing_sounds.length, 0);
|
||||
});
|
||||
|
||||
test('Figura workaround respects sound mute, scrubbing, other formats and resumed native audio', () => {
|
||||
const h = harness(), f = brokenFigura(h);
|
||||
h.beginFiguraSoundPlayback();
|
||||
f.animation.time = 0.7; f.effects.displayFrame(false);
|
||||
assert.equal(h.context.played.length, 0);
|
||||
f.animation.time = 0; h.beginFiguraSoundPlayback();
|
||||
f.effects.muted.sound = true;
|
||||
f.animation.time = 0.6; f.effects.displayFrame(true);
|
||||
assert.equal(h.context.played.length, 0);
|
||||
f.effects.muted.sound = false;
|
||||
f.animation.time = 0; h.beginFiguraSoundPlayback();
|
||||
h.context.Format.id = 'figura';
|
||||
f.animation.time = 0.6; f.effects.displayFrame(true);
|
||||
assert.equal(h.context.played.length, 0);
|
||||
h.context.Format.id = 'geckolib_model';
|
||||
f.animation.time = 0; h.beginFiguraSoundPlayback();
|
||||
h.context.Timeline.playing_sounds.push({ keyframe_id: 'cue' });
|
||||
f.animation.time = 0.6; f.effects.displayFrame(true);
|
||||
assert.equal(h.context.played.length, 0);
|
||||
});
|
||||
|
||||
test('Figura workaround honors custom loop ranges and leaves later plugin patches installed', () => {
|
||||
const h = harness(), f = brokenFigura(h);
|
||||
Object.assign(h.context.Timeline, { custom_range: [0.3, 1] });
|
||||
f.effects.sound = [{ uuid: 'before', time: 0.1, data_points: [{ file: 'excluded.ogg' }] },
|
||||
{ uuid: 'start', time: 0.3, data_points: [{ file: 'included.ogg' }] },
|
||||
{ uuid: 'after', time: 1.2, data_points: [{ file: 'excluded.ogg' }] }];
|
||||
f.animation.time = 0.3; h.beginFiguraSoundPlayback();
|
||||
f.animation.time = 0.35; f.effects.displayFrame(true);
|
||||
f.animation.time = 0.95; f.effects.displayFrame(true);
|
||||
f.animation.time = 0.3; f.effects.displayFrame(true);
|
||||
assert.deepEqual(h.context.played.map((media) => media.file), ['included.ogg', 'included.ogg']);
|
||||
const patched = h.context.EffectAnimator.prototype.displayFrame;
|
||||
const later = function (...args) { return patched.apply(this, args); };
|
||||
h.context.EffectAnimator.prototype.displayFrame = later;
|
||||
h.removeFiguraSoundCompat();
|
||||
assert.equal(h.context.EffectAnimator.prototype.displayFrame, later);
|
||||
f.animation.time = 0.95; f.effects.displayFrame(true);
|
||||
f.animation.time = 0.3; f.effects.displayFrame(true);
|
||||
assert.equal(h.context.played.length, 2);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user