Files
AnimationDirector-Blockbenc…/test/animdef-generator.test.js
T
2026-09-28 22:39:00 +02:00

236 lines
10 KiB
JavaScript

const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const vm = require('node:vm');
function createHarness() {
const file = path.join(__dirname, '..', 'AnimationDirector.js');
const source = fs.readFileSync(file, 'utf8').replace(
' Plugin.register(PLUGIN_ID, {',
' globalThis.adTest = { showAnimationDefinitionReviewDialog };\n Plugin.register(PLUGIN_ID, {'
);
const dialogs = [];
const exports = [];
const errors = [];
const project = { multiactor_actor_names: JSON.stringify({ 1: 'player', 2: 'wolf' }) };
const context = {
Project: project,
Plugin: { register() {} },
Dialog: class {
constructor(options) { this.options = options; dialogs.push(options); }
show() {}
},
Blockbench: {
export(options, callback) { exports.push(JSON.parse(options.content)); callback(); },
showMessageBox(options) { errors.push(options.message); },
showQuickMessage() {},
},
console,
};
vm.runInNewContext(source, context, { filename: file });
const group = { id: 'test_animation' };
const analysis = {
actorIndices: [1, 2],
actorLabels: new Map([[1, 'player'], [2, 'wolf']]),
stages: [
{ stageNumber: 1, name: 'test_animation.p1', loop: true, cycleSeconds: 1 },
{ stageNumber: 2, name: 'test_animation.p2', loop: false, cycleSeconds: 2 },
],
warnings: [],
};
function open() {
context.adTest.showAnimationDefinitionReviewDialog(group, analysis);
return dialogs.at(-1);
}
return { open, exports, errors, project, analysis };
}
function defaults(form) {
return Object.fromEntries(Object.entries(form)
.filter(([, field]) => field && typeof field === 'object' && 'value' in field)
.map(([key, field]) => [key, field.value]));
}
test('metadata-only playback stages remain editable and survive definition export', () => {
const h = createHarness();
h.analysis.stages = h.analysis.stages.slice(0, 1);
h.project.animationdirector_animdef_definitions = JSON.stringify({ test_animation: {
actors: [{ label: 'player', entity_types: ['minecraft:player'] }, { label: 'wolf', entity_types: ['minecraft:wolf'] }],
stages: [{ stage: 1, loop: true }, { stage: 2, use_stage: 1, speed: 0.8, non_peak: true, escapable: false }],
} });
const dialog = h.open();
assert.equal(dialog.form.stub_stage_2_use_stage.value, '1');
dialog.onConfirm(defaults(dialog.form));
assert.deepEqual(h.errors, []);
assert.equal(h.exports[0].stages.length, 2);
assert.equal(h.exports[0].stages[1].use_stage, 1);
assert.equal(h.exports[0].stages[1].speed, 0.8);
assert.equal(h.analysis.stages.length, 1);
});
test('prop tool metadata survives generator export including reused-stage overrides', () => {
const h = createHarness();
h.project.animationdirector_animdef_definitions = JSON.stringify({ test_animation: {
actors: [{label:'player',entity_types:['minecraft:player'],prop_right:'minecraft:carrot'},
{label:'wolf',entity_types:['minecraft:wolf'],prop_floor:'minecraft:stone'}],
stages: [{stage:1,props:{player:{prop_left:'minecraft:stick'}}},
{stage:2,use_stage:1,props:{player:{prop_right:null},wolf:null}}],
} });
const dialog=h.open(); dialog.onConfirm(defaults(dialog.form));
assert.deepEqual(h.errors,[]);
const def=h.exports[0];
assert.equal(def.actors[0].prop_right,'minecraft:carrot');
assert.equal(def.actors[1].prop_floor,'minecraft:stone');
assert.equal(def.stages[0].props.player.prop_left,'minecraft:stick');
assert.equal(def.stages[1].props.player.prop_right,null);
assert.equal(def.stages[1].props.wolf,null);
});
test('generates extended animdef fields and restores them on reopen', () => {
const harness = createHarness();
const dialog = harness.open();
const result = defaults(dialog.form);
result.stub_actor_1_injector = 'V';
result.stub_actor_1_receiver = true;
result.stub_actor_1_injects_into = ['wolf'];
result.stub_actor_1_held_items = 'minecraft:carrot, #minecraft:swords';
result.stub_actor_1_held_hand = 'either';
result.stub_actor_1_held_prop_slot = 'right';
result.stub_actor_2_receiver = true;
result.system_tags = 'manualpeak';
result.content_tags = 'test, tag';
result.required_union_tags = 'example.tag';
result.join_after = 'creator:previous';
result.match_content_on_join = true;
result.weight = '2.5';
result.attack_eligible = false;
result.manual_peak_priority = '10';
result.liquid_gain_multiplier = '0.5';
result.base_speed = '1.25';
result.water = 'surface';
result.position_anchor_actor = 'wolf';
result.camera_torso_tracking = 'continuous';
result.camera_block_collision = 'ignore';
result.stub_stage_2_use_stage = '1';
assert.equal(dialog.onConfirm(result), true);
assert.equal(harness.errors.length, 0);
assert.equal(harness.exports.length, 1);
const definition = harness.exports[0];
assert.equal(definition.weight, 2.5);
assert.equal(definition.attack_eligible, false);
assert.deepEqual(Array.from(definition.system_tags), ['manualpeak']);
assert.deepEqual(Array.from(definition.content_tags), ['test', 'tag']);
assert.deepEqual(Array.from(definition.join_after), ['creator:previous']);
assert.equal(definition.manual_peak.priority, 10);
assert.equal(definition.actors[0].injects_into, 'wolf');
assert.equal(definition.actors[0].receiver, true);
assert.equal(definition.actors[0].held_item.prop_slot, 'right');
assert.equal('prop_floor' in definition.actors[1], false);
assert.equal(definition.stages[1].use_stage, 1);
assert.equal(definition.camera.torso_tracking, 'continuous');
assert.equal(definition.water, 'surface');
assert.equal(definition.position_anchor_actor, 'wolf');
assert.equal('stage_seconds' in definition, false);
assert.equal('escapable' in definition, false);
const reopened = harness.open();
assert.equal(reopened.form.weight.value, 2.5);
assert.equal(reopened.form.matching_advanced.value, true);
assert.equal(reopened.form.system_tags.value, 'manualpeak');
assert.equal(reopened.form.stub_actor_1_injector.value, 'V');
assert.deepEqual(Array.from(reopened.form.stub_actor_1_injects_into.value), ['wolf']);
assert.equal(reopened.form.stub_stage_2_use_stage.value, '1');
});
test('rejects nonexistent injection targets and playback stages', () => {
const harness = createHarness();
const dialog = harness.open();
const result = defaults(dialog.form);
result.stub_actor_1_injector = 'V';
result.stub_actor_1_injects_into = ['missing'];
assert.equal(dialog.onConfirm(result), false);
assert.match(harness.errors.at(-1), /invalid injection target/);
result.stub_actor_1_injects_into = ['wolf'];
result.stub_stage_2_use_stage = '99';
assert.equal(dialog.onConfirm(result), false);
assert.match(harness.errors.at(-1), /missing playback stage/);
assert.equal(harness.exports.length, 0);
});
test('groups advanced matching and offers only other actors as injection targets', () => {
const form = createHarness().open().form;
const keys = Object.keys(form);
assert.equal(form.matching_advanced.value, false);
for (const key of ['required_union_tags', 'join_after', 'match_content_on_join', 'weight']) {
assert.equal(form[key].condition({ matching_advanced: false }), false);
assert.equal(form[key].condition({ matching_advanced: true }), true);
assert.ok(keys.indexOf(key) > keys.indexOf('matching_advanced'));
}
assert.equal(form.stub_actor_1_receiver.label, 'Receiver');
assert.equal(form.stub_actor_1_injects_into.type, 'multi_select');
assert.deepEqual(Object.keys(form.stub_actor_1_injects_into.options), ['wolf']);
assert.deepEqual(Object.keys(form.stub_actor_2_injects_into.options), ['player']);
assert.equal(keys.indexOf('stub_actor_1_injects_into'), keys.indexOf('stub_actor_1_injector') + 1);
assert.equal(keys.some((key) => /_prop_(left|right|floor)$/.test(key)), false);
assert.equal('default_stage_seconds' in form, false);
assert.equal('default_escapable' in form, false);
assert.equal('stub_behavior' in form, false);
assert.equal(form.system_tags.type, 'select');
assert.deepEqual(Object.keys(form.system_tags.options), [
'', 'manualpeak', 'defeated', 'stuck', 'tease', 'birth', 'fillbottle', 'other',
]);
assert.equal(form.stub_stage_2_use_stage.type, 'select');
assert.deepEqual(Object.keys(form.stub_stage_2_use_stage.options), ['1', '2', '']);
});
test('keeps minimal definitions minimal and supports specialized behavior', () => {
const harness = createHarness();
const dialog = harness.open();
const minimal = defaults(dialog.form);
assert.equal(dialog.onConfirm(minimal), true);
const first = harness.exports.at(-1);
for (const key of ['weight', 'camera', 'water', 'manual_peak', 'block_requirements']) {
assert.equal(key in first, false, `${key} should be omitted by default`);
}
const teaseDialog = harness.open();
const tease = defaults(teaseDialog.form);
tease.system_tags = 'tease';
tease.stub_tease_multiplier = 3;
assert.equal(teaseDialog.onConfirm(tease), true);
const second = harness.exports.at(-1);
assert.deepEqual(Array.from(second.system_tags), ['tease']);
assert.equal(second.tease.near_animation_energy_multiplier, 3);
const stuckDialog = harness.open();
const stuck = defaults(stuckDialog.form);
stuck.system_tags = 'stuck';
stuck.content_tags = 'stuckfence';
assert.equal(stuckDialog.onConfirm(stuck), true);
const third = harness.exports.at(-1);
assert.deepEqual(Array.from(third.system_tags), ['stuck']);
assert.deepEqual(Array.from(third.content_tags), ['stuckfence']);
assert.equal(third.stuck.damage_behavior, 'stop_on_damage');
});
test('exports one custom behavior through Other', () => {
const harness = createHarness();
const dialog = harness.open();
const result = defaults(dialog.form);
result.system_tags = 'other';
result.other_system_tags = 'custom_tag';
assert.equal(dialog.onConfirm(result), true);
const definition = harness.exports.at(-1);
assert.deepEqual(Array.from(definition.system_tags), ['custom_tag']);
assert.equal('tease' in definition, false);
assert.equal('stuck' in definition, false);
const reopened = harness.open();
assert.equal(reopened.form.system_tags.value, 'other');
assert.equal(reopened.form.other_system_tags.value, 'custom_tag');
assert.equal(reopened.form.other_system_tags.condition({ system_tags: 'other' }), true);
assert.equal(reopened.form.other_system_tags.condition({ system_tags: '' }), false);
});