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_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'];
const SOUND_EVENTS = ['select_animation', 'load_project', 'select_project', 'update_keyframe_selection', 'undo', 'redo'];
let soundLibraryAction, soundLibraryDialog, soundLibraryListener, soundPreviewProperty, soundAudition;
let soundLibrary, soundLibraryGeneration = 0;
const soundAudioUrls = new Map(), soundMappedPoints = new WeakMap();
@@ -2826,11 +2826,19 @@
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 };
setProjectActorAnimdefMetadata(mergedMetadata);
setProjectAnimdefAnimators(definition.animators || []);
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(
{
type: 'AFW Animation Definition',
@@ -2981,6 +2989,7 @@
const props = importPropPreviews(group.id, definition, sourcePaths, readPropPreviews());
cancelBlockRequirementUpdate();
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);
void refreshPropPreviews(true);
if (blockRequirementPanel?.vue) blockRequirementPanel.vue.load(group.id);
@@ -3041,7 +3050,7 @@
type: 'info',
text:
`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) {
@@ -3071,7 +3080,7 @@
form.animators = {
label: 'Animators',
type: 'text',
value: (saved.animators || getProjectAnimdefAnimators()).join(', '),
value: (saved.animators || (Object.keys(saved).length ? [] : getProjectAnimdefAnimators())).join(', '),
placeholder: 'Animator One, Animator Two',
description: 'Optional comma-separated individual animation credits. Saved in this Blockbench project.',
};
@@ -3363,15 +3372,7 @@
};
});
const dialog = new Dialog({
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) => {
const submit = (result, exportFile) => {
try {
const actorMetadata = {};
const actors = [];
@@ -3525,9 +3526,14 @@
}
definition.actors = actors;
definition.stages = stages;
confirmUnresolvedActorTypes(unresolvedLabels, () =>
exportAnimationDefinitionStub(group, definition, actorMetadata)
);
if (exportFile) {
confirmUnresolvedActorTypes(unresolvedLabels, () =>
exportAnimationDefinitionStub(group, definition, actorMetadata)
);
} else {
saveAnimationDefinitionFields(group, definition, actorMetadata);
Blockbench.showQuickMessage(`Saved animation definition fields for ${group.id}.`);
}
return true;
} catch (e) {
Blockbench.showMessageBox({
@@ -3537,6 +3543,21 @@
});
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();
@@ -5413,28 +5434,36 @@
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 bare = text.replace(/^(?:needsofnature|minecraft):/, '');
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' };
if (semantic[bare.toLowerCase()]) return `needsofnature:${semantic[bare.toLowerCase()]}`;
const id = text.includes(':') ? text : `needsofnature:${text}`;
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())) {
const match = String(cue).trim().match(/^random\((.*)\)$/i);
if (!match) return null;
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;
}
return null;
}
const id = soundCueId(cue);
const id = soundCueId(cue, animation);
if (!id || visiting.has(id) || visiting.size >= 16) return null;
const event = library.events.get(id);
if (!event) return null;
@@ -5446,7 +5475,7 @@
const resource = importResourceId(name);
if (!resource) continue;
if (object.type === 'event') {
const audio = resolveSoundAudio(name, library, next);
const audio = resolveSoundAudio(name, library, next, animation);
if (audio) return audio;
continue;
}
@@ -5468,15 +5497,18 @@
function mapAnimationSounds(animations) {
for (const animation of animations || []) {
const peaked = isPeakedSoundStage(animation);
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}:${point.effect}:${point.sound}:${point.file}`;
const signature = `${soundLibraryGeneration}:${peaked}:${point.effect}:${point.sound}:${point.file}`;
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[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();
ensureSoundLibrary();
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,
component: {
data() { return { query: '', category: 'all', selected: '', page: 0,
@@ -5549,7 +5581,7 @@
},
preview() {
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; }
soundAudition = new Audio(file);
soundAudition.volume = Math.max(0, Math.min(1, typeof settings === 'undefined' ? 1 : (settings.volume?.value ?? 100) / 100));
@@ -5638,6 +5670,7 @@
function beginFiguraSoundPlayback() {
installFiguraSoundCompat();
if (typeof Animation === 'undefined') return;
mapAnimationSounds(Animation.all);
for (const animation of Animation.all) {
const effects = animation.animators?.effects;
if (effects) for (const patch of figuraSoundPatches) {
@@ -5661,7 +5694,7 @@
const panel = document.getElementById('panel_keyframe');
if (panel && !document.getElementById('animationdirector_sound_library_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.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', {
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.',
click: showSoundLibrary,
});