Files
2026-10-01 02:00:33 +02:00

174 lines
8.9 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 variantBuilder() {
const source = fs.readFileSync(path.join(__dirname, '..', 'AnimationDirector.js'), 'utf8').replace(
' Plugin.register(PLUGIN_ID, {',
' globalThis.variantTest = { buildGenderModelVariant, refreshModelVariantFields, createModelVariantBoneSections, buildJemModelVariant, normalizeGeckoExportName };\n Plugin.register(PLUGIN_ID, {'
);
const context = { Plugin: { register() {} }, console };
vm.runInNewContext(source, context);
return Object.assign(context.variantTest.buildGenderModelVariant, { refresh: context.variantTest.refreshModelVariantFields,
sections: context.variantTest.createModelVariantBoneSections, jem: context.variantTest.buildJemModelVariant,
normalizeName: context.variantTest.normalizeGeckoExportName });
}
test('gender variants omit selected bones, descendants, locators and texture overrides without changing source', () => {
const build = variantBuilder();
const source = { format_version: '1.12.0', 'minecraft:geometry': [{
description: { identifier: 'geometry.example', texture_width: 64 },
bones: [
{ name: 'detail', parent: 'd', cubes: [{ origin: [0, 0, 0] }] },
{ name: 'body', cubes: [{}] },
{ name: 'd', parent: 'body', cubes: [{}], locators: { D: [0, 1, 0] } },
{ name: 'head', parent: 'body', cubes: [{}] },
],
}], afw_bone_textures: { d: 'example:textures/custom.png' } };
const before = JSON.stringify(source);
const female = JSON.parse(JSON.stringify(build(source, ['d'])));
assert.deepEqual(female['minecraft:geometry'][0].bones.map((bone) => bone.name), ['body', 'head']);
assert.deepEqual(female['minecraft:geometry'][0].bones[0].cubes, [{}]);
assert.equal(female.afw_bone_textures, undefined);
assert.equal(JSON.stringify(build(source, [])), before);
assert.equal(JSON.stringify(source), before);
});
test('GeckoLib export filenames strip repeated geo and project extensions', () => {
const normalize = variantBuilder().normalizeName;
for (const name of ['horse', 'horse.geo', 'horse.geo.geo', 'horse.geo.json', 'horse.geo.bbench', 'horse.geo.bbmodel']) {
assert.equal(normalize(name), 'horse');
}
assert.equal(normalize('horse.f.geo.geo.json'), 'horse.f');
assert.equal(normalize('geology'), 'geology');
});
test('variant subtree propagation handles nested children and unknown saved selections', () => {
const build = variantBuilder();
const source = { 'minecraft:geometry': [{ bones: [
{ name: 'grandchild', parent: 'child', cubes: [{}] },
{ name: 'child', parent: 'root', cubes: [{}] },
{ name: 'root', cubes: [{}] },
] }] };
const hidden = build(source, ['root', 'deleted']);
assert.equal(hidden['minecraft:geometry'][0].bones.length, 0);
});
test('selected hidden parents disable and clear descendant selections separately for each gender', () => {
const build = variantBuilder();
const bones = [{ name: 'body' }, { name: 'child', parent: 'body' }, { name: 'tip', parent: 'child' }];
const fields = {}, updates = [];
for (const gender of ['m', 'f', 'mf']) for (let index = 0; index < bones.length; index++) {
const input = { disabled: false };
fields[`hide_${gender}_${index}`] = { input, bar: { style: {}, querySelectorAll: () => [input] } };
}
const dialog = { form: { form_data: fields, setValues: (values) => updates.push(values) } };
const values = { hide_m_0: true, hide_m_1: true, hide_m_2: true, hide_f_1: true, hide_mf_0: true, hide_mf_2: true };
build.refresh(dialog, bones, values);
assert.equal(values.hide_m_1, false);
assert.equal(values.hide_m_2, false);
assert.equal(fields.hide_m_1.input.disabled, true);
assert.equal(fields.hide_m_2.input.disabled, true);
assert.equal(fields.hide_f_1.input.disabled, false);
assert.equal(fields.hide_f_2.input.disabled, true);
assert.equal(values.hide_mf_2, false);
assert.equal(fields.hide_mf_2.input.disabled, true);
assert.match(fields.hide_m_2.bar.title, /Already excluded/);
values.hide_m_0 = false;
build.refresh(dialog, bones, values);
assert.equal(fields.hide_m_2.input.disabled, false);
});
test('each gender bone list gets an independent bounded scroll container', () => {
const build = variantBuilder();
const nodes = [];
const document = { createElement() {
return { style: {}, children: [], appendChild(child) {
nodes.splice(nodes.indexOf(child), 1);
this.children.push(child);
child.parentNode = this;
} };
} };
const parent = { insertBefore(node, reference) { nodes.splice(nodes.indexOf(reference), 0, node); } };
const fields = {};
for (const gender of ['m', 'f', 'mf']) for (let index = 0; index < 2; index++) {
const bar = { parentNode: parent, ownerDocument: document, style: {}, querySelectorAll: () => [] };
fields[`hide_${gender}_${index}`] = { bar };
nodes.push(bar);
}
const dialog = { form: { form_data: fields } };
const bones = [{ name: 'body' }, { name: 'head', parent: 'body' }];
build.sections(dialog, bones);
assert.equal(nodes.length, 3);
for (const gender of ['m', 'f', 'mf']) {
const section = dialog.modelVariantBoneSections[gender];
assert.equal(section.children.length, 2);
assert.equal(section.style.maxHeight, '160px');
assert.equal(section.style.overflowY, 'auto');
assert.equal(section.children[0], fields[`hide_${gender}_0`].bar);
}
build.refresh(dialog, bones, { m: true, f: false, mf: true });
assert.equal(dialog.modelVariantBoneSections.f.style.display, 'none');
assert.equal(dialog.modelVariantBoneSections.mf.style.display, '');
});
test('JEM exclusion removes whole subtrees without changing remaining roots or source', () => {
const model = { texture: 'custom.png', textureSize: [64, 64], models: [{ part: 'body', id: 'body',
boxes: [{}], submodels: [{ id: 'd', boxes: [{}], sprites: [{}],
animations: [{ 'd.visible': 'nbt(NeedsOfNatureEnergy,range:70-200)' }],
submodel: { id: 'tip', boxes: [{}] } }, { id: 'detail', boxes: [{}] }] }] };
const original = JSON.stringify(model);
const output = JSON.parse(JSON.stringify(variantBuilder().jem(model, ['0/0'])));
assert.deepEqual(output.models[0].boxes, [{}]);
assert.equal(output.models[0].submodels.length, 1);
assert.equal(output.models[0].submodels[0].id, 'detail');
assert.deepEqual(output.models[0].submodels[0].boxes, [{}]);
assert.equal(output.texture, 'custom.png');
assert.equal(JSON.stringify(model), original);
});
test('JEM export opens the shared variant screen directly and writes numbered gender files', () => {
const source = fs.readFileSync(path.join(__dirname, '..', 'AnimationDirector.js'), 'utf8').replace(
' Plugin.register(PLUGIN_ID, {',
' globalThis.variantTest = { showCreateJemModel };\n Plugin.register(PLUGIN_ID, {'
);
const model = { models: [{ part: 'body', id: 'body', boxes: [{}], submodels: [{ id: 'd', boxes: [{}] }] }] };
let dialog;
const files = [], errors = [];
const codec = { compile: () => model, fileName: () => 'wolf' };
const context = { Plugin: { register() {} }, Project: { name: 'wolf' },
Format: { id: 'optifine_entity', codec }, Formats: { optifine_entity: { codec } }, console,
Dialog: class {
constructor(options) { dialog = options; this.form = { form_data: {} }; }
show() {}
getFormResult() { return Object.fromEntries(Object.entries(dialog.form).map(([key, field]) => [key, field.value])); }
},
Blockbench: { export: (file, callback) => { files.push(file); callback(); },
showMessageBox: (error) => errors.push(error.message) },
};
vm.runInNewContext(source, context);
context.variantTest.showCreateJemModel();
assert.equal(dialog.title, 'Export JEM Models');
assert.equal(dialog.form.hide_mf_1.label, 'd');
const result = Object.fromEntries(Object.entries(dialog.form).map(([key, field]) => [key, field.value]));
Object.assign(result, { base: false, m: true, f: true, mf: true, hide_f_1: true });
dialog.onConfirm(result);
assert.deepEqual(errors, []);
assert.deepEqual(files.map((file) => file.name), ['wolf1', 'wolf2', 'wolf3']);
assert.deepEqual(Array.from(files[0].extensions), ['jem']);
assert.equal(JSON.parse(files[1].content).models[0].submodels.length, 0);
assert.deepEqual(JSON.parse(files[2].content), model);
assert.equal(JSON.parse(context.Project.animationdirector_jem_variants).hide_f[0], '0/0');
});
test('JEM exclusion supports singular submodels and clears animation targets for removed IDs', () => {
const model = { models: [{ id: 'body', submodel: { id: 'd', boxes: [{}] },
animations: [{ 'd.visible': 'true', 'body.visible': 'true' }] }, { id: 'head' }] };
const output = JSON.parse(JSON.stringify(variantBuilder().jem(model, ['0/single', '1'])));
assert.equal(output.models.length, 1);
assert.equal(output.models[0].submodel, undefined);
assert.deepEqual(output.models[0].animations, [{ 'body.visible': 'true' }]);
});