diff --git a/src/module/actor/character/document.ts b/src/module/actor/character/document.ts index cf3a4c5c70d..ec9726ccbb1 100644 --- a/src/module/actor/character/document.ts +++ b/src/module/actor/character/document.ts @@ -1,5 +1,6 @@ import { CreaturePF2e, type FamiliarPF2e } from "@actor"; import { Abilities } from "@actor/creature/data.ts"; +import { CreatureSaves } from "@actor/creature/saves.ts"; import { CreatureUpdateCallbackOptions, ResourceData } from "@actor/creature/types.ts"; import { ALLIANCES, SAVING_THROW_ATTRIBUTES } from "@actor/creature/values.ts"; import { StrikeData } from "@actor/data/base.ts"; @@ -769,8 +770,7 @@ class CharacterPF2e { + const saves = R.mapToObj(SAVE_TYPES, (saveType) => { const save = this.system.saves[saveType]; const saveName = _loc(CONFIG.PF2E.saves[saveType]); const modifiers: Modifier[] = []; @@ -833,6 +833,7 @@ class CharacterPF2e; declare parties: Set; + /** A creature always has an AC */ declare armorClass: StatisticDifficultyClass; + /** Skill checks for the creature, built during data prep */ declare skills: Record>; + /** Saving throw rolls for the creature, built during data prep */ - declare saves: Record; + declare saves: CreatureSaves; declare perception: PerceptionStatistic; diff --git a/src/module/actor/creature/saves.ts b/src/module/actor/creature/saves.ts new file mode 100644 index 00000000000..26968eff676 --- /dev/null +++ b/src/module/actor/creature/saves.ts @@ -0,0 +1,53 @@ +import type { SaveType } from "@actor/types.ts"; +import type { Statistic } from "@system/statistic/statistic.ts"; +import { tupleHasValue } from "@util"; +import type { CreaturePF2e } from "./document.ts"; + +/** A record of saving throws with convenience getters for derived data */ +export class CreatureSaves { + constructor(saves: Record>) { + this.fortitude = saves.fortitude; + this.reflex = saves.reflex; + this.will = saves.will; + } + + readonly fortitude: Statistic; + + readonly reflex: Statistic; + + readonly will: Statistic; + + /** + * The highest saving throw factoring in only attribute modifier and proficiency bonus (or base in the case of NPCS) + */ + get baseHighest(): Statistic { + return [this.fortitude, this.reflex, this.will].reduce((highest, candidate) => { + const candidateBase = this.#getBaseValue(candidate); + const highestBase = this.#getBaseValue(highest); + return candidateBase > highestBase ? candidate : highest; + }); + } + + /** + * The highest saving throw factoring in only attribute modifier and proficiency bonus (or base in the case of NPCS) + */ + get baseLowest(): Statistic { + return [this.fortitude, this.reflex, this.will].reduce((lowest, candidate) => { + const candidateBase = this.#getBaseValue(candidate); + const lowestBase = this.#getBaseValue(lowest); + return candidateBase < lowestBase ? candidate : lowest; + }); + } + + /** + * Get the "base" value of a saving-throw statistic, or the sum of attribute modifier and proficiency bonus (or just + * the "base"-slugged modifier in the case of NPCS). + */ + #getBaseValue(statistic: Statistic): number { + const baseTypes = ["ability", "proficiency"] as const; + return statistic.modifiers.reduce( + (b, m) => (tupleHasValue(baseTypes, m.type) || m.slug === "base" ? b + m.value : b), + 0, + ); + } +} diff --git a/src/module/actor/familiar/document.ts b/src/module/actor/familiar/document.ts index 39ed700dd63..a59d44b401e 100644 --- a/src/module/actor/familiar/document.ts +++ b/src/module/actor/familiar/document.ts @@ -1,10 +1,9 @@ import { CreaturePF2e, type CharacterPF2e } from "@actor"; import type { ActorPF2e } from "@actor/base.ts"; -import type { CreatureSaves } from "@actor/creature/data.ts"; import type { CreatureUpdateCallbackOptions } from "@actor/creature/index.ts"; +import { CreatureSaves } from "@actor/creature/saves.ts"; import { createEncounterRollOptions, setHitPointsRollOptions } from "@actor/helpers.ts"; import { Modifier, applyStackingRules } from "@actor/modifiers.ts"; -import type { SaveType } from "@actor/types.ts"; import { SAVE_TYPES } from "@actor/values.ts"; import type { DatabaseDeleteCallbackOptions } from "@common/abstract/_types.d.mts"; import type { ActorUUID } from "@common/documents/_module.d.mts"; @@ -115,29 +114,23 @@ class FamiliarPF2e { - const save = master?.saves[saveType]; - const source = save?.modifiers.filter((m) => !["status", "circumstance"].includes(m.type)) ?? []; - const totalMod = applyStackingRules(source); - const attribute = CONFIG.PF2E.savingThrowDefaultAttributes[saveType]; - const selectors = [saveType, `${attribute}-based`, "saving-throw", "all"]; - const stat = new Statistic(this, { - slug: saveType, - label: _loc(CONFIG.PF2E.saves[saveType]), - domains: selectors, - modifiers: [new Modifier(`PF2E.MasterSavingThrow.${saveType}`, totalMod, "untyped")], - check: { type: "saving-throw" }, - }); - - return { ...partialSaves, [saveType]: stat }; - }, - {} as Record, - ); - system.saves = SAVE_TYPES.reduce( - (partial, saveType) => ({ ...partial, [saveType]: this.saves[saveType].getTraceData() }), - {} as CreatureSaves, - ); + const saves = R.mapToObj(SAVE_TYPES, (saveType) => { + const save = master?.saves[saveType]; + const source = save?.modifiers.filter((m) => !["status", "circumstance"].includes(m.type)) ?? []; + const totalMod = applyStackingRules(source); + const attribute = CONFIG.PF2E.savingThrowDefaultAttributes[saveType]; + const selectors = [saveType, `${attribute}-based`, "saving-throw", "all"]; + const statistic = new Statistic(this, { + slug: saveType, + label: _loc(CONFIG.PF2E.saves[saveType]), + domains: selectors, + modifiers: [new Modifier(`PF2E.MasterSavingThrow.${saveType}`, totalMod, "untyped")], + check: { type: "saving-throw" }, + }); + return [saveType, statistic]; + }); + this.saves = new CreatureSaves(saves); + system.saves = R.mapToObj(SAVE_TYPES, (t) => [t, this.saves[t].getTraceData()]); // Attack const masterLevel = game.pf2e.settings.variants.pwol.enabled ? 0 : level; diff --git a/src/module/actor/npc/document.ts b/src/module/actor/npc/document.ts index 61a23764093..26e24dc22bf 100644 --- a/src/module/actor/npc/document.ts +++ b/src/module/actor/npc/document.ts @@ -3,11 +3,12 @@ import type { ActorUpdateCallbackOptions, ActorUpdateOperation } from "@actor/ba import type { Abilities } from "@actor/creature/data.ts"; import { getHpAdjustment } from "@actor/creature/helpers.ts"; import type { CreatureUpdateCallbackOptions } from "@actor/creature/index.ts"; +import { CreatureSaves } from "@actor/creature/saves.ts"; import { ActorSizePF2e } from "@actor/data/size.ts"; import { attackFromMeleeItem, setHitPointsRollOptions } from "@actor/helpers.ts"; import { ActorInitiative } from "@actor/initiative.ts"; import { Modifier, StatisticModifier } from "@actor/modifiers.ts"; -import type { MovementType, SaveType } from "@actor/types.ts"; +import type { MovementType } from "@actor/types.ts"; import { SAVE_TYPES } from "@actor/values.ts"; import type { UserAction } from "@common/constants.d.mts"; import type { ItemPF2e, MeleePF2e } from "@item"; @@ -301,16 +302,12 @@ class NPCPF2e> = {}; - for (const saveType of SAVE_TYPES) { + const saves = R.mapToObj(SAVE_TYPES, (saveType) => { const save = system.saves[saveType]; const saveName = _loc(CONFIG.PF2E.saves[saveType]); const base = save.value; const attribute = save.attribute; const domains = [saveType, `${attribute}-based`, "saving-throw", "all"]; - const statistic = new Statistic(this, { slug: saveType, label: saveName, @@ -327,13 +324,11 @@ class NPCPF2e; + return [saveType, statistic]; + }); + this.saves = new CreatureSaves(saves); } private prepareSkills() {