tweaks and improvements
This commit is contained in:
+59
-26
@@ -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;
|
||||||
|
if (exportFile) {
|
||||||
confirmUnresolvedActorTypes(unresolvedLabels, () =>
|
confirmUnresolvedActorTypes(unresolvedLabels, () =>
|
||||||
exportAnimationDefinitionStub(group, definition, actorMetadata)
|
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,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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' },
|
||||||
|
|||||||
@@ -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' }));
|
||||||
|
|||||||
Reference in New Issue
Block a user