This commit is contained in:
2026-09-29 15:36:03 +02:00
parent 7f1c7d70f8
commit 312dbf7e92
2 changed files with 72 additions and 3 deletions
+25 -1
View File
@@ -426,6 +426,7 @@
type: 'select', type: 'select',
options: { '': 'None', V: 'V', A: 'A', M: 'M' }, options: { '': 'None', V: 'V', A: 'A', M: 'M' },
value: metadata.injector || '', value: metadata.injector || '',
description: 'Selecting an injector adds gender.male to required actor tags. You can remove that tag afterward.',
condition: fieldCondition, condition: fieldCondition,
}; };
form[`${base}_injects_into`] = { form[`${base}_injects_into`] = {
@@ -486,6 +487,25 @@
}); });
} }
function createInjectorGenderDefaultHandler(form) {
const roles = new Map(Object.entries(form).filter(([key]) => key.endsWith('_injector'))
.map(([key, field]) => [key, field.value || '']));
return (dialog, result) => {
const updates = {};
for (const [key, previous] of roles) {
if (!result || !(key in result)) continue;
const role = result[key];
roles.set(key, role);
if (role === previous || !['V', 'A', 'M'].includes(role)) continue;
const tagsKey = key.replace(/_injector$/, '_actor_tags');
const tags = uniqueTrimmedStrings(parseCommaSeparatedStrings(result[tagsKey]));
if (tags.includes('gender.male')) continue;
result[tagsKey] = updates[tagsKey] = [...tags, 'gender.male'].join(', ');
}
if (Object.keys(updates).length) dialog.form.setValues(updates, false);
};
}
function validateActorAnimdefMetadata(metadata, actorLabel) { function validateActorAnimdefMetadata(metadata, actorLabel) {
for (const entityType of metadata.entity_types || []) { for (const entityType of metadata.entity_types || []) {
if (!RESOURCE_ID_PATTERN.test(entityType)) { if (!RESOURCE_ID_PATTERN.test(entityType)) {
@@ -1650,6 +1670,7 @@
}; };
} }
const updateInjectorGenderDefault = createInjectorGenderDefaultHandler(form);
const dialog = new Dialog({ const dialog = new Dialog({
id: 'edit_actor_preferences', id: 'edit_actor_preferences',
title: 'Actor Preferences', title: 'Actor Preferences',
@@ -1658,9 +1679,10 @@
buttons: ['Cancel', 'Save & Apply'], buttons: ['Cancel', 'Save & Apply'],
cancelIndex: 0, cancelIndex: 0,
confirmIndex: 1, confirmIndex: 1,
onFormChange: (result) => { onFormChange(result) {
showBoneTextureOverrides = !!result?.show_bone_texture_overrides; showBoneTextureOverrides = !!result?.show_bone_texture_overrides;
showAnimdefMetadata = !!result?.show_animdef_metadata; showAnimdefMetadata = !!result?.show_animdef_metadata;
updateInjectorGenderDefault(this, result);
}, },
onConfirm: (result) => { onConfirm: (result) => {
let editing = false; let editing = false;
@@ -4088,11 +4110,13 @@
return false; return false;
} }
}; };
const updateInjectorGenderDefault = createInjectorGenderDefaultHandler(form);
const dialog = new Dialog({ const dialog = new Dialog({
id: 'create_afw_animation_definition_review', id: 'create_afw_animation_definition_review',
title: `Animation Definition: ${group.id}`, title: `Animation Definition: ${group.id}`,
width: 760, width: 760,
form, form,
onFormChange(result) { updateInjectorGenderDefault(this, result); },
buttons: ['Cancel', 'Save', 'Create Animdef'], buttons: ['Cancel', 'Save', 'Create Animdef'],
cancelIndex: 0, cancelIndex: 0,
confirmIndex: 2, confirmIndex: 2,
+47 -2
View File
@@ -8,7 +8,7 @@ function createHarness() {
const file = path.join(__dirname, '..', 'AnimationDirector.js'); const file = path.join(__dirname, '..', 'AnimationDirector.js');
const source = fs.readFileSync(file, 'utf8').replace( const source = fs.readFileSync(file, 'utf8').replace(
' Plugin.register(PLUGIN_ID, {', ' Plugin.register(PLUGIN_ID, {',
' globalThis.adTest = { showAnimationDefinitionReviewDialog };\n Plugin.register(PLUGIN_ID, {' ' globalThis.adTest = { showAnimationDefinitionReviewDialog, createInjectorGenderDefaultHandler };\n Plugin.register(PLUGIN_ID, {'
); );
const dialogs = []; const dialogs = [];
const exports = []; const exports = [];
@@ -45,7 +45,8 @@ function createHarness() {
context.adTest.showAnimationDefinitionReviewDialog(group, analysis); context.adTest.showAnimationDefinitionReviewDialog(group, analysis);
return dialogs.at(-1); return dialogs.at(-1);
} }
return { open, exports, errors, project, analysis, setFormResult(result) { formResult = result; } }; return { open, exports, errors, project, analysis, createInjectorGenderDefaultHandler: context.adTest.createInjectorGenderDefaultHandler,
setFormResult(result) { formResult = result; } };
} }
function defaults(form) { function defaults(form) {
@@ -54,6 +55,50 @@ function defaults(form) {
.map(([key, field]) => [key, field.value])); .map(([key, field]) => [key, field.value]));
} }
test('selecting an injector adds an editable male tag without enforcing it on export', () => {
for (const role of ['V', 'A', 'M']) {
const h = createHarness(), dialog = h.open(), result = defaults(dialog.form), updates = [];
const instance = { form: { setValues(values, notify) { updates.push(values); assert.equal(notify, false); } } };
result.stub_actor_1_actor_tags = 'custom.tag';
result.stub_actor_1_injector = role;
dialog.onFormChange.call(instance, result);
assert.equal(result.stub_actor_1_actor_tags, 'custom.tag, gender.male');
assert.equal(updates.length, 1);
result.stub_actor_1_actor_tags = 'custom.tag';
dialog.onFormChange.call(instance, result);
result.display_name = 'Unrelated edit';
dialog.onFormChange.call(instance, result);
assert.equal(updates.length, 1, 'Manual removal survives unrelated changes');
result.stub_actor_2_entity_types = 'minecraft:wolf';
dialog.onConfirm(result);
assert.equal(h.errors.length, 0);
assert.equal(h.exports[0].actors[0].injector, role);
assert.deepEqual(h.exports[0].actors[0].actor_tags, ['custom.tag']);
}
});
test('injector defaults preserve loaded roles, existing tags, and actor preferences edits', () => {
const h = createHarness();
const form = {
preferences_animdef_1_injector: { value: 'V' },
preferences_animdef_2_injector: { value: '' },
};
const update = h.createInjectorGenderDefaultHandler(form), writes = [];
const dialog = { form: { setValues(values) { writes.push(values); } } };
const result = { preferences_animdef_1_injector: 'V', preferences_animdef_1_actor_tags: 'custom.tag',
preferences_animdef_2_injector: '', preferences_animdef_2_actor_tags: 'gender.male, other.tag' };
update(dialog, result);
assert.equal(writes.length, 0, 'Opening an imported/saved role does not add a tag');
result.preferences_animdef_2_injector = 'A'; update(dialog, result);
assert.equal(writes.length, 0, 'Existing male tags are not duplicated');
result.preferences_animdef_2_injector = ''; update(dialog, result);
assert.equal(result.preferences_animdef_2_actor_tags, 'gender.male, other.tag', 'Deselecting a role does not remove explicit tags');
result.preferences_animdef_2_actor_tags = 'other.tag';
result.preferences_animdef_2_injector = 'M'; update(dialog, result);
assert.equal(result.preferences_animdef_2_actor_tags, 'other.tag, gender.male');
assert.equal(result.preferences_animdef_1_actor_tags, 'custom.tag', 'Only the changed actor is affected');
});
test('Save retains definition fields in the project without exporting and restores them on reopen', () => { test('Save retains definition fields in the project without exporting and restores them on reopen', () => {
const h = createHarness(), dialog = h.open(), result = defaults(dialog.form); const h = createHarness(), dialog = h.open(), result = defaults(dialog.form);
assert.deepEqual(Array.from(dialog.buttons), ['Cancel', 'Save', 'Create Animdef']); assert.deepEqual(Array.from(dialog.buttons), ['Cancel', 'Save', 'Create Animdef']);