From ab55366c9b80bc63db673eeb809410252ca69e7a Mon Sep 17 00:00:00 2001 From: iDantar <130079801+iDantar@users.noreply.github.com> Date: Wed, 6 May 2026 21:27:27 +0300 Subject: [PATCH 1/4] Insert heritage into ancestry features list --- .../actor/character/feats/collection.ts | 3 ++ src/module/actor/character/sheet.ts | 42 ++++++++++++++++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/module/actor/character/feats/collection.ts b/src/module/actor/character/feats/collection.ts index 65f434801f7..b5e0ac1f3ff 100644 --- a/src/module/actor/character/feats/collection.ts +++ b/src/module/actor/character/feats/collection.ts @@ -247,6 +247,9 @@ class CharacterFeats extends Collection extends CreatureSheetPF2e /** Non-persisted tweaks to formula data */ #formulaQuantities: Record = {}; + /** Nested feat rows for sheet UI from `flags.pf2e.itemGrants` (same rules as `FeatGroup.#getChildSlots`). */ + #getNestedSlots(granter: ItemPF2e): FeatSlot>[] { + const itemGrants = (granter.flags?.[SYSTEM_ID]?.itemGrants ?? {}) as Record< + string, + { id?: string; nested?: boolean } + >; + + return Object.values(itemGrants) + .filter((g) => (g?.nested ?? null) !== false) + .map((g) => (typeof g?.id === "string" && g.id.length > 0 ? (this.actor.items.get(g.id) ?? null) : null)) + .filter((i): i is FeatPF2e => !!i && i.isOfType("feat")) + .map( + (grant): FeatSlot> => ({ + id: grant.id, + label: null, + level: null, + feat: grant, + children: this.#getNestedSlots(grant), + }), + ); + } + static override get defaultOptions(): ActorSheetOptions { const options = super.defaultOptions; options.classes = [...options.classes, "character"]; @@ -288,7 +310,23 @@ class CharacterSheetPF2e extends CreatureSheetPF2e // Is the stamina variant rule enabled? sheetData.hasStamina = game.pf2e.settings.variants.stamina; sheetData.actions = this.#prepareAbilities(); - sheetData.feats = [...actor.feats, actor.feats.bonus]; + + const featGroups: FeatGroup[] = [...actor.feats, actor.feats.bonus]; + const ancestryFeatures = featGroups.find((g) => g.id === "ancestryfeature"); + // Add the heritage to the ancestry features if it is not already present + if (actor.heritage && ancestryFeatures) { + const alreadyPresent = ancestryFeatures.feats.some((f) => f.feat?.id === actor.heritage?.id); + if (!alreadyPresent) { + ancestryFeatures.feats.unshift({ + id: "heritage", + label: null, + level: null, + feat: actor.heritage, + children: this.#getNestedSlots(actor.heritage), + } as never); + } + } + sheetData.feats = featGroups; sheetData.crafting = await this.#prepareCrafting(); From fe1dc293033659f662fab14293354d2c4a62e381 Mon Sep 17 00:00:00 2001 From: iDantar <130079801+iDantar@users.noreply.github.com> Date: Mon, 11 May 2026 19:47:40 +0300 Subject: [PATCH 2/4] PR fixes --- src/module/actor/character/sheet.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/module/actor/character/sheet.ts b/src/module/actor/character/sheet.ts index b5459fdaed0..3770e94fd92 100644 --- a/src/module/actor/character/sheet.ts +++ b/src/module/actor/character/sheet.ts @@ -317,13 +317,14 @@ class CharacterSheetPF2e extends CreatureSheetPF2e if (actor.heritage && ancestryFeatures) { const alreadyPresent = ancestryFeatures.feats.some((f) => f.feat?.id === actor.heritage?.id); if (!alreadyPresent) { - ancestryFeatures.feats.unshift({ + const heritageRow: FeatSlot> = { id: "heritage", label: null, level: null, feat: actor.heritage, children: this.#getNestedSlots(actor.heritage), - } as never); + }; + ancestryFeatures.feats.unshift(heritageRow as FeatGroup["feats"][number]); } } sheetData.feats = featGroups; From 4cb1e34dc05491424d238f1ae5fd0d463e656cba Mon Sep 17 00:00:00 2001 From: iDantar <130079801+iDantar@users.noreply.github.com> Date: Sat, 16 May 2026 01:09:48 +0300 Subject: [PATCH 3/4] move heritage insertion to CharacterFeats, account for heritages given by feats --- .../actor/character/feats/collection.ts | 56 ++++++++++++++++++- src/module/actor/character/feats/group.ts | 45 ++++++++++----- src/module/actor/character/sheet.ts | 42 +------------- 3 files changed, 86 insertions(+), 57 deletions(-) diff --git a/src/module/actor/character/feats/collection.ts b/src/module/actor/character/feats/collection.ts index b5e0ac1f3ff..e27bcb6784b 100644 --- a/src/module/actor/character/feats/collection.ts +++ b/src/module/actor/character/feats/collection.ts @@ -1,9 +1,9 @@ import type { CharacterPF2e } from "@actor"; -import type { FeatPF2e, ItemPF2e } from "@item"; +import type { FeatPF2e, HeritagePF2e, ItemPF2e } from "@item"; import { sluggify } from "@util"; import * as R from "remeda"; import { FeatGroup } from "./group.ts"; -import { FeatGroupData, FeatSlotData } from "./types.ts"; +import type { FeatGroupData, FeatSlot, FeatSlotData } from "./types.ts"; class CharacterFeats extends Collection> { /** Feats belonging no actual group ("bonus feats" in rules text) */ @@ -262,6 +262,58 @@ class CharacterFeats extends Collection): FeatSlot>[] { + const rawGrants = granter.flags[SYSTEM_ID]?.itemGrants ?? {}; + const grantsById = R.mapKeys(rawGrants, (_, g) => g.id) as Record; + + const grantFeats: FeatPF2e[] = granter.isOfType("feat") + ? granter.grants.filter( + (g): g is FeatPF2e => g.isOfType("feat") && grantsById[g.id]?.nested !== false, + ) + : Object.values(rawGrants) + .filter((g) => R.isPlainObject(g) && (g.nested ?? null) !== false && typeof g.id === "string") + .map((g) => this.actor.items.get(g.id)) + .filter((i): i is FeatPF2e => !!i && i.isOfType("feat")); + + return grantFeats.map( + (grant): FeatSlot> => ({ + id: grant.id, + label: null, + level: null, + feat: grant, + children: this.#nestedGrantSlotsFromItemGrants(grant), + }), + ); + } + + #prependHeritageToAncestryFeatures(): void { + const group = this.get("ancestryfeature"); + if (!group) return; + + const isGrantedByFeat = (heritage: HeritagePF2e): boolean => + heritage.grantedBy?.isOfType("feat") ?? false; + + const primary = this.actor.heritage; + const others = this.actor.itemTypes.heritage + .filter((h) => !primary || h.id !== primary.id) + .filter((h) => !isGrantedByFeat(h)) + .sort((a, b) => a.sort - b.sort); + + const heritageSlots = [primary, ...others].filter(R.isTruthy).map((heritage) => { + const slot: FeatSlot> = { + id: heritage.id, + label: null, + level: null, + feat: heritage, + children: this.#nestedGrantSlotsFromItemGrants(heritage), + }; + return slot as FeatGroup["feats"][number]; + }); + group.feats.unshift(...heritageSlots); } } diff --git a/src/module/actor/character/feats/group.ts b/src/module/actor/character/feats/group.ts index 4faeea18000..14f386b0efa 100644 --- a/src/module/actor/character/feats/group.ts +++ b/src/module/actor/character/feats/group.ts @@ -121,21 +121,36 @@ class FeatGroup): FeatSlot | HeritagePF2e>[] { - if (!feat?.isOfType("feat")) return []; - const grantsById = R.mapKeys(feat.flags[SYSTEM_ID].itemGrants, (_, g) => g.id); - - return feat.grants - .filter((g) => grantsById[g.id]?.nested !== false) - .map((grant): FeatSlot | HeritagePF2e> => { - return { - id: grant.id, - label: null, - level: grant.system.level?.taken ?? null, - feat: grant, - children: this.#getChildSlots(grant), - }; - }); + #getChildSlots(granter: Maybe): FeatSlot | HeritagePF2e>[] { + if (!granter) return []; + const rawGrants = granter.flags[SYSTEM_ID]?.itemGrants ?? {}; + const grantsById = R.mapKeys(rawGrants, (_, g) => g.id) as Record; + + const grants: (FeatPF2e | HeritagePF2e)[] = []; + const grantIds = granter.isOfType("feat") + ? granter.grants.map((g) => g.id) + : granter.isOfType("heritage") + ? Object.values(rawGrants) + .filter( + (g) => R.isPlainObject(g) && (g.nested ?? null) !== false && typeof g.id === "string", + ) + .map((g) => g.id) + : []; + + for (const grantId of grantIds) { + if (grantsById[grantId]?.nested === false) continue; + const item = this.actor.items.get(grantId); + if (item?.isOfType("feat")) grants.push(item); + else if (item?.isOfType("heritage")) grants.push(item); + } + + return grants.map((grant): FeatSlot | HeritagePF2e> => ({ + id: grant.id, + label: null, + level: grant.isOfType("feat") ? (grant.system.level?.taken ?? null) : null, + feat: grant, + children: this.#getChildSlots(grant), + })); } /** Returns true if this feat is a valid type for the group */ diff --git a/src/module/actor/character/sheet.ts b/src/module/actor/character/sheet.ts index 3770e94fd92..a806991f0f2 100644 --- a/src/module/actor/character/sheet.ts +++ b/src/module/actor/character/sheet.ts @@ -71,7 +71,7 @@ import { } from "./data.ts"; import type { CharacterPF2e } from "./document.ts"; import { ElementalBlast, ElementalBlastConfig } from "./elemental-blast.ts"; -import type { FeatBrowserFilterProps, FeatGroup, FeatSlot } from "./feats/index.ts"; +import type { FeatBrowserFilterProps, FeatGroup } from "./feats/index.ts"; import { getItemProficiencyRank } from "./helpers.ts"; import { PCSheetTabManager } from "./tab-manager.ts"; import { CHARACTER_SHEET_TABS } from "./values.ts"; @@ -85,28 +85,6 @@ class CharacterSheetPF2e extends CreatureSheetPF2e /** Non-persisted tweaks to formula data */ #formulaQuantities: Record = {}; - /** Nested feat rows for sheet UI from `flags.pf2e.itemGrants` (same rules as `FeatGroup.#getChildSlots`). */ - #getNestedSlots(granter: ItemPF2e): FeatSlot>[] { - const itemGrants = (granter.flags?.[SYSTEM_ID]?.itemGrants ?? {}) as Record< - string, - { id?: string; nested?: boolean } - >; - - return Object.values(itemGrants) - .filter((g) => (g?.nested ?? null) !== false) - .map((g) => (typeof g?.id === "string" && g.id.length > 0 ? (this.actor.items.get(g.id) ?? null) : null)) - .filter((i): i is FeatPF2e => !!i && i.isOfType("feat")) - .map( - (grant): FeatSlot> => ({ - id: grant.id, - label: null, - level: null, - feat: grant, - children: this.#getNestedSlots(grant), - }), - ); - } - static override get defaultOptions(): ActorSheetOptions { const options = super.defaultOptions; options.classes = [...options.classes, "character"]; @@ -311,23 +289,7 @@ class CharacterSheetPF2e extends CreatureSheetPF2e sheetData.hasStamina = game.pf2e.settings.variants.stamina; sheetData.actions = this.#prepareAbilities(); - const featGroups: FeatGroup[] = [...actor.feats, actor.feats.bonus]; - const ancestryFeatures = featGroups.find((g) => g.id === "ancestryfeature"); - // Add the heritage to the ancestry features if it is not already present - if (actor.heritage && ancestryFeatures) { - const alreadyPresent = ancestryFeatures.feats.some((f) => f.feat?.id === actor.heritage?.id); - if (!alreadyPresent) { - const heritageRow: FeatSlot> = { - id: "heritage", - label: null, - level: null, - feat: actor.heritage, - children: this.#getNestedSlots(actor.heritage), - }; - ancestryFeatures.feats.unshift(heritageRow as FeatGroup["feats"][number]); - } - } - sheetData.feats = featGroups; + sheetData.feats = [...actor.feats, actor.feats.bonus]; sheetData.crafting = await this.#prepareCrafting(); From 6f010eb3a47fc1854339e7700be2286e0d107e8a Mon Sep 17 00:00:00 2001 From: iDantar <130079801+iDantar@users.noreply.github.com> Date: Sat, 16 May 2026 01:13:59 +0300 Subject: [PATCH 4/4] linter fixes --- src/module/actor/character/feats/group.ts | 20 ++++++++++---------- src/module/actor/character/sheet.ts | 1 - 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/module/actor/character/feats/group.ts b/src/module/actor/character/feats/group.ts index 14f386b0efa..fa142c31283 100644 --- a/src/module/actor/character/feats/group.ts +++ b/src/module/actor/character/feats/group.ts @@ -131,9 +131,7 @@ class FeatGroup g.id) : granter.isOfType("heritage") ? Object.values(rawGrants) - .filter( - (g) => R.isPlainObject(g) && (g.nested ?? null) !== false && typeof g.id === "string", - ) + .filter((g) => R.isPlainObject(g) && (g.nested ?? null) !== false && typeof g.id === "string") .map((g) => g.id) : []; @@ -144,13 +142,15 @@ class FeatGroup | HeritagePF2e> => ({ - id: grant.id, - label: null, - level: grant.isOfType("feat") ? (grant.system.level?.taken ?? null) : null, - feat: grant, - children: this.#getChildSlots(grant), - })); + return grants.map( + (grant): FeatSlot | HeritagePF2e> => ({ + id: grant.id, + label: null, + level: grant.isOfType("feat") ? (grant.system.level?.taken ?? null) : null, + feat: grant, + children: this.#getChildSlots(grant), + }), + ); } /** Returns true if this feat is a valid type for the group */ diff --git a/src/module/actor/character/sheet.ts b/src/module/actor/character/sheet.ts index a806991f0f2..2e6dc2470cc 100644 --- a/src/module/actor/character/sheet.ts +++ b/src/module/actor/character/sheet.ts @@ -288,7 +288,6 @@ class CharacterSheetPF2e extends CreatureSheetPF2e // Is the stamina variant rule enabled? sheetData.hasStamina = game.pf2e.settings.variants.stamina; sheetData.actions = this.#prepareAbilities(); - sheetData.feats = [...actor.feats, actor.feats.bonus]; sheetData.crafting = await this.#prepareCrafting();