Skip to content

Commit d990268

Browse files
committed
updated: migrated special secondary skills
1 parent eccb388 commit d990268

20 files changed

Lines changed: 462 additions & 133 deletions

File tree

src/module/actor/ABFActor.js

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export class ABFActor extends Actor {
6565
this.system = inflateSystemFromTypeMarkers(this.system);
6666

6767
this._removeNullCustomAttributes();
68+
this._removeNullSecondarySpecialSkills();
6869

6970
buildTypedNodes(this, TYPED_PATHS);
7071
this._applyDefaultKeysToTypedNodes();
@@ -706,6 +707,26 @@ export class ABFActor extends Actor {
706707
this.updateSource(updates);
707708
}
708709

710+
_removeNullSecondarySpecialSkills() {
711+
const secondarySpecialSkills = this.system?.secondaries?.secondarySpecialSkills;
712+
713+
if (!secondarySpecialSkills || typeof secondarySpecialSkills !== 'object') return;
714+
715+
const nullKeys = Object.entries(secondarySpecialSkills)
716+
.filter(([, value]) => value === null)
717+
.map(([key]) => key);
718+
719+
if (nullKeys.length === 0) return;
720+
721+
const updates = {};
722+
723+
for (const key of nullKeys) {
724+
updates[`system.secondaries.secondarySpecialSkills.-=${key}`] = null;
725+
}
726+
727+
this.updateSource(updates);
728+
}
729+
709730
/**
710731
* @param {Object} data
711732
* @param {import('../items/ABFItems').ABFItemsEnum} data.type
@@ -750,10 +771,6 @@ export class ABFActor extends Actor {
750771
return this.getItemsOf(type).find(item => item._id === itemId);
751772
}
752773

753-
getSecondarySpecialSkills() {
754-
return this.getItemsOf(ABFItems.SECONDARY_SPECIAL_SKILL);
755-
}
756-
757774
getKnownSpells() {
758775
return this.getItemsOf(ABFItems.SPELL);
759776
}

src/module/actor/ABFActorSheet.js

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ import { Logger } from '../../utils';
1212
import { ABFSettingsKeys } from '../../utils/registerSettings';
1313
import { createClickHandlers } from './utils/createClickHandlers';
1414
import { TypeEditorRegistry } from './types/TypeEditorRegistry.js';
15+
import {
16+
isTypedNodeDeletable,
17+
resolveTypedNodeDeletableConfig
18+
} from './types/typedNodeSheetMenuConfig.js';
1519
import { findEffectLinkedToItem } from './utils/findEffectLinkedToItem.js';
1620
import { ensureLinkedEffectForItem } from './utils/ensureLinkedEffectForItem.js';
1721

@@ -181,7 +185,7 @@ export default class ABFActorSheet extends ActorSheetV1 {
181185

182186
if (!this.options.editable) return;
183187

184-
this._activateBaseTypeContextMenu(html);
188+
this._activateTypedNodeContextMenu(html);
185189

186190
this._setupDebouncedSheetUpdates(html);
187191

@@ -192,17 +196,38 @@ export default class ABFActorSheet extends ActorSheetV1 {
192196
this._activateEffectControls(html);
193197
}
194198

195-
_activateBaseTypeContextMenu(html) {
199+
_activateTypedNodeContextMenu(html) {
196200
const ContextMenuImpl = foundry.applications?.ux?.ContextMenu?.implementation ?? ContextMenu;
197201
const isV14 = !!foundry.applications?.ux?.ContextMenu?.implementation;
202+
const sheet = this;
203+
198204
new ContextMenuImpl(
199205
html instanceof HTMLElement ? html : html[0],
200206
'.base-type-row',
201207
[
202208
{
203209
name: game.i18n.localize('contextualMenu.common.options.edit') ?? 'Edit…',
204210
icon: '<i class="fas fa-edit"></i>',
205-
callback: target => this._openBaseTypeEditor(target instanceof HTMLElement ? target : target[0])
211+
callback: target => sheet._openBaseTypeEditor(target instanceof HTMLElement ? target : target[0])
212+
},
213+
{
214+
name: game.i18n.localize('contextualMenu.common.options.delete') ?? 'Delete',
215+
icon: '<i class="fas fa-trash"></i>',
216+
condition: target => {
217+
const el = target instanceof HTMLElement ? target : target[0];
218+
const path = el?.dataset?.path;
219+
if (!path) return false;
220+
return isTypedNodeDeletable(sheet.actor, path);
221+
},
222+
callback: target => {
223+
const el = target instanceof HTMLElement ? target : target[0];
224+
const path = el?.dataset?.path;
225+
if (!path) return;
226+
227+
const typedNode = sheet.actor.typedNodes?.get(path) ?? null;
228+
const cfg = resolveTypedNodeDeletableConfig(path, typedNode);
229+
cfg?.onDelete(sheet, el);
230+
}
206231
}
207232
],
208233
...(isV14 ? [{ jQuery: false }] : [])

src/module/actor/constants.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ export const INITIAL_ACTOR_DATA = {
389389
puppetMaking: { __type: '{"type":"SecondaryAbility","attribute":"power"}' }
390390
},
391391

392-
secondarySpecialSkills: []
392+
secondarySpecialSkills: {}
393393
},
394394

395395
combat: {
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { deleteCustomAttribute } from '../utils/buttonCallbacks/customAttributes.js';
2+
import { deleteSecondarySpecialSkill } from '../utils/buttonCallbacks/secondarySpecialSkills.js';
3+
4+
/**
5+
* Dev-only registry: typed node paths that may show "Delete" in the actor sheet
6+
* context menu. Add entries here when a dynamic typed node group should be
7+
* removable from the sheet (static template secondaries stay edit-only).
8+
*
9+
* @typedef {{
10+
* id: string,
11+
* pathPrefix: string,
12+
* type?: string,
13+
* onDelete: (sheet: import('../ABFActorSheet').default, target: HTMLElement) => Promise<void> | void
14+
* }} TypedNodeDeletableConfig
15+
*/
16+
17+
/** @type {TypedNodeDeletableConfig[]} */
18+
export const TYPED_NODE_DELETABLE_CONFIG = [
19+
{
20+
id: 'secondarySpecialSkills',
21+
pathPrefix: 'system.secondaries.secondarySpecialSkills.',
22+
type: 'SecondaryAbility',
23+
onDelete: deleteSecondarySpecialSkill
24+
},
25+
{
26+
id: 'customAttributes',
27+
pathPrefix: 'system.effects.customAttributes.',
28+
type: 'CustomAttribute',
29+
onDelete: deleteCustomAttribute
30+
}
31+
];
32+
33+
/**
34+
* @param {string} systemPath
35+
* @param {import('./BaseType.js').BaseType | null | undefined} typedNode
36+
* @returns {TypedNodeDeletableConfig | null}
37+
*/
38+
export function resolveTypedNodeDeletableConfig(systemPath, typedNode = null) {
39+
const path = String(systemPath ?? '');
40+
41+
for (const cfg of TYPED_NODE_DELETABLE_CONFIG) {
42+
if (!path.startsWith(cfg.pathPrefix)) continue;
43+
44+
const nodeType = typedNode?.constructor?.type;
45+
if (cfg.type && nodeType !== cfg.type) continue;
46+
47+
return cfg;
48+
}
49+
50+
return null;
51+
}
52+
53+
/**
54+
* @param {import('../ABFActor').ABFActor} actor
55+
* @param {string} systemPath
56+
* @returns {boolean}
57+
*/
58+
export function isTypedNodeDeletable(actor, systemPath) {
59+
const typedNode = actor?.typedNodes?.get(systemPath) ?? null;
60+
return resolveTypedNodeDeletableConfig(systemPath, typedNode) != null;
61+
}

src/module/actor/utils/buttonCallbacks/customAttributes.js

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -99,14 +99,25 @@ export async function addCustomAttribute(sheet) {
9999

100100
addCustomAttribute.action = 'add-custom-attribute';
101101

102-
export async function deleteCustomAttribute(sheet, event) {
103-
event?.preventDefault?.();
104-
event?.stopPropagation?.();
102+
const CUSTOM_ATTRIBUTE_PATH_PREFIX = 'system.effects.customAttributes.';
103+
104+
function customAttributeKeyFromPath(path) {
105+
const normalized = String(path ?? '');
106+
if (!normalized.startsWith(CUSTOM_ATTRIBUTE_PATH_PREFIX)) return '';
107+
return normalized.slice(CUSTOM_ATTRIBUTE_PATH_PREFIX.length);
108+
}
109+
110+
export async function deleteCustomAttribute(sheet, eventOrTarget) {
111+
eventOrTarget?.preventDefault?.();
112+
eventOrTarget?.stopPropagation?.();
105113

106114
const actor = sheet?.actor;
107115
if (!actor) return;
108116

109-
const key = String(event?.currentTarget?.dataset?.customAttributeKey ?? '').trim();
117+
const el = eventOrTarget?.currentTarget ?? eventOrTarget;
118+
const key =
119+
String(el?.dataset?.customAttributeKey ?? '').trim() ||
120+
customAttributeKeyFromPath(el?.dataset?.path);
110121
if (!key) return;
111122

112123
await sheet?._flushPendingSheetUpdatesImmediately?.();
@@ -123,5 +134,3 @@ export async function deleteCustomAttribute(sheet, event) {
123134
sheet.render?.(false);
124135
}
125136

126-
deleteCustomAttribute.action = 'delete-custom-attribute';
127-
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { openSimpleInputDialog } from '../../../utils/dialogs/openSimpleInputDialog.js';
2+
3+
const TYPE_MARKER = '{"type":"SecondaryAbility","attribute":"intelligence"}';
4+
5+
function toTypedNode(raw, fallbackKey) {
6+
const src = raw && typeof raw === 'object' ? raw : {};
7+
8+
return {
9+
__type: TYPE_MARKER,
10+
key: String(src.key ?? fallbackKey),
11+
attribute: String(src.attribute ?? 'intelligence'),
12+
base: { value: Number(src.base?.value ?? src.base ?? 0) || 0 },
13+
special: { value: Number(src.special?.value ?? src.special ?? 0) || 0 }
14+
};
15+
}
16+
17+
function normalizeSecondarySpecialSkills(raw) {
18+
if (Array.isArray(raw)) return {};
19+
20+
if (!raw || typeof raw !== 'object') return {};
21+
22+
const out = {};
23+
for (const [key, value] of Object.entries(raw)) {
24+
if (value == null) continue;
25+
out[key] = toTypedNode(value, key);
26+
}
27+
28+
return out;
29+
}
30+
31+
function sanitizeKey(name) {
32+
const normalized = String(name ?? '')
33+
.trim()
34+
.replaceAll('.', '_');
35+
36+
return normalized || 'secondarySpecialSkill';
37+
}
38+
39+
function uniqueKey(map, baseKey) {
40+
let candidate = baseKey;
41+
let i = 2;
42+
43+
while (Object.prototype.hasOwnProperty.call(map, candidate)) {
44+
candidate = `${baseKey}_${i}`;
45+
i += 1;
46+
}
47+
48+
return candidate;
49+
}
50+
51+
export async function addSecondarySpecialSkill(sheet) {
52+
const name = await openSimpleInputDialog({
53+
content: game.i18n.localize('dialogs.items.secondarySkill.content')
54+
});
55+
56+
const cleanName = String(name ?? '').trim();
57+
if (!cleanName) return;
58+
59+
const existing = normalizeSecondarySpecialSkills(
60+
sheet.actor.system.secondaries?.secondarySpecialSkills
61+
);
62+
const key = uniqueKey(existing, sanitizeKey(cleanName));
63+
64+
existing[key] = {
65+
__type: TYPE_MARKER,
66+
key,
67+
attribute: 'intelligence',
68+
base: { value: 0 },
69+
special: { value: 0 }
70+
};
71+
72+
await sheet.actor.update({ 'system.secondaries.secondarySpecialSkills': existing });
73+
}
74+
75+
addSecondarySpecialSkill.action = 'add-secondary-special-skill';
76+
77+
const SECONDARY_SPECIAL_SKILL_PATH_PREFIX = 'system.secondaries.secondarySpecialSkills.';
78+
79+
function secondarySpecialSkillKeyFromPath(path) {
80+
const normalized = String(path ?? '');
81+
if (!normalized.startsWith(SECONDARY_SPECIAL_SKILL_PATH_PREFIX)) return '';
82+
return normalized.slice(SECONDARY_SPECIAL_SKILL_PATH_PREFIX.length);
83+
}
84+
85+
export async function deleteSecondarySpecialSkill(sheet, eventOrTarget) {
86+
eventOrTarget?.preventDefault?.();
87+
eventOrTarget?.stopPropagation?.();
88+
89+
const actor = sheet?.actor;
90+
if (!actor) return;
91+
92+
const el = eventOrTarget?.currentTarget ?? eventOrTarget;
93+
const key =
94+
String(el?.dataset?.secondarySpecialSkillKey ?? '').trim() ||
95+
secondarySpecialSkillKeyFromPath(el?.dataset?.path);
96+
if (!key) return;
97+
98+
await sheet?._flushPendingSheetUpdatesImmediately?.();
99+
100+
await actor.update(
101+
{
102+
[`system.secondaries.secondarySpecialSkills.${key}`]: null
103+
},
104+
{
105+
unset: true
106+
}
107+
);
108+
109+
sheet.render?.(false);
110+
}

src/module/actor/utils/prepareActor/calculations/actor/secondaries/mutateSecondariesData.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,6 @@ export const mutateSecondariesData = data => {
2828
/** @type {keyof import('../../../../../../types/Actor').ABFActorDataSourceData['secondaries']} */
2929
const secondaryKey = rawSecondaryKey;
3030

31-
if (secondaryKey === 'secondarySpecialSkills') continue;
32-
3331
for (const key of Object.keys(secondaries[secondaryKey])) {
3432
/** @type {import('../../../../../../types/Items').SecondaryData} */
3533
const secondary = data.secondaries[secondaryKey][key];

src/module/actor/utils/prepareActor/prepareActor.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,14 @@ export const prepareActor = async actor => {
133133
await runEffectFlow(actor, { derivedFns: DERIVED_DATA_FUNCTIONS });
134134

135135
// 4) UI-only derived (AQUÍ VA “LO NUEVO”)
136-
actor.system.general.description.enriched = await (foundry.applications?.ux?.TextEditor?.implementation ?? TextEditor).enrichHTML(
137-
actor.system.general.description.value,
138-
{ async: true }
139-
);
136+
const description = actor.system.general.description.value ?? '';
137+
if (game.animabf?._migrationActive) {
138+
actor.system.general.description.enriched = description;
139+
} else {
140+
actor.system.general.description.enriched = await (
141+
foundry.applications?.ux?.TextEditor?.implementation ?? TextEditor
142+
).enrichHTML(description, { async: true });
143+
}
140144

141145
for (const key of Object.keys(actor.system.ui.contractibleItems ?? {})) {
142146
if (typeof actor.system.ui.contractibleItems[key] === 'string') {

src/module/actor/utils/prepareItems/constants.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import { NemesisSkillItemConfig } from '../../../types/domine/NemesisSkillItemCo
1818
import { NoteItemConfig } from '../../../types/general/NoteItemConfig';
1919
import { PsychicDisciplineItemConfig } from '../../../types/psychic/PsychicDisciplineItemConfig';
2020
import { PsychicPowerItemConfig } from '../../../types/psychic/PsychicPowerItemConfig';
21-
import { SecondarySpecialSkillItemConfig } from '../../../types/secondaries/SecondarySpecialSkillItemConfig';
2221
import { SelectedSpellItemConfig } from '../../../types/mystic/SelectedSpellItemConfig';
2322
import { ActViaItemConfig } from '../../../types/mystic/ActViaItemConfig';
2423
import { InnateMagicViaItemConfig } from '../../../types/mystic/InnateMagicViaItemConfig';
@@ -51,7 +50,6 @@ export const INTERNAL_ITEM_CONFIGURATIONS = {
5150
[MartialArtItemConfig.type]: MartialArtItemConfig,
5251
[MetamagicItemConfig.type]: MetamagicItemConfig,
5352
[NemesisSkillItemConfig.type]: NemesisSkillItemConfig,
54-
[SecondarySpecialSkillItemConfig.type]: SecondarySpecialSkillItemConfig,
5553
[SelectedSpellItemConfig.type]: SelectedSpellItemConfig,
5654
[SpecialSkillItemConfig.type]: SpecialSkillItemConfig,
5755
[SpellMaintenanceItemConfig.type]: SpellMaintenanceItemConfig,

0 commit comments

Comments
 (0)