Skip to content

Commit 1d32464

Browse files
committed
Add system changes to facilitate Whirling Swipe
1 parent 66e5e9f commit 1d32464

File tree

7 files changed

+129
-68
lines changed

7 files changed

+129
-68
lines changed

src/module/actor/creature/document.ts

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -95,28 +95,17 @@ abstract class CreaturePF2e<
9595
*/
9696
override getReach({ action = "interact", weapon = null }: GetReachParameters = {}): number {
9797
const baseReach = this.attributes.reach.base;
98-
const weaponReach = weapon?.isOfType("melee") ? weapon.reach : null;
9998

10099
if (action === "interact" || this.type === "familiar") {
101-
return baseReach;
102-
} else if (typeof weaponReach === "number") {
103-
return weaponReach;
100+
return this.attributes.reach.base;
101+
} else if (weapon?.isOfType("melee", "weapon")) {
102+
return weapon.reach ?? baseReach;
104103
} else {
105-
const attacks: { item: ItemPF2e<ActorPF2e>; ready: boolean }[] = weapon
106-
? [{ item: weapon, ready: true }]
107-
: (this.system.actions ?? []);
108-
const readyAttacks = attacks.filter((a) => a.ready);
109-
const traitsFromItems = readyAttacks.map((a) => new Set(a.item.system.traits?.value ?? []));
110-
if (traitsFromItems.length === 0) return baseReach;
111-
112-
const reaches = traitsFromItems.map((traits): number => {
113-
if (setHasElement(traits, "reach")) return baseReach + 5;
114-
115-
const reachNPattern = /^reach-\d{1,3}$/;
116-
return Number([...traits].find((t) => reachNPattern.test(t))?.replace("reach-", "")) || baseReach;
117-
});
118-
119-
return Math.max(...reaches);
104+
const readiedItems = this.system.actions?.filter((a) => a.ready).map((a) => a.item) ?? [];
105+
return Math.max(
106+
baseReach,
107+
...readiedItems.map((i) => i.reach).filter((r): r is number => typeof r === "number"),
108+
);
120109
}
121110
}
122111

src/module/item/melee/document.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class MeleePF2e<TParent extends ActorPF2e | null = ActorPF2e | null> extends Ite
4949
return Number(this.system.bonus.value) || 0;
5050
}
5151

52+
/** The reach of this npc attack when they attack with it */
5253
get reach(): number | null {
5354
if (this.isRanged) return null;
5455
const reachTrait = this.system.traits.value.find((t) => /^reach-\d+$/.test(t));

src/module/item/weapon/document.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,17 @@ class WeaponPF2e<TParent extends ActorPF2e | null = ActorPF2e | null> extends Ph
105105
return this.system.maxRange ?? (this.system.range ? this.system.range * 6 : null);
106106
}
107107

108+
/** The reach of this weapon when using it. */
109+
get reach(): number | null {
110+
if (!this.isMelee) return null;
111+
112+
const actor = this.actor;
113+
const baseReach = actor?.isOfType("creature") ? (actor.attributes.reach?.base ?? 5) : 5;
114+
const traits = this.system.traits.value;
115+
const reachIncrease = traits.includes("reach") ? 5 : 0;
116+
return baseReach + reachIncrease;
117+
}
118+
108119
/** A single object containing range increment and maximum */
109120
get range(): RangeData | null {
110121
const rangeIncrement = this.system.range;

src/module/rules/rule-element/base.ts

Lines changed: 63 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -256,28 +256,50 @@ abstract class RuleElementPF2e<TSchema extends RuleElementSchema = RuleElementSc
256256
"rule",
257257
...Object.keys(injectables).filter((i) => /^[a-z][a-z]+$/g.test(i)),
258258
];
259-
const pattern = new RegExp(String.raw`{(${injectableKeys.join("|")})\|(.*?)}`, "g");
259+
260+
const injectablePattern = new RegExp(String.raw`^(${injectableKeys.join("|")})\|(.*?)$`, "g");
260261
const allInjectables: Record<string, object> = {
261262
actor: this.actor,
262263
item: this.item,
263264
rule: this,
264265
...injectables,
265266
};
266-
return source.replace(pattern, (_match, key: string, prop: string) => {
267-
const data = allInjectables[key];
268-
const value = (() => {
269-
// In case of formerly deprecated paths upstream now throws on
270-
try {
271-
return fu.getProperty(data, prop);
272-
} catch {
273-
return undefined;
267+
268+
const bracketPattern = /{([^{}]+)}/g;
269+
return source.replace(bracketPattern, (match, value) => {
270+
// Support {@actor.level} type stuff
271+
if (value.includes("@")) {
272+
const result = this.#saferEval(
273+
Roll.replaceFormulaData(value, {
274+
...this.item.getRollData(),
275+
rule: this,
276+
...injectables,
277+
}),
278+
{ warn, defaultValue: match },
279+
);
280+
return String(result);
281+
}
282+
283+
const injectableMatch = injectablePattern.exec(value);
284+
if (injectableMatch) {
285+
const [_, key, prop] = injectableMatch;
286+
const data = allInjectables[key];
287+
const value = (() => {
288+
// In case of formerly deprecated paths upstream now throws on
289+
try {
290+
return fu.getProperty(data, prop);
291+
} catch {
292+
return undefined;
293+
}
294+
})();
295+
if (value === undefined) {
296+
this.ignored = true;
297+
if (warn) this.failValidation(`Failed to resolve injected property "${prop}" in "${source}"`);
274298
}
275-
})();
276-
if (value === undefined) {
277-
this.ignored = true;
278-
if (warn) this.failValidation(`Failed to resolve injected property "${prop}" in "${source}"`);
299+
return String(value);
279300
}
280-
return String(value);
301+
302+
return match;
281303
});
282304
}
283305

@@ -308,7 +330,7 @@ abstract class RuleElementPF2e<TSchema extends RuleElementSchema = RuleElementSc
308330
if (typeof value === "number" || typeof value === "boolean" || value === null) {
309331
return value;
310332
}
311-
value = this.resolveInjectedProperties(value, { warn });
333+
value = this.resolveInjectedProperties(value, { warn, injectables: resolvables });
312334

313335
if (Array.isArray(value)) return value;
314336

@@ -324,48 +346,51 @@ abstract class RuleElementPF2e<TSchema extends RuleElementSchema = RuleElementSc
324346
}
325347

326348
if (typeof resolvedFromBracket === "string") {
327-
const saferEval = (formula: string): number => {
328-
try {
329-
// If any resolvables were not provided for this formula, return the default value
330-
const unresolveds = formula.match(/@[a-z0-9.]+/gi) ?? [];
331-
// Allow failure of "@target" and "@actor.conditions" with no warning
332-
if (unresolveds.length > 0) {
333-
const shouldWarn =
334-
warn &&
335-
!unresolveds.every((u) => u.startsWith("@target.") || u.startsWith("@actor.conditions."));
336-
this.ignored = true;
337-
if (shouldWarn) {
338-
this.failValidation(`unable to resolve formula, "${formula}"`);
339-
}
340-
return Number(defaultValue);
341-
}
342-
return Roll.safeEval(formula);
343-
} catch {
344-
this.failValidation(`unable to evaluate formula, "${formula}"`);
345-
return 0;
346-
}
347-
};
348-
349349
// Include worn armor as resolvable for PCs since there is guaranteed to be no more than one
350350
if (this.actor.isOfType("character")) {
351351
resolvables.armor = this.actor.wornArmor;
352352
}
353353

354354
const trimmed = resolvedFromBracket.trim();
355355
return (trimmed.includes("@") || /^-?\d+$/.test(trimmed)) && evaluate
356-
? saferEval(
356+
? this.#saferEval(
357357
Roll.replaceFormulaData(trimmed, {
358358
...this.actor.getRollData(),
359359
item: this.item,
360360
...resolvables,
361361
}),
362+
{ warn, defaultValue },
362363
)
363364
: trimmed;
364365
}
365366

366367
return defaultValue;
367368
}
368369

370+
#saferEval(
371+
formula: string,
372+
{ warn, defaultValue }: { warn: boolean; defaultValue: Exclude<RuleValue, BracketedValue> | null },
373+
): number {
374+
try {
375+
// If any resolvables were not provided for this formula, return the default value
376+
const unresolveds = formula.match(/@[a-z0-9.]+/gi) ?? [];
377+
// Allow failure of "@target" and "@actor.conditions" with no warning
378+
if (unresolveds.length > 0) {
379+
const shouldWarn =
380+
warn && !unresolveds.every((u) => u.startsWith("@target.") || u.startsWith("@actor.conditions."));
381+
this.ignored = true;
382+
if (shouldWarn) {
383+
this.failValidation(`unable to resolve formula, "${formula}"`);
384+
}
385+
return Number(defaultValue);
386+
}
387+
return Roll.safeEval(formula);
388+
} catch {
389+
this.failValidation(`unable to evaluate formula, "${formula}"`);
390+
return 0;
391+
}
392+
}
393+
369394
protected isBracketedValue(value: unknown): value is BracketedValue {
370395
return isBracketedValue(value);
371396
}

src/module/rules/rule-element/item-alteration/alteration.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ class ItemAlteration extends foundry.abstract.DataModel<RuleElementPF2e, ItemAlt
5050
"traits",
5151
] as const;
5252

53+
static VALID_PHASES = {
54+
"pd-recovery-dc": ["afterDerived"],
55+
// Much prep like strikes and armor occur before afterDerived, and the traits must be set by then
56+
traits: ["applyAEs", "beforeDerived"],
57+
} as const;
58+
5359
static override defineSchema(): ItemAlterationSchema {
5460
return {
5561
mode: new fields.StringField({
@@ -88,7 +94,9 @@ class ItemAlteration extends foundry.abstract.DataModel<RuleElementPF2e, ItemAlt
8894
alteration: {
8995
mode: this.mode,
9096
itemType: item.type,
91-
value: (this.value = this.parent.resolveValue(this.value, fallbackValue)),
97+
value: (this.value = this.parent.resolveValue(this.value, fallbackValue, {
98+
resolvables: { targetItem: item },
99+
})),
92100
},
93101
};
94102
if (this.parent.ignored) return;

src/module/rules/rule-element/item-alteration/rule-element.ts

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import type { ActorPF2e } from "@actor";
22
import type { ItemPF2e, PhysicalItemPF2e } from "@item";
33
import type { ItemType } from "@item/base/data/index.ts";
44
import { PHYSICAL_ITEM_TYPES } from "@item/physical/values.ts";
5+
import { objectHasKey } from "@util";
56
import * as R from "remeda";
6-
import { AELikeRuleElement } from "../ae-like.ts";
7+
import { AELikeDataPrepPhase, AELikeRuleElement } from "../ae-like.ts";
78
import { RuleElementPF2e } from "../base.ts";
89
import type { ModelPropsFromRESchema, RuleElementSchema } from "../data.ts";
910
import { ItemAlteration, ItemAlterationSchema } from "./alteration.ts";
@@ -34,6 +35,15 @@ class ItemAlterationRuleElement extends RuleElementPF2e<ItemAlterationRuleSchema
3435
choices: itemTypeChoices,
3536
initial: undefined,
3637
}),
38+
phase: new fields.StringField({
39+
required: true,
40+
choices: ["applyAEs", "beforeDerived", "afterDerived"],
41+
initial: (data: unknown) => {
42+
const validPhasesMap = ItemAlteration.VALID_PHASES;
43+
const property = R.isPlainObject(data) ? data["property"] : null;
44+
return objectHasKey(validPhasesMap, property) ? validPhasesMap[property][0] : "applyAEs";
45+
},
46+
}),
3747
...ItemAlteration.defineSchema(),
3848
};
3949
}
@@ -44,10 +54,17 @@ class ItemAlterationRuleElement extends RuleElementPF2e<ItemAlterationRuleSchema
4454
if (!data.itemId && !data.itemType) {
4555
throw Error("one of itemId and itemType must be defined");
4656
}
47-
}
4857

49-
/** Alteration properties that should be processed at the end of data preparation */
50-
static #DELAYED_PROPERTIES = ["pd-recovery-dc"];
58+
// Validate that the phase is supported by the property
59+
const { property, phase } = data;
60+
const validPhasesMap = ItemAlteration.VALID_PHASES;
61+
const validPhases: readonly ItemAlterationPrepPhase[] = objectHasKey(validPhasesMap, property)
62+
? validPhasesMap[property]
63+
: (["applyAEs"] as const);
64+
if (phase && !validPhases.includes(phase)) {
65+
throw Error(`phase: ${phase} is not a valid choice, must be one of ${validPhases.join(", ")}`);
66+
}
67+
}
5168

5269
/** Alteration properties that should only be processed when requested directly */
5370
static #LAZY_PROPERTIES = ["description"];
@@ -93,16 +110,22 @@ class ItemAlterationRuleElement extends RuleElementPF2e<ItemAlterationRuleSchema
93110
}
94111

95112
override onApplyActiveEffects(): void {
96-
this.actor.synthetics.itemAlterations.push(this);
97-
const isDelayed = this.constructor.#DELAYED_PROPERTIES.includes(this.property);
98-
if (!this.isLazy && !isDelayed) {
113+
if (!this.isLazy && this.phase === "applyAEs") {
114+
this.actor.synthetics.itemAlterations.push(this);
115+
this.applyAlteration();
116+
}
117+
}
118+
119+
override beforePrepareData(): void {
120+
if (!this.isLazy && this.phase === "beforeDerived") {
121+
this.actor.synthetics.itemAlterations.push(this);
99122
this.applyAlteration();
100123
}
101124
}
102125

103126
override afterPrepareData(): void {
104-
const isDelayed = this.constructor.#DELAYED_PROPERTIES.includes(this.property);
105-
if (!this.isLazy && isDelayed) {
127+
if (!this.isLazy && this.phase === "afterDerived") {
128+
this.actor.synthetics.itemAlterations.push(this);
106129
this.applyAlteration();
107130
}
108131
}
@@ -175,12 +198,16 @@ interface ItemAlterationRuleElement
175198
constructor: typeof ItemAlterationRuleElement;
176199
}
177200

201+
type ItemAlterationPrepPhase = Exclude<AELikeDataPrepPhase, "beforeRoll">;
202+
178203
type ItemAlterationRuleSchema = RuleElementSchema &
179204
ItemAlterationSchema & {
180205
/** The type of items to alter */
181206
itemType: fields.StringField<ItemType, ItemType, false, false, false>;
182207
/** As an alternative to specifying item types, an exact item ID can be provided */
183208
itemId: fields.StringField<string, string, false, false, false>;
209+
/** The phase to run the alteration in. Most only support applyAEs */
210+
phase: fields.StringField<ItemAlterationPrepPhase, ItemAlterationPrepPhase, true, false, true>;
184211
};
185212

186213
interface ApplyAlterationOptions {

src/module/rules/rule-element/item-alteration/schemas.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { ItemSourcePF2e, ItemType } from "@item/base/data/index.ts";
44
import type { ItemTrait } from "@item/base/types.ts";
55
import { itemIsOfType } from "@item/helpers.ts";
66
import { PHYSICAL_ITEM_TYPES, PRECIOUS_MATERIAL_TYPES } from "@item/physical/values.ts";
7+
import { MANDATORY_RANGED_GROUPS } from "@item/weapon/values.ts";
78
import { RARITIES } from "@module/data.ts";
89
import { DamageRoll } from "@system/damage/roll.ts";
910
import type { DamageDiceFaces, DamageType } from "@system/damage/types.ts";
@@ -14,7 +15,6 @@ import * as R from "remeda";
1415
import type { AELikeChangeMode } from "../ae-like.ts";
1516
import fields = foundry.data.fields;
1617
import validation = foundry.data.validation;
17-
import { MANDATORY_RANGED_GROUPS } from "@item/weapon/values.ts";
1818

1919
/** A `SchemaField` reappropriated for validation of specific item alterations */
2020
class ItemAlterationValidator<TSchema extends AlterationSchema> extends fields.SchemaField<TSchema> {

0 commit comments

Comments
 (0)