This commit is contained in:
2026-10-01 01:35:33 +02:00
parent e7bb16df64
commit a249f1bbdd
2 changed files with 105 additions and 16 deletions
+58 -13
View File
@@ -149,6 +149,37 @@
setProjectStringMap(ACTOR_NAMES_PROPERTY, names);
}
function remapActorReferences(previousNames, nextNames, indices, metadata) {
const labels = new Map(indices.map((index) => [previousNames[index] || `actor${index}`,
nextNames[index] || `actor${index}`]));
const nextLabels = [...labels.values()];
if (new Set(nextLabels).size !== nextLabels.length) throw new Error('Actor export names must be unique.');
const rename = (label) => labels.get(label) ?? label;
const targets = (value) => Array.isArray(value) ? value.map(rename) : typeof value === 'string' ? rename(value) : value;
for (const entry of Object.values(metadata)) {
if (entry.injects_into) entry.injects_into = targets(entry.injects_into);
}
const definitions = JSON.parse(Project[ANIMDEF_DEFINITIONS_PROPERTY] || '{}');
for (const definition of Object.values(definitions)) {
for (const actor of definition.actors || []) {
actor.label = rename(actor.label);
if (actor.injects_into) actor.injects_into = targets(actor.injects_into);
}
if (definition.position_anchor_actor) definition.position_anchor_actor = rename(definition.position_anchor_actor);
for (const stage of definition.stages || []) {
if (stage.props && typeof stage.props === 'object') {
stage.props = Object.fromEntries(Object.entries(stage.props).map(([label, value]) => [rename(label), value]));
}
}
}
const previews = Object.fromEntries(Object.values(readPropPreviews()).map((entry) => {
const updated = { ...entry, actor: rename(entry.actor) };
return [propEntryKey(updated.animation, updated.actor, updated.slot, updated.stage), updated];
}));
Project[ANIMDEF_DEFINITIONS_PROPERTY] = JSON.stringify(definitions);
Project[PROP_PREVIEWS_PROPERTY] = JSON.stringify(previews);
}
function getProjectActorTextures() {
return getProjectStringMap(ACTOR_TEXTURES_PROPERTY);
}
@@ -392,6 +423,9 @@
for (const label of actorLabels) {
if (label !== actorLabel) targetOptions[label] = label;
}
for (const label of metadata.injects_into || []) {
if (!Object.hasOwn(targetOptions, label)) targetOptions[label] = `${label} (unavailable - remove or replace)`;
}
form[`${base}_heading`] = {
type: 'info',
text: `#### Actor ${actorIndex} (${actorLabel})`,
@@ -1732,12 +1766,14 @@
const aspects = { elements: Cube.all.slice(), uv_mode: true, actor_preferences_edit: true };
Undo.initEdit(aspects);
editing = true;
remapActorReferences(names, outNames, actorIndices, outAnimdefMetadata);
setProjectActorNames(outNames);
setProjectActorTextures(outTextures);
setProjectActorAnimdefMetadata(outAnimdefMetadata);
setProjectBoneTextureOverrides(outBoneTextureOverrides);
applyAllActorTexturePreviews(true);
Undo.finishEdit('Edit Actor Preferences', { ...aspects, elements: Cube.all.slice() });
void refreshPropPreviews(true);
return true;
} catch (e) {
if (editing) Undo.cancelEdit();
@@ -3371,9 +3407,9 @@
const cycleSeconds = parseRequiredPositiveNumber(result?.[`${base}_cycle_seconds`], `Stage p${stage.stageNumber} cycle seconds`);
const speed = parseOptionalNumber(result?.[`${base}_speed`], `Stage p${stage.stageNumber} speed`, (value) => value > 0);
const midpointOffset = parseOptionalNumber(
result?.[`${base}_midpoint_offset`],
loop ? result?.[`${base}_midpoint_offset`] : '',
`Stage p${stage.stageNumber} midpoint offset`,
() => loop
() => true
);
const stageSeconds = parseOptionalNumber(
result?.[`${base}_stage_seconds`],
@@ -3381,12 +3417,12 @@
(value) => Number.isInteger(value) && (value === -1 || (value >= 1 && value <= 300))
);
const durationMultiplier = parseOptionalNumber(
result?.[`${base}_duration_multiplier`],
loop ? result?.[`${base}_duration_multiplier`] : '',
`Stage p${stage.stageNumber} duration multiplier`,
(value) => loop && value >= 0.05 && value <= 20
(value) => value >= 0.05 && value <= 20
);
const useStage = parseOptionalNumber(
result?.[`${base}_use_stage`],
result?.[`${base}_use_stage`] === 'own' ? '' : result?.[`${base}_use_stage`],
`Stage p${stage.stageNumber} playback stage`,
(value) => Number.isInteger(value) && value > 0
);
@@ -3403,13 +3439,13 @@
? !!result[`${base}_allow_join`]
: !isLastStage,
};
if (midpointOffset !== null) output.cycle_midpoint_offset_seconds = midpointOffset;
if (loop && midpointOffset !== null) output.cycle_midpoint_offset_seconds = midpointOffset;
if (useStage !== null && useStage !== stage.stageNumber) output.use_stage = useStage;
if (speed !== null) output.speed = speed;
if (peaked) output.non_peak = true;
if (escapable !== 'default') output.escapable = escapable === 'true';
if (stageSeconds !== null) output.stage_seconds = stageSeconds;
if (durationMultiplier !== null) output.stage_duration_multiplier = durationMultiplier;
if (loop && durationMultiplier !== null) output.stage_duration_multiplier = durationMultiplier;
return output;
}
@@ -3829,6 +3865,9 @@
const label = analysis.actorLabels.get(actorIndex);
anchorOptions[label] = label;
}
if (saved.position_anchor_actor && !Object.hasOwn(anchorOptions, saved.position_anchor_actor)) {
anchorOptions[saved.position_anchor_actor] = `${saved.position_anchor_actor} (unavailable - replace)`;
}
form.position_anchor_actor = {
label: 'Position anchor actor', type: 'select', value: saved.position_anchor_actor || '', options: anchorOptions,
description: 'Prefer this actor as the animation placement anchor.',
@@ -3929,15 +3968,18 @@
label: 'Play animation from stage',
type: 'select',
options: Object.fromEntries([
['', `Own stage (p${stage.stageNumber})`],
['own', `Own stage (p${stage.stageNumber})`],
...analysis.stages.map((availableStage) => [
String(availableStage.stageNumber), `Stage p${availableStage.stageNumber}`,
]),
]),
value: savedStage.use_stage == null ? '' : String(savedStage.use_stage),
value: savedStage.use_stage == null ? 'own' : String(savedStage.use_stage),
description: 'Optional stage number whose GeckoLib clip should be played instead.',
condition: showAdvanced,
};
if (savedStage.use_stage != null && !Object.hasOwn(form[`${base}_use_stage`].options, String(savedStage.use_stage))) {
form[`${base}_use_stage`].options[String(savedStage.use_stage)] = `Stage p${savedStage.use_stage} (unavailable - replace)`;
}
form[`${base}_stage_seconds`] = {
label: 'Stage seconds',
type: 'text',
@@ -4041,6 +4083,7 @@
const water = String(result?.water || 'none');
if (water !== 'none') definition.water = water;
const anchor = String(result?.position_anchor_actor || '');
if (anchor && !actorsByLabel.has(anchor)) throw new Error(`Position anchor "${anchor}" is unavailable. Choose another actor or Automatic anchor.`);
if (anchor) definition.position_anchor_actor = anchor;
const camera = unmanagedAnimdefFields(saved.camera, new Set(['torso_tracking', 'block_requirement_collision']));
if (result?.camera_torso_tracking === 'continuous') camera.torso_tracking = 'continuous';
@@ -6656,7 +6699,7 @@
Plugin.register(PLUGIN_ID, {
title: 'AnimationDirector',
author: 'L1Z0',
icon: 'cinematic_blur',
icon: 'diversity_4',
description: 'Multi-actor animation in Blockbench without bone-name clashes.',
about: `AnimationDirector is a Blockbench plugin for creating multi-actor animations for Needs of Nature.
@@ -6671,6 +6714,7 @@ Build, preview, and export multi-actor animations with tools for actor models, a
- Edit block requirements with live previews of required blocks and clearance areas.
- Preview item props using Minecraft assets or custom textures. <sup>1</sup>
- Browse and audition sounds with the NoN Sound Library.
- Browse particle sprites and assign cues and locators with the NoN Particle Library, including approximate timeline previews for supported NoN particles. <sup>1</sup>
- Export multi-actor, multi-stage animations and GeckoLib models.
- Replace actor models while preserving matching animation tracks.
- Transfer added geometry into vanilla CEM presets to create JEM models, with optional energy-dependent visibility. <sup>2</sup>
@@ -6679,11 +6723,11 @@ Build, preview, and export multi-actor animations with tools for actor models, a
**Addon Notes**
<sup>1</sup> Asset Browser - optional; its cached Minecraft assets can be used for textures and item previews.
<sup>1</sup> Asset Browser - optional; its cached Minecraft assets can be used for textures, item previews, and the particle library.
<sup>2</sup> CEM Template Loader - required for JEM creation.
Features without a reference need no additional addon. Whole-animation import and local asset tools require desktop Blockbench. The sound library uses locally available NoN/pack sound files.`,
Features without a reference need no additional addon. Whole-animation import and local asset tools require desktop Blockbench. The sound library uses locally available NoN/pack sound files; NoN particle previews require their texture assets from a NoN JAR or extracted assets folder.`,
version: '2.0',
min_version: '5.2.1',
variant: 'both',
@@ -6821,7 +6865,8 @@ Features without a reference need no additional addon. Whole-animation import an
if (aspects.actor_texture_edit) save[ACTOR_TEXTURES_PROPERTY] = Project[ACTOR_TEXTURES_PROPERTY] || '{}';
if (aspects.actor_preferences_edit) {
for (const property of [ACTOR_NAMES_PROPERTY, ACTOR_TEXTURES_PROPERTY, ACTOR_ANIMDEF_METADATA_PROPERTY,
BONE_TEXTURE_OVERRIDES_PROPERTY, ACTOR_UV_BASIS_PROPERTY]) save[property] = Project[property] || '{}';
BONE_TEXTURE_OVERRIDES_PROPERTY, ACTOR_UV_BASIS_PROPERTY, ANIMDEF_DEFINITIONS_PROPERTY,
PROP_PREVIEWS_PROPERTY]) save[property] = Project[property] || '{}';
}
if (aspects.automatic_animation_import) {
for (const property of IMPORT_METADATA) save[property] = Project[property] ?? (property === ANIMDEF_ANIMATORS_PROPERTY ? '' : '{}');