added a way to auto import full animations
This commit is contained in:
@@ -7,7 +7,7 @@ const vm = require('node:vm');
|
||||
function harness() {
|
||||
const source = fs.readFileSync(path.join(__dirname, '..', 'AnimationDirector.js'), 'utf8').replace(
|
||||
' Plugin.register(PLUGIN_ID, {',
|
||||
' globalThis.blockTest = { readBlockRequirementForm, buildBlockRequirementGuideBoxes, stripBlockRequirementGuideBones, showAnimationDefinitionReviewDialog, rebuildBlockRequirementGuides, getAuthoredBlockRequirement, compileGeckoModelJson, createBlockRequirementPanel, hideInternalBlockGuideFaces, refreshBlockGuideFaces, getPanel: () => blockRequirementPanel };\n Plugin.register(PLUGIN_ID, {'
|
||||
' globalThis.blockTest = { readBlockRequirementForm, buildBlockRequirementGuideBoxes, stripBlockRequirementGuideBones, showAnimationDefinitionReviewDialog, rebuildBlockRequirementGuides, getAuthoredBlockRequirement, compileGeckoModelJson, createBlockRequirementPanel, hideInternalBlockGuideFaces, refreshBlockGuideFaces, prepareAnimationDefinitionImport, applyAnimationDefinitionImport, importAnimationDefinition, getSavedAnimdef, getPanel: () => blockRequirementPanel };\n Plugin.register(PLUGIN_ID, {'
|
||||
);
|
||||
const project = { uuid: 'project-one', multiactor_actor_names: JSON.stringify({ 1: 'player', 2: 'wolf' }) };
|
||||
const dialogs = [];
|
||||
@@ -18,6 +18,7 @@ function harness() {
|
||||
const previewEvents = new Map();
|
||||
const timers = new Map();
|
||||
const previewUpdates = [];
|
||||
const menus = [];
|
||||
let sequence = 0;
|
||||
class Group {
|
||||
static all = [];
|
||||
@@ -45,7 +46,7 @@ function harness() {
|
||||
}
|
||||
const context = {
|
||||
Project: project, Group, Cube, Plugin: { register(id, definition) { context.registration = definition; } },
|
||||
Action: class { constructor(id, options) { Object.assign(this, options); } },
|
||||
Action: class { constructor(id, options) { Object.assign(this, options, { id }); } },
|
||||
Panel: class {
|
||||
constructor(id, options) {
|
||||
this.options = options;
|
||||
@@ -55,7 +56,7 @@ function harness() {
|
||||
},
|
||||
setTimeout(callback) { const id = ++sequence; timers.set(id, callback); return id; },
|
||||
clearTimeout(id) { timers.delete(id); },
|
||||
MenuBar: { menus: { tools: { addAction() {} } }, update() {} },
|
||||
MenuBar: { menus: { tools: { addAction(menu) { menus.push(menu); } } }, update() {} },
|
||||
THREE: { MeshBasicMaterial: class { constructor(options) { Object.assign(this, options); } }, DoubleSide: 2 },
|
||||
Canvas: {
|
||||
updateAll() {
|
||||
@@ -85,7 +86,7 @@ function harness() {
|
||||
console,
|
||||
};
|
||||
vm.runInNewContext(source, context);
|
||||
return { ...context.blockTest, context, project, dialogs, exports, errors, edits, events, previewEvents, timers, previewUpdates, Group, Cube };
|
||||
return { ...context.blockTest, context, project, dialogs, exports, errors, edits, events, previewEvents, timers, previewUpdates, menus, Group, Cube };
|
||||
}
|
||||
|
||||
function wallForm(overrides = {}) {
|
||||
@@ -93,6 +94,139 @@ function wallForm(overrides = {}) {
|
||||
facing: '', clearance_width: 1, clearance_height: 2, clearance_depth: 1, wall_thickness: 1, ...overrides };
|
||||
}
|
||||
|
||||
function importAnalysis() {
|
||||
return { actorIndices: [1, 2], actorLabels: new Map([[1, 'player'], [2, 'wolf']]),
|
||||
stages: [{ stageNumber: 1, name: 'test.p1', loop: true, cycleSeconds: 1 },
|
||||
{ stageNumber: 2, name: 'test.p2', loop: false, cycleSeconds: 2 }], warnings: [] };
|
||||
}
|
||||
|
||||
test('animation menu orders import and creation actions and has no deprecated split-file tools', () => {
|
||||
const h = harness();
|
||||
h.context.registration.onload();
|
||||
const menu = h.menus.find((entry) => entry.id === 'animationdirector_anims');
|
||||
assert.deepEqual(Array.from(menu.children, (action) => action.name), [
|
||||
'Automatically Import Animation', 'Manual Import', 'Actor Preferences',
|
||||
'Replace Actor Model', 'Export Animations', 'Block Requirements', 'Create Animation Definition',
|
||||
]);
|
||||
assert.deepEqual(Array.from(menu.children[1].children, (action) => action.name), [
|
||||
'Import Actor', 'Import Animation', 'Import Animation Definition', 'Pack Folders',
|
||||
]);
|
||||
const source = fs.readFileSync(path.join(__dirname, '..', 'AnimationDirector.js'), 'utf8');
|
||||
assert.doesNotMatch(source, /importSplitActorAnimation|buildSplitImportFile|analyzeSplitAnimationFiles|exportSelectedActorAnimation|exportAllActorAnimations|animationdirector_deprecated/);
|
||||
});
|
||||
|
||||
test('import prefills generator fields, builds guides and retains fields without UI controls on export', () => {
|
||||
const h = harness();
|
||||
const source = {
|
||||
display_name: 'Imported animation', animators: ['Animator'], content_tags: ['test'],
|
||||
system_tags: ['manualpeak'], weight: 2.5, required_union_tags: ['example.tag'],
|
||||
join_after: ['example:previous'], match_content_on_join: true, attack_eligible: false,
|
||||
liquid_gain_multiplier: 0.5, manual_peak: { priority: 5 }, speed: 1.5,
|
||||
water: 'surface', position_anchor_actor: 'wolf',
|
||||
camera: { torso_tracking: 'continuous', block_requirement_collision: 'ignore' },
|
||||
addon_data: { example: { flag: true } }, escapable: false,
|
||||
actors: [
|
||||
{ label: 'player', entity_types: ['minecraft:player'], activity: 'passive', receiver: true, age: 'adult' },
|
||||
{ label: 'wolf', entity_types: ['minecraft:wolf'], actor_tags: ['gender.male'], activity: 'active',
|
||||
injector: 'V', injects_into: 'player', held_item: { items: ['#minecraft:swords'], hand: 'either', prop_slot: 'right' } },
|
||||
],
|
||||
stages: [
|
||||
{ stage: 1, loop: true, cycle_seconds: 0.75, allow_join: false, escapable: true,
|
||||
speed: 1.25, stage_duration_multiplier: 0.5, cycle_midpoint_offset_seconds: 0.1,
|
||||
props: { right: { item: 'minecraft:carrot' } } },
|
||||
{ stage: 2, loop: false, cycle_seconds: 1.75, allow_join: true, escapable: false, non_peak: true },
|
||||
],
|
||||
block_requirements: { type: 'wall', blocks: '#minecraft:fences', height: { min: 3 },
|
||||
width: { min: 2, max: 2, both_axes: true }, facing: 'front', clearance: { width: 1, height: 2, depth: 1 } },
|
||||
};
|
||||
h.applyAnimationDefinitionImport({ id: 'test' }, importAnalysis(), source);
|
||||
const dialog = h.dialogs.at(-1);
|
||||
const values = Object.fromEntries(Object.entries(dialog.form)
|
||||
.filter(([, field]) => field && typeof field === 'object' && 'value' in field).map(([key, field]) => [key, field.value]));
|
||||
assert.equal(values.display_name, source.display_name);
|
||||
assert.equal(values.animators, 'Animator');
|
||||
assert.equal(values.weight, 2.5);
|
||||
assert.equal(values.system_tags, 'manualpeak');
|
||||
assert.equal(values.manual_peak_priority, 5);
|
||||
assert.equal(values.stub_actor_1_receiver, true);
|
||||
assert.equal(values.stub_actor_2_injector, 'V');
|
||||
assert.deepEqual(Array.from(values.stub_actor_2_injects_into), ['player']);
|
||||
assert.equal(values.stub_actor_2_held_items, '#minecraft:swords');
|
||||
assert.equal(values.stub_actor_2_held_hand, 'either');
|
||||
assert.equal(values.stub_stage_1_duration_multiplier, 0.5);
|
||||
assert.equal(values.stub_stage_1_cycle_seconds, 0.75);
|
||||
assert.equal(values.stub_stage_2_peaked, true);
|
||||
assert.ok(h.Cube.all.length > 0);
|
||||
assert.equal(h.edits.length, 1);
|
||||
assert.equal(h.edits[0].action, 'Import animation definition');
|
||||
assert.equal(h.edits[0].aspects.animdef_edit, true);
|
||||
assert.equal(dialog.onConfirm(values), true);
|
||||
const exported = h.exports.at(-1);
|
||||
assert.deepEqual(exported.stages[0].props, source.stages[0].props);
|
||||
assert.deepEqual(exported.addon_data, source.addon_data);
|
||||
assert.equal(exported.actors[0].age, 'adult');
|
||||
assert.equal(exported.actors[1].injects_into, 'player');
|
||||
assert.equal(exported.block_requirements.width.both_axes, true);
|
||||
assert.equal(h.errors.length, 0);
|
||||
});
|
||||
|
||||
test('import maps explicit labels before positional fallbacks and updates actor references', () => {
|
||||
const h = harness();
|
||||
const source = { actors: [{ label: 'wolf', injector: 'V', injects_into: 'bottom' },
|
||||
{ label: 'bottom', receiver: true }], stages: [{ stage: 1 }, { stage: 2 }], position_anchor_actor: 'bottom' };
|
||||
const definition = h.prepareAnimationDefinitionImport(source, importAnalysis());
|
||||
assert.equal(definition.actors[0].label, 'player');
|
||||
assert.equal(definition.actors[0].receiver, true);
|
||||
assert.equal(definition.actors[1].label, 'wolf');
|
||||
assert.deepEqual(Array.from(definition.actors[1].injects_into), ['player']);
|
||||
assert.equal(definition.position_anchor_actor, 'player');
|
||||
assert.equal(source.actors[1].label, 'bottom', 'Input JSON is not mutated');
|
||||
const positional = h.prepareAnimationDefinitionImport({ actors: [{ receiver: true }, { injector: 'V' }],
|
||||
stages: [{ stage: 1 }, { stage: 2 }] }, importAnalysis());
|
||||
assert.equal(positional.actors[0].label, 'player');
|
||||
assert.equal(positional.actors[1].label, 'wolf');
|
||||
});
|
||||
|
||||
test('invalid imports leave definitions and guides unchanged', () => {
|
||||
const h = harness();
|
||||
h.rebuildBlockRequirementGuides('test', h.readBlockRequirementForm(wallForm()));
|
||||
const before = h.project.animationdirector_block_requirements;
|
||||
const source = { actors: [{}, {}], stages: [{ stage: 1 }, { stage: 2 }] };
|
||||
for (const invalid of [null, { animations: {} }, { ...source, actors: [{}] },
|
||||
{ ...source, stages: [{ stage: 1 }] }, { ...source, stages: [{ stage: 1 }, { stage: 1 }] },
|
||||
{ ...source, actors: [{ label: 'same' }, { label: 'same' }] },
|
||||
{ ...source, actors: [{ injects_into: 'unknown' }, {}] },
|
||||
{ ...source, content_tags: 'wrong' },
|
||||
{ ...source, block_requirements: { type: 'wall', width: { min: 2, max: 1 } } }]) {
|
||||
assert.throws(() => h.applyAnimationDefinitionImport({ id: 'test' }, importAnalysis(), invalid));
|
||||
}
|
||||
assert.equal(h.project.animationdirector_block_requirements, before);
|
||||
assert.equal(h.project.animationdirector_animdef_definitions, undefined);
|
||||
assert.equal(h.edits.length, 1);
|
||||
});
|
||||
|
||||
test('import without a requirement removes old guides and undo restores definition metadata too', () => {
|
||||
const h = harness();
|
||||
h.context.registration.onload();
|
||||
h.rebuildBlockRequirementGuides('test', h.readBlockRequirementForm(wallForm()));
|
||||
const before = {};
|
||||
h.events.get('create_undo_save')({ save: before, aspects: { block_requirement_edit: true, animdef_edit: true } });
|
||||
h.applyAnimationDefinitionImport({ id: 'test' }, importAnalysis(), {
|
||||
actors: [{}, {}], stages: [{ stage: 1 }, { stage: 2 }], animation_tags: ['defeat'] });
|
||||
assert.equal(h.Cube.all.length, 0);
|
||||
assert.equal(h.getAuthoredBlockRequirement('test').requirement, null);
|
||||
assert.deepEqual(Array.from(h.getSavedAnimdef('test').system_tags), ['defeat']);
|
||||
assert.equal(h.dialogs.at(-1).form.stub_stage_2_peaked.value, false, 'Missing peak flags are not invented');
|
||||
const after = {};
|
||||
h.events.get('create_undo_save')({ save: after, aspects: { block_requirement_edit: true, animdef_edit: true } });
|
||||
h.events.get('load_undo_save')({ save: before });
|
||||
assert.equal(h.getAuthoredBlockRequirement('test').requirement.type, 'wall');
|
||||
assert.equal(Object.keys(h.getSavedAnimdef('test')).length, 0);
|
||||
h.events.get('load_undo_save')({ save: after });
|
||||
assert.equal(h.getAuthoredBlockRequirement('test').requirement, null);
|
||||
assert.equal(h.getSavedAnimdef('test').actors.length, 2);
|
||||
});
|
||||
|
||||
test('building, editing, refreshing and removing guides never resets actor or folder texture overrides', () => {
|
||||
const h = harness();
|
||||
const actor = new h.Group({ name: 'actor1_body' });
|
||||
|
||||
Reference in New Issue
Block a user