Add PF2e attack effects, ability frequency, and perception details
Show inline on-hit effects on attack lines (e.g., "plus Grab"), frequency limits on abilities (e.g., "(1/day)"), and perception details text alongside senses. Strip redundant frequency lines from Foundry descriptions. Also add resilient PF2e source fetching: batched requests with retry, graceful handling of ad-blocker-blocked creature files (partial success with toast warning and re-fetch prompt for missing creatures). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -131,6 +131,39 @@ describe("normalizeFoundryCreature", () => {
|
||||
);
|
||||
expect(creature.senses).toBe("Scent 60 feet");
|
||||
});
|
||||
|
||||
it("extracts perception details", () => {
|
||||
const creature = normalizeFoundryCreature(
|
||||
minimalCreature({
|
||||
system: {
|
||||
...minimalCreature().system,
|
||||
perception: {
|
||||
mod: 35,
|
||||
details: "smoke vision",
|
||||
senses: [{ type: "darkvision" }],
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(creature.perceptionDetails).toBe("smoke vision");
|
||||
expect(creature.senses).toBe("Darkvision");
|
||||
});
|
||||
|
||||
it("omits perception details when empty", () => {
|
||||
const creature = normalizeFoundryCreature(
|
||||
minimalCreature({
|
||||
system: {
|
||||
...minimalCreature().system,
|
||||
perception: {
|
||||
mod: 8,
|
||||
details: "",
|
||||
senses: [{ type: "darkvision" }],
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(creature.perceptionDetails).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("languages formatting", () => {
|
||||
@@ -386,6 +419,101 @@ describe("normalizeFoundryCreature", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("includes attack effects in damage text", () => {
|
||||
const creature = normalizeFoundryCreature(
|
||||
minimalCreature({
|
||||
items: [
|
||||
{
|
||||
_id: "atk1",
|
||||
name: "talon",
|
||||
type: "melee",
|
||||
system: {
|
||||
bonus: { value: 14 },
|
||||
damageRolls: {
|
||||
abc: {
|
||||
damage: "1d10+6",
|
||||
damageType: "piercing",
|
||||
},
|
||||
},
|
||||
traits: { value: [] },
|
||||
attackEffects: { value: ["grab"] },
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
const attack = creature.attacks?.[0];
|
||||
expect(attack?.segments[0]).toEqual({
|
||||
type: "text",
|
||||
value: "+14, 1d10+6 piercing plus Grab",
|
||||
});
|
||||
});
|
||||
|
||||
it("joins multiple attack effects with 'and'", () => {
|
||||
const creature = normalizeFoundryCreature(
|
||||
minimalCreature({
|
||||
items: [
|
||||
{
|
||||
_id: "atk1",
|
||||
name: "claw",
|
||||
type: "melee",
|
||||
system: {
|
||||
bonus: { value: 18 },
|
||||
damageRolls: {
|
||||
abc: {
|
||||
damage: "2d8+6",
|
||||
damageType: "slashing",
|
||||
},
|
||||
},
|
||||
traits: { value: [] },
|
||||
attackEffects: {
|
||||
value: ["grab", "knockdown"],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
const attack = creature.attacks?.[0];
|
||||
expect(attack?.segments[0]).toEqual({
|
||||
type: "text",
|
||||
value: "+18, 2d8+6 slashing plus Grab and Knockdown",
|
||||
});
|
||||
});
|
||||
|
||||
it("strips creature-name prefix from attack effect slugs", () => {
|
||||
const creature = normalizeFoundryCreature(
|
||||
minimalCreature({
|
||||
name: "Lich",
|
||||
items: [
|
||||
{
|
||||
_id: "atk1",
|
||||
name: "hand",
|
||||
type: "melee",
|
||||
system: {
|
||||
bonus: { value: 24 },
|
||||
damageRolls: {
|
||||
abc: {
|
||||
damage: "2d12+7",
|
||||
damageType: "negative",
|
||||
},
|
||||
},
|
||||
traits: { value: [] },
|
||||
attackEffects: {
|
||||
value: ["lich-siphon-life"],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
const attack = creature.attacks?.[0];
|
||||
expect(attack?.segments[0]).toEqual({
|
||||
type: "text",
|
||||
value: "+24, 2d12+7 negative plus Siphon Life",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("ability normalization", () => {
|
||||
@@ -539,6 +667,114 @@ describe("normalizeFoundryCreature", () => {
|
||||
: undefined,
|
||||
).toBe("(Concentrate, Polymorph) Takes a new form.");
|
||||
});
|
||||
|
||||
it("extracts frequency from ability", () => {
|
||||
const creature = normalizeFoundryCreature(
|
||||
minimalCreature({
|
||||
items: [
|
||||
{
|
||||
_id: "a1",
|
||||
name: "Drain Soul Cage",
|
||||
type: "action",
|
||||
system: {
|
||||
category: "offensive",
|
||||
actionType: { value: "free" },
|
||||
actions: { value: null },
|
||||
traits: { value: [] },
|
||||
description: { value: "<p>Drains the soul.</p>" },
|
||||
frequency: { max: 1, per: "day" },
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
expect(creature.abilitiesBot?.[0]?.frequency).toBe("1/day");
|
||||
});
|
||||
|
||||
it("strips redundant frequency line from description", () => {
|
||||
const creature = normalizeFoundryCreature(
|
||||
minimalCreature({
|
||||
items: [
|
||||
{
|
||||
_id: "a1",
|
||||
name: "Consult the Text",
|
||||
type: "action",
|
||||
system: {
|
||||
category: "offensive",
|
||||
actionType: { value: "action" },
|
||||
actions: { value: 1 },
|
||||
traits: { value: [] },
|
||||
description: {
|
||||
value:
|
||||
"<p><strong>Frequency</strong> once per day</p>\n<hr />\n<p><strong>Effect</strong> The lich opens their spell tome.</p>",
|
||||
},
|
||||
frequency: { max: 1, per: "day" },
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
const text =
|
||||
creature.abilitiesBot?.[0]?.segments[0]?.type === "text"
|
||||
? creature.abilitiesBot[0].segments[0].value
|
||||
: "";
|
||||
expect(text).not.toContain("Frequency");
|
||||
expect(text).toContain("The lich opens their spell tome.");
|
||||
});
|
||||
|
||||
it("strips frequency line even when preceded by other text", () => {
|
||||
const creature = normalizeFoundryCreature(
|
||||
minimalCreature({
|
||||
items: [
|
||||
{
|
||||
_id: "a1",
|
||||
name: "Drain Soul Cage",
|
||||
type: "action",
|
||||
system: {
|
||||
category: "offensive",
|
||||
actionType: { value: "free" },
|
||||
actions: { value: null },
|
||||
traits: { value: [] },
|
||||
description: {
|
||||
value:
|
||||
"<p>6th rank</p>\n<hr />\n<p><strong>Frequency</strong> once per day</p>\n<hr />\n<p><strong>Effect</strong> The lich taps into their soul cage.</p>",
|
||||
},
|
||||
frequency: { max: 1, per: "day" },
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
const text =
|
||||
creature.abilitiesBot?.[0]?.segments[0]?.type === "text"
|
||||
? creature.abilitiesBot[0].segments[0].value
|
||||
: "";
|
||||
expect(text).not.toContain("Frequency");
|
||||
expect(text).toContain("6th rank");
|
||||
expect(text).toContain("The lich taps into their soul cage.");
|
||||
});
|
||||
|
||||
it("omits frequency when not present", () => {
|
||||
const creature = normalizeFoundryCreature(
|
||||
minimalCreature({
|
||||
items: [
|
||||
{
|
||||
_id: "a1",
|
||||
name: "Strike",
|
||||
type: "action",
|
||||
system: {
|
||||
category: "offensive",
|
||||
actionType: { value: "action" },
|
||||
actions: { value: 1 },
|
||||
traits: { value: [] },
|
||||
description: { value: "<p>Strikes.</p>" },
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
expect(creature.abilitiesBot?.[0]?.frequency).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("equipment normalization", () => {
|
||||
|
||||
Reference in New Issue
Block a user