tweaks and improvements

This commit is contained in:
2026-09-29 01:58:15 +02:00
parent 61df6bb0d3
commit 53a5c47549
4 changed files with 166 additions and 31 deletions
+62 -29
View File
@@ -28,7 +28,7 @@
const SOUND_SOURCE_KEY = 'animationdirector_sound_source'; const SOUND_SOURCE_KEY = 'animationdirector_sound_source';
const SOUND_SELECTION_BLACKLIST = new Set(['needsofnature:a', 'needsofnature:funkytown']); const SOUND_SELECTION_BLACKLIST = new Set(['needsofnature:a', 'needsofnature:funkytown']);
const SOUND_PREVIEW_PROPERTY = 'animationdirector_audio_preview'; const SOUND_PREVIEW_PROPERTY = 'animationdirector_audio_preview';
const SOUND_EVENTS = ['select_animation', 'load_project', 'select_project', 'update_keyframe_selection']; const SOUND_EVENTS = ['select_animation', 'load_project', 'select_project', 'update_keyframe_selection', 'undo', 'redo'];
let soundLibraryAction, soundLibraryDialog, soundLibraryListener, soundPreviewProperty, soundAudition; let soundLibraryAction, soundLibraryDialog, soundLibraryListener, soundPreviewProperty, soundAudition;
let soundLibrary, soundLibraryGeneration = 0; let soundLibrary, soundLibraryGeneration = 0;
const soundAudioUrls = new Map(), soundMappedPoints = new WeakMap(); const soundAudioUrls = new Map(), soundMappedPoints = new WeakMap();
@@ -2826,11 +2826,19 @@
return output; return output;
} }
function exportAnimationDefinitionStub(group, definition, actorMetadata) { function saveAnimationDefinitionFields(group, definition, actorMetadata) {
const undo = typeof Undo !== 'undefined';
if (undo) Undo.initEdit({ animdef_edit: true, actor_preferences_edit: true });
const mergedMetadata = { ...getProjectActorAnimdefMetadata(), ...actorMetadata }; const mergedMetadata = { ...getProjectActorAnimdefMetadata(), ...actorMetadata };
setProjectActorAnimdefMetadata(mergedMetadata); setProjectActorAnimdefMetadata(mergedMetadata);
setProjectAnimdefAnimators(definition.animators || []); setProjectAnimdefAnimators(definition.animators || []);
saveAnimdef(group.id, definition); saveAnimdef(group.id, definition);
if (undo) Undo.finishEdit('Save animation definition');
mapAnimationSounds(typeof Animation === 'undefined' ? [] : Animation.all);
}
function exportAnimationDefinitionStub(group, definition, actorMetadata) {
saveAnimationDefinitionFields(group, definition, actorMetadata);
Blockbench.export( Blockbench.export(
{ {
type: 'AFW Animation Definition', type: 'AFW Animation Definition',
@@ -2981,6 +2989,7 @@
const props = importPropPreviews(group.id, definition, sourcePaths, readPropPreviews()); const props = importPropPreviews(group.id, definition, sourcePaths, readPropPreviews());
cancelBlockRequirementUpdate(); cancelBlockRequirementUpdate();
rebuildBlockRequirementGuides(group.id, entry, definition, true, props.entries); rebuildBlockRequirementGuides(group.id, entry, definition, true, props.entries);
mapAnimationSounds(typeof Animation === 'undefined' ? [] : Animation.all);
if (props.warnings.length) Blockbench.showQuickMessage(`Item props: ${props.warnings.join(' ')}`, 6000); if (props.warnings.length) Blockbench.showQuickMessage(`Item props: ${props.warnings.join(' ')}`, 6000);
void refreshPropPreviews(true); void refreshPropPreviews(true);
if (blockRequirementPanel?.vue) blockRequirementPanel.vue.load(group.id); if (blockRequirementPanel?.vue) blockRequirementPanel.vue.load(group.id);
@@ -3041,7 +3050,7 @@
type: 'info', type: 'info',
text: text:
`Creating ${group.id}.json with ${analysis.actorIndices.length} actor(s) and ${analysis.stages.length} stage(s). ` + `Creating ${group.id}.json with ${analysis.actorIndices.length} actor(s) and ${analysis.stages.length} stage(s). ` +
'Save it under data/<namespace>/afw_animdefs/. Your choices are stored in this Blockbench project for later exports.', 'Save stores your fields in this Blockbench project. Create Animdef also exports JSON for data/<namespace>/afw_animdefs/.',
}, },
}; };
if (analysis.warnings.length) { if (analysis.warnings.length) {
@@ -3071,7 +3080,7 @@
form.animators = { form.animators = {
label: 'Animators', label: 'Animators',
type: 'text', type: 'text',
value: (saved.animators || getProjectAnimdefAnimators()).join(', '), value: (saved.animators || (Object.keys(saved).length ? [] : getProjectAnimdefAnimators())).join(', '),
placeholder: 'Animator One, Animator Two', placeholder: 'Animator One, Animator Two',
description: 'Optional comma-separated individual animation credits. Saved in this Blockbench project.', description: 'Optional comma-separated individual animation credits. Saved in this Blockbench project.',
}; };
@@ -3363,15 +3372,7 @@
}; };
}); });
const dialog = new Dialog({ const submit = (result, exportFile) => {
id: 'create_afw_animation_definition_review',
title: `Create Animation Definition: ${group.id}`,
width: 760,
form,
buttons: ['Cancel', 'Create Animdef'],
cancelIndex: 0,
confirmIndex: 1,
onConfirm: (result) => {
try { try {
const actorMetadata = {}; const actorMetadata = {};
const actors = []; const actors = [];
@@ -3525,9 +3526,14 @@
} }
definition.actors = actors; definition.actors = actors;
definition.stages = stages; definition.stages = stages;
confirmUnresolvedActorTypes(unresolvedLabels, () => if (exportFile) {
exportAnimationDefinitionStub(group, definition, actorMetadata) confirmUnresolvedActorTypes(unresolvedLabels, () =>
); exportAnimationDefinitionStub(group, definition, actorMetadata)
);
} else {
saveAnimationDefinitionFields(group, definition, actorMetadata);
Blockbench.showQuickMessage(`Saved animation definition fields for ${group.id}.`);
}
return true; return true;
} catch (e) { } catch (e) {
Blockbench.showMessageBox({ Blockbench.showMessageBox({
@@ -3537,6 +3543,21 @@
}); });
return false; return false;
} }
};
const dialog = new Dialog({
id: 'create_afw_animation_definition_review',
title: `Create Animation Definition: ${group.id}`,
width: 760,
form,
buttons: ['Cancel', 'Save', 'Create Animdef'],
cancelIndex: 0,
confirmIndex: 2,
onConfirm: (result) => submit(result, true),
onButton(index) {
if (index === 1) {
submit(dialog.getFormResult(), false);
return false;
}
}, },
}); });
dialog.show(); dialog.show();
@@ -5413,28 +5434,36 @@
return soundLibrary; return soundLibrary;
} }
function soundCueId(cue) { function isPeakedSoundStage(animation) {
if (typeof Project === 'undefined' || !Project) return false;
const parsed = parseAfwStageAnimationName(animation);
if (!parsed) return false;
const stages = getSavedAnimdef(parsed.id).stages;
return Array.isArray(stages) && stages.some((stage) => stage.stage === parsed.stageNumber && stage.non_peak === true);
}
function soundCueId(cue, animation) {
const text = String(cue || '').trim(); const text = String(cue || '').trim();
const bare = text.replace(/^(?:needsofnature|minecraft):/, ''); const bare = text.replace(/^(?:needsofnature|minecraft):/, '');
if (bare.toLowerCase() === 'reactiveimpact_silent') return null; if (bare.toLowerCase() === 'reactiveimpact_silent') return null;
const semantic = { reactiveimpact: 'impactdry01', const semantic = { reactiveimpact: isPeakedSoundStage(animation) ? 'shot_in01' : 'impactdry01',
impactdry: 'impactdry01', impactwet: 'impactwet01', birth: 'pop' }; impactdry: 'impactdry01', impactwet: 'impactwet01', birth: 'pop' };
if (semantic[bare.toLowerCase()]) return `needsofnature:${semantic[bare.toLowerCase()]}`; if (semantic[bare.toLowerCase()]) return `needsofnature:${semantic[bare.toLowerCase()]}`;
const id = text.includes(':') ? text : `needsofnature:${text}`; const id = text.includes(':') ? text : `needsofnature:${text}`;
return importResourceId(id) ? id : null; return importResourceId(id) ? id : null;
} }
function resolveSoundAudio(cue, library = ensureSoundLibrary(), visiting = new Set()) { function resolveSoundAudio(cue, library = ensureSoundLibrary(), visiting = new Set(), animation = null) {
if (/^random\(/i.test(String(cue).trim())) { if (/^random\(/i.test(String(cue).trim())) {
const match = String(cue).trim().match(/^random\((.*)\)$/i); const match = String(cue).trim().match(/^random\((.*)\)$/i);
if (!match) return null; if (!match) return null;
for (const option of match[1].split(',').slice(0, 256)) { for (const option of match[1].split(',').slice(0, 256)) {
const audio = resolveSoundAudio(option, library, visiting); const audio = resolveSoundAudio(option, library, visiting, animation);
if (audio) return audio; if (audio) return audio;
} }
return null; return null;
} }
const id = soundCueId(cue); const id = soundCueId(cue, animation);
if (!id || visiting.has(id) || visiting.size >= 16) return null; if (!id || visiting.has(id) || visiting.size >= 16) return null;
const event = library.events.get(id); const event = library.events.get(id);
if (!event) return null; if (!event) return null;
@@ -5446,7 +5475,7 @@
const resource = importResourceId(name); const resource = importResourceId(name);
if (!resource) continue; if (!resource) continue;
if (object.type === 'event') { if (object.type === 'event') {
const audio = resolveSoundAudio(name, library, next); const audio = resolveSoundAudio(name, library, next, animation);
if (audio) return audio; if (audio) return audio;
continue; continue;
} }
@@ -5468,15 +5497,18 @@
function mapAnimationSounds(animations) { function mapAnimationSounds(animations) {
for (const animation of animations || []) { for (const animation of animations || []) {
const peaked = isPeakedSoundStage(animation);
for (const keyframe of animation?.animators?.effects?.sound || []) { for (const keyframe of animation?.animators?.effects?.sound || []) {
for (const point of keyframe.data_points || []) { for (const point of keyframe.data_points || []) {
if (point.file && point.file !== point[SOUND_PREVIEW_PROPERTY]) continue; if (point.file && point.file !== point[SOUND_PREVIEW_PROPERTY]) continue;
const signature = `${soundLibraryGeneration}:${point.effect}:${point.sound}:${point.file}`; const signature = `${soundLibraryGeneration}:${peaked}:${point.effect}:${point.sound}:${point.file}`;
if (soundMappedPoints.get(point) === signature) continue; if (soundMappedPoints.get(point) === signature) continue;
const audio = resolveSoundAudio(point.effect) || (point.sound ? resolveSoundAudio(point.sound) : null); const library = ensureSoundLibrary();
const audio = resolveSoundAudio(point.effect, library, new Set(), animation)
|| (point.sound ? resolveSoundAudio(point.sound, library, new Set(), animation) : null);
point.file = audio || ''; point.file = audio || '';
point[SOUND_PREVIEW_PROPERTY] = point.file; point[SOUND_PREVIEW_PROPERTY] = point.file;
soundMappedPoints.set(point, `${soundLibraryGeneration}:${point.effect}:${point.sound}:${point.file}`); soundMappedPoints.set(point, `${soundLibraryGeneration}:${peaked}:${point.effect}:${point.sound}:${point.file}`);
} }
} }
} }
@@ -5516,7 +5548,7 @@
stopSoundAudition(); stopSoundAudition();
ensureSoundLibrary(); ensureSoundLibrary();
soundLibraryDialog?.delete(); soundLibraryDialog?.delete();
soundLibraryDialog = new Dialog({ id: 'animationdirector_sound_library', title: 'Sound Library', width: 700, soundLibraryDialog = new Dialog({ id: 'animationdirector_sound_library', title: 'NoN Sound Library', width: 700,
buttons: ['Close'], onConfirm: stopSoundAudition, onCancel: stopSoundAudition, buttons: ['Close'], onConfirm: stopSoundAudition, onCancel: stopSoundAudition,
component: { component: {
data() { return { query: '', category: 'all', selected: '', page: 0, data() { return { query: '', category: 'all', selected: '', page: 0,
@@ -5549,7 +5581,7 @@
}, },
preview() { preview() {
stopSoundAudition(); this.error = ''; stopSoundAudition(); this.error = '';
const file = resolveSoundAudio(this.selected); const file = resolveSoundAudio(this.selected, ensureSoundLibrary(), new Set(), typeof Animation === 'undefined' ? null : Animation.selected);
if (!file) { this.error = this.selected === 'reactiveimpact_silent' ? 'Silent cue: gameplay effects only.' : 'No audio found. Select the NoN JAR or an extracted assets folder.'; return; } if (!file) { this.error = this.selected === 'reactiveimpact_silent' ? 'Silent cue: gameplay effects only.' : 'No audio found. Select the NoN JAR or an extracted assets folder.'; return; }
soundAudition = new Audio(file); soundAudition = new Audio(file);
soundAudition.volume = Math.max(0, Math.min(1, typeof settings === 'undefined' ? 1 : (settings.volume?.value ?? 100) / 100)); soundAudition.volume = Math.max(0, Math.min(1, typeof settings === 'undefined' ? 1 : (settings.volume?.value ?? 100) / 100));
@@ -5638,6 +5670,7 @@
function beginFiguraSoundPlayback() { function beginFiguraSoundPlayback() {
installFiguraSoundCompat(); installFiguraSoundCompat();
if (typeof Animation === 'undefined') return; if (typeof Animation === 'undefined') return;
mapAnimationSounds(Animation.all);
for (const animation of Animation.all) { for (const animation of Animation.all) {
const effects = animation.animators?.effects; const effects = animation.animators?.effects;
if (effects) for (const patch of figuraSoundPatches) { if (effects) for (const patch of figuraSoundPatches) {
@@ -5661,7 +5694,7 @@
const panel = document.getElementById('panel_keyframe'); const panel = document.getElementById('panel_keyframe');
if (panel && !document.getElementById('animationdirector_sound_library_button')) { if (panel && !document.getElementById('animationdirector_sound_library_button')) {
const button = document.createElement('button'); const button = document.createElement('button');
button.id = 'animationdirector_sound_library_button'; button.textContent = 'Sound Library...'; button.id = 'animationdirector_sound_library_button'; button.textContent = 'NoN Sound Library';
button.style.cssText = 'margin:8px;width:calc(100% - 16px)'; button.style.cssText = 'margin:8px;width:calc(100% - 16px)';
button.addEventListener('click', showSoundLibrary); panel.appendChild(button); button.addEventListener('click', showSoundLibrary); panel.appendChild(button);
} }
@@ -5688,7 +5721,7 @@ You can import or export complete AFW animation IDs as one file containing all s
}); });
} }
soundLibraryAction = new Action('animationdirector_sound_library', { soundLibraryAction = new Action('animationdirector_sound_library', {
name: 'Sound Library...', icon: 'library_music', category: 'animation', name: 'NoN Sound Library', icon: 'library_music', category: 'animation',
description: 'Browse NoN and pack sounds, audition audio and add sound cues to the timeline.', description: 'Browse NoN and pack sounds, audition audio and add sound cues to the timeline.',
click: showSoundLibrary, click: showSoundLibrary,
}); });
+39 -1
View File
@@ -13,6 +13,7 @@ function createHarness() {
const dialogs = []; const dialogs = [];
const exports = []; const exports = [];
const errors = []; const errors = [];
let formResult;
const project = { multiactor_actor_names: JSON.stringify({ 1: 'player', 2: 'wolf' }) }; const project = { multiactor_actor_names: JSON.stringify({ 1: 'player', 2: 'wolf' }) };
const context = { const context = {
Project: project, Project: project,
@@ -20,6 +21,7 @@ function createHarness() {
Dialog: class { Dialog: class {
constructor(options) { this.options = options; dialogs.push(options); } constructor(options) { this.options = options; dialogs.push(options); }
show() {} show() {}
getFormResult() { return formResult ?? defaults(this.options.form); }
}, },
Blockbench: { Blockbench: {
export(options, callback) { exports.push(JSON.parse(options.content)); callback(); }, export(options, callback) { exports.push(JSON.parse(options.content)); callback(); },
@@ -43,7 +45,7 @@ function createHarness() {
context.adTest.showAnimationDefinitionReviewDialog(group, analysis); context.adTest.showAnimationDefinitionReviewDialog(group, analysis);
return dialogs.at(-1); return dialogs.at(-1);
} }
return { open, exports, errors, project, analysis }; return { open, exports, errors, project, analysis, setFormResult(result) { formResult = result; } };
} }
function defaults(form) { function defaults(form) {
@@ -52,6 +54,42 @@ function defaults(form) {
.map(([key, field]) => [key, field.value])); .map(([key, field]) => [key, field.value]));
} }
test('Save retains definition fields in the project without exporting and restores them on reopen', () => {
const h = createHarness(), dialog = h.open(), result = defaults(dialog.form);
assert.deepEqual(Array.from(dialog.buttons), ['Cancel', 'Save', 'Create Animdef']);
assert.equal(dialog.confirmIndex, 2);
result.display_name = 'Saved definition';
result.content_tags = 'saved, tags';
result.weight = '3';
result.stub_stage_1_peaked = true;
result.stub_stage_2_peaked = false;
h.setFormResult(result);
assert.equal(dialog.onButton(1), false);
assert.equal(h.exports.length, 0);
assert.deepEqual(h.errors, []);
assert.equal(h.project.saved, false);
const reopened = h.open();
assert.equal(reopened.form.display_name.value, result.display_name);
assert.equal(reopened.form.content_tags.value, result.content_tags);
assert.equal(reopened.form.weight.value, 3);
assert.equal(reopened.form.stub_stage_1_peaked.value, true);
assert.equal(reopened.form.stub_stage_2_peaked.value, false);
assert.equal(reopened.onConfirm(defaults(reopened.form)), true);
assert.equal(h.exports[0].display_name, result.display_name);
});
test('invalid Save leaves previously saved fields intact', () => {
const h = createHarness(), dialog = h.open(), result = defaults(dialog.form);
result.display_name = 'Original';
h.setFormResult(result); dialog.onButton(1);
const original = h.project.animationdirector_animdef_definitions;
h.setFormResult({ ...result, display_name: 'Invalid', weight: '0' });
dialog.onButton(1);
assert.equal(h.project.animationdirector_animdef_definitions, original);
assert.equal(h.exports.length, 0);
assert.equal(h.errors.length, 1);
});
test('metadata-only playback stages remain editable and survive definition export', () => { test('metadata-only playback stages remain editable and survive definition export', () => {
const h = createHarness(); const h = createHarness();
h.analysis.stages = h.analysis.stages.slice(0, 1); h.analysis.stages = h.analysis.stages.slice(0, 1);
+26 -1
View File
@@ -107,7 +107,7 @@ test('animation menu orders import and creation actions and has no deprecated sp
const menu = h.menus.find((entry) => entry.id === 'animationdirector_anims'); const menu = h.menus.find((entry) => entry.id === 'animationdirector_anims');
assert.deepEqual(Array.from(menu.children, (action) => action.name), [ assert.deepEqual(Array.from(menu.children, (action) => action.name), [
'Automatically Import Animation', 'Manual Import', 'Actor Preferences', 'Automatically Import Animation', 'Manual Import', 'Actor Preferences',
'Replace Actor Model', 'Block Requirements', 'Item Props', 'Sound Library...', 'Create Animation Definition', 'Export Animations', 'Replace Actor Model', 'Block Requirements', 'Item Props', 'NoN Sound Library', 'Create Animation Definition', 'Export Animations',
]); ]);
assert.deepEqual(Array.from(menu.children[1].children, (action) => action.name), [ assert.deepEqual(Array.from(menu.children[1].children, (action) => action.name), [
'Import Actor', 'Import Animation', 'Import Animation Definition', 'Pack Folders', 'Import Actor', 'Import Animation', 'Import Animation Definition', 'Pack Folders',
@@ -171,6 +171,31 @@ test('import prefills generator fields, builds guides and retains fields without
assert.equal(h.errors.length, 0); assert.equal(h.errors.length, 0);
}); });
test('importing an animdef replaces saved fields instead of retaining old settings', () => {
const h = harness();
h.project.multiactor_animdef_animators = 'Old author';
h.project.animationdirector_animdef_definitions = JSON.stringify({ test: {
display_name: 'Old', weight: 8, content_tags: ['old'], addon_data: { old: true },
actors: [{ label: 'player', entity_types: ['minecraft:player'] }, { label: 'wolf', entity_types: ['minecraft:wolf'] }],
stages: [{ stage: 1, non_peak: true }, { stage: 2 }],
}, other: { display_name: 'Unchanged' } });
const replacement = { display_name: 'Replacement',
actors: [{ label: 'player', entity_types: ['minecraft:player'] }, { label: 'wolf', entity_types: ['minecraft:wolf'] }],
stages: [{ stage: 1 }, { stage: 2, non_peak: true }],
};
h.applyAnimationDefinitionImport({ id: 'test' }, importAnalysis(), replacement);
const saved = h.getSavedAnimdef('test');
assert.equal(saved.display_name, 'Replacement');
assert.equal(saved.weight, undefined);
assert.equal(saved.content_tags, undefined);
assert.equal(saved.addon_data, undefined);
assert.equal(h.getSavedAnimdef('other').display_name, 'Unchanged');
const dialog = h.dialogs.at(-1);
assert.equal(dialog.form.stub_stage_1_peaked.value, false);
assert.equal(dialog.form.stub_stage_2_peaked.value, true);
assert.equal(dialog.form.animators.value, '');
});
test('import maps explicit labels before positional fallbacks and updates actor references', () => { test('import maps explicit labels before positional fallbacks and updates actor references', () => {
const h = harness(); const h = harness();
const source = { actors: [{ label: 'wolf', injector: 'V', injects_into: 'bottom' }, const source = { actors: [{ label: 'wolf', injector: 'V', injects_into: 'bottom' },
+39
View File
@@ -135,6 +135,45 @@ test('mapping retains cue IDs and manual previews, updates managed files and han
assert.equal(points[0].file, ''); assert.equal(points[0].file, '');
}); });
test('reactive impact uses saved peak-stage metadata separately for each animation stage', (t) => {
const h = harness();
h.storage.set(h.SOUND_SOURCE_KEY, assets(t, { impactdry01: { sounds: ['needsofnature:dry'] },
shot_in01: { sounds: ['needsofnature:peak'] } }, { dry: 'dry', peak: 'peak' }));
const library = h.loadSoundLibrary();
h.context.Project = { animationdirector_animdef_definitions: JSON.stringify({ test: {
stages: [{ stage: 1 }, { stage: 2, non_peak: true }],
} }) };
const normal = { name: 'test.p1', animators: { effects: { sound: [{ data_points: [{ effect: 'reactiveimpact' }] }] } } };
const peaked = { name: 'test.p2', animators: { effects: { sound: [{ data_points: [{ effect: 'reactiveimpact' },
{ effect: 'reactiveimpact', file: 'manual.ogg' }, { effect: 'reactiveimpact_silent' }] }] } } };
h.mapAnimationSounds([normal, peaked]);
assert.equal(normal.animators.effects.sound[0].data_points[0].file, h.resolveSoundAudio('impactdry01'));
assert.equal(peaked.animators.effects.sound[0].data_points[0].file, h.resolveSoundAudio('shot_in01'));
assert.equal(peaked.animators.effects.sound[0].data_points[1].file, 'manual.ogg');
assert.equal(peaked.animators.effects.sound[0].data_points[2].file, '');
h.context.Animation.selected = peaked;
h.showSoundLibrary();
h.context.dialog.component.methods.preview.call({ selected: 'reactiveimpact', error: '' });
assert.equal(h.context.played[0].file, h.resolveSoundAudio('shot_in01'));
h.context.Project.animationdirector_animdef_definitions = JSON.stringify({ test: { stages: [{ stage: 2, non_peak: false }] } });
h.mapAnimationSounds([peaked]);
assert.equal(peaked.animators.effects.sound[0].data_points[0].file, h.resolveSoundAudio('impactdry01'));
assert.equal(h.resolveSoundAudio('random(needsofnature:reactiveimpact)', library, new Set(), normal), h.resolveSoundAudio('impactdry01'));
});
test('missing stage metadata defaults to a normal impact; reused peak stages do not change the source stage', (t) => {
const h = harness();
h.storage.set(h.SOUND_SOURCE_KEY, assets(t, { impactdry01: { sounds: ['needsofnature:dry'] },
shot_in01: { sounds: ['needsofnature:peak'] } }, { dry: 'dry', peak: 'peak' }));
const library = h.loadSoundLibrary();
h.context.Project = { animationdirector_animdef_definitions: JSON.stringify({ test: {
stages: [{ stage: 1 }, { stage: 2, use_stage: 1, non_peak: true }],
} }) };
assert.equal(h.resolveSoundAudio('reactiveimpact', library, new Set(), { name: 'test.p1' }), h.resolveSoundAudio('impactdry01'));
assert.equal(h.resolveSoundAudio('reactiveimpact', library, new Set(), { name: 'test.p2' }), h.resolveSoundAudio('shot_in01'));
assert.equal(h.resolveSoundAudio('reactiveimpact', library, new Set(), { name: 'other.p3' }), h.resolveSoundAudio('impactdry01'));
});
test('insertion is undoable and preserves existing cues at the same time; Apply edits selection', (t) => { test('insertion is undoable and preserves existing cues at the same time; Apply edits selection', (t) => {
const h = harness(); const h = harness();
h.storage.set(h.SOUND_SOURCE_KEY, assets(t, { one: { sounds: ['needsofnature:one'] } }, { one: 'one' })); h.storage.set(h.SOUND_SOURCE_KEY, assets(t, { one: { sounds: ['needsofnature:one'] } }, { one: 'one' }));