Compare commits

..

5 Commits
0.9.23 ... main

Author SHA1 Message Date
Lukas
57278e0c82 Add PF2e action cost icons to ability names
All checks were successful
CI / check (push) Successful in 2m21s
CI / build-image (push) Successful in 17s
Show Unicode action icons (◆/◆◆/◆◆◆ for actions, ◇ for free,
↺ for reaction) in ability names from the activity field.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 16:31:24 +02:00
Lukas
f9cfaa2570 Include traits on PF2e ability blocks
Parse and display traits (concentrate, divine, polymorph, etc.)
on ability entries, matching how attack traits are already shown.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 16:29:08 +02:00
Lukas
3e62e54274 Strip all angle brackets in PF2e attack traits and damage
All checks were successful
CI / check (push) Successful in 2m23s
CI / build-image (push) Successful in 17s
Broaden stripDiceBrackets to stripAngleBrackets to handle all
PF2e tools angle-bracket formatting (e.g. <10 feet>, <15 feet>),
not just dice notation. Also strip in damage text.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 15:34:28 +02:00
Lukas
12a089dfd7 Fix PF2e condition tooltip descriptions and sort picker alphabetically
Correct inaccurate PF2e condition descriptions against official AoN
rules (blinded, deafened, confused, grabbed, hidden, paralyzed,
unconscious, drained, fascinated, enfeebled, stunned). Sort condition
picker alphabetically per game system.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 14:29:49 +02:00
Lukas
65e4db153b Fix PF2e stat block senses and attack trait rendering
All checks were successful
CI / check (push) Successful in 2m20s
CI / build-image (push) Successful in 17s
- Format senses with type (imprecise/precise) and range in feet,
  and strip {@ability} tags (e.g. tremorsense)
- Strip angle-bracket dice notation in attack traits (<d8> → d8)
- Fix existing weakness/resistance tests to nest under defenses
- Fix non-null assertions in 5e bestiary adapter tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 12:23:08 +02:00
4 changed files with 336 additions and 47 deletions

View File

@@ -6,7 +6,8 @@ import {
} from "../bestiary-adapter.js"; } from "../bestiary-adapter.js";
/** Flatten segments to a single string for simple text assertions. */ /** Flatten segments to a single string for simple text assertions. */
function flatText(trait: TraitBlock): string { function flatText(trait: TraitBlock | undefined): string {
if (!trait) return "";
return trait.segments return trait.segments
.map((s) => .map((s) =>
s.type === "text" s.type === "text"
@@ -88,11 +89,11 @@ describe("normalizeBestiary", () => {
expect(c.senses).toBe("Darkvision 60 ft."); expect(c.senses).toBe("Darkvision 60 ft.");
expect(c.languages).toBe("Common, Goblin"); expect(c.languages).toBe("Common, Goblin");
expect(c.actions).toHaveLength(1); expect(c.actions).toHaveLength(1);
expect(flatText(c.actions![0])).toContain("Melee Attack Roll:"); expect(flatText(c.actions?.[0])).toContain("Melee Attack Roll:");
expect(flatText(c.actions![0])).not.toContain("{@"); expect(flatText(c.actions?.[0])).not.toContain("{@");
expect(c.bonusActions).toHaveLength(1); expect(c.bonusActions).toHaveLength(1);
expect(flatText(c.bonusActions![0])).toContain("Disengage"); expect(flatText(c.bonusActions?.[0])).toContain("Disengage");
expect(flatText(c.bonusActions![0])).not.toContain("{@"); expect(flatText(c.bonusActions?.[0])).not.toContain("{@");
}); });
it("normalizes a creature with legendary actions", () => { it("normalizes a creature with legendary actions", () => {
@@ -347,9 +348,9 @@ describe("normalizeBestiary", () => {
const creatures = normalizeBestiary(raw); const creatures = normalizeBestiary(raw);
const bite = creatures[0].actions?.[0]; const bite = creatures[0].actions?.[0];
expect(flatText(bite!)).toContain("Melee Weapon Attack:"); expect(flatText(bite)).toContain("Melee Weapon Attack:");
expect(flatText(bite!)).not.toContain("mw"); expect(flatText(bite)).not.toContain("mw");
expect(flatText(bite!)).not.toContain("{@"); expect(flatText(bite)).not.toContain("{@");
}); });
it("handles fly speed with hover condition", () => { it("handles fly speed with hover condition", () => {
@@ -438,14 +439,15 @@ describe("normalizeBestiary", () => {
}; };
const creatures = normalizeBestiary(raw); const creatures = normalizeBestiary(raw);
const trait = creatures[0].traits![0]; const trait = creatures[0].traits?.[0];
expect(trait.name).toBe("Confusing Burble"); expect(trait).toBeDefined();
expect(trait.segments).toHaveLength(2); expect(trait?.name).toBe("Confusing Burble");
expect(trait.segments[0]).toEqual({ expect(trait?.segments).toHaveLength(2);
expect(trait?.segments[0]).toEqual({
type: "text", type: "text",
value: expect.stringContaining("d4"), value: expect.stringContaining("d4"),
}); });
expect(trait.segments[1]).toEqual({ expect(trait?.segments[1]).toEqual({
type: "list", type: "list",
items: [ items: [
{ label: "1-2", text: "The creature does nothing." }, { label: "1-2", text: "The creature does nothing." },
@@ -498,8 +500,9 @@ describe("normalizeBestiary", () => {
}; };
const creatures = normalizeBestiary(raw); const creatures = normalizeBestiary(raw);
const trait = creatures[0].traits![0]; const trait = creatures[0].traits?.[0];
expect(trait.segments[1]).toEqual({ expect(trait).toBeDefined();
expect(trait?.segments[1]).toEqual({
type: "list", type: "list",
items: [ items: [
{ label: "1", text: "Nothing happens." }, { label: "1", text: "Nothing happens." },

View File

@@ -1,11 +1,11 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { normalizePf2eBestiary } from "../pf2e-bestiary-adapter.js"; import { normalizePf2eBestiary } from "../pf2e-bestiary-adapter.js";
function minimalCreature(defenses?: Record<string, unknown>) { function minimalCreature(overrides?: Record<string, unknown>) {
return { return {
name: "Test Creature", name: "Test Creature",
source: "TST", source: "TST",
defenses, ...overrides,
}; };
} }
@@ -15,7 +15,9 @@ describe("normalizePf2eBestiary", () => {
const [creature] = normalizePf2eBestiary({ const [creature] = normalizePf2eBestiary({
creature: [ creature: [
minimalCreature({ minimalCreature({
weaknesses: [{ name: "fire", amount: 5 }], defenses: {
weaknesses: [{ name: "fire", amount: 5 }],
},
}), }),
], ],
}); });
@@ -26,7 +28,9 @@ describe("normalizePf2eBestiary", () => {
const [creature] = normalizePf2eBestiary({ const [creature] = normalizePf2eBestiary({
creature: [ creature: [
minimalCreature({ minimalCreature({
weaknesses: [{ name: "smoke susceptibility" }], defenses: {
weaknesses: [{ name: "smoke susceptibility" }],
},
}), }),
], ],
}); });
@@ -37,9 +41,11 @@ describe("normalizePf2eBestiary", () => {
const [creature] = normalizePf2eBestiary({ const [creature] = normalizePf2eBestiary({
creature: [ creature: [
minimalCreature({ minimalCreature({
weaknesses: [ defenses: {
{ name: "cold iron", amount: 5, note: "except daggers" }, weaknesses: [
], { name: "cold iron", amount: 5, note: "except daggers" },
],
},
}), }),
], ],
}); });
@@ -50,7 +56,9 @@ describe("normalizePf2eBestiary", () => {
const [creature] = normalizePf2eBestiary({ const [creature] = normalizePf2eBestiary({
creature: [ creature: [
minimalCreature({ minimalCreature({
weaknesses: [{ name: "smoke susceptibility", note: "see below" }], defenses: {
weaknesses: [{ name: "smoke susceptibility", note: "see below" }],
},
}), }),
], ],
}); });
@@ -65,12 +73,237 @@ describe("normalizePf2eBestiary", () => {
}); });
}); });
describe("senses formatting", () => {
it("strips tags and includes type and range", () => {
const [creature] = normalizePf2eBestiary({
creature: [
minimalCreature({
senses: [
{
type: "imprecise",
name: "{@ability tremorsense}",
range: 30,
},
],
}),
],
});
expect(creature.senses).toBe("Tremorsense (imprecise) 30 feet");
});
it("formats sense with only a name", () => {
const [creature] = normalizePf2eBestiary({
creature: [
minimalCreature({
senses: [{ name: "darkvision" }],
}),
],
});
expect(creature.senses).toBe("Darkvision");
});
it("formats sense with name and range but no type", () => {
const [creature] = normalizePf2eBestiary({
creature: [
minimalCreature({
senses: [{ name: "scent", range: 60 }],
}),
],
});
expect(creature.senses).toBe("Scent 60 feet");
});
});
describe("attack formatting", () => {
it("strips angle brackets from traits", () => {
const [creature] = normalizePf2eBestiary({
creature: [
minimalCreature({
attacks: [
{
name: "stinger",
range: "Melee",
attack: 11,
traits: ["deadly <d8>"],
damage: "1d6+4 piercing",
},
],
}),
],
});
const attack = creature.attacks?.[0];
expect(attack).toBeDefined();
expect(attack?.segments[0]).toEqual(
expect.objectContaining({
type: "text",
value: expect.stringContaining("(deadly d8)"),
}),
);
});
it("strips angle brackets from reach values in traits", () => {
const [creature] = normalizePf2eBestiary({
creature: [
minimalCreature({
attacks: [
{
name: "tentacle",
range: "Melee",
attack: 18,
traits: ["agile", "chaotic", "magical", "reach <10 feet>"],
damage: "2d8+6 piercing",
},
],
}),
],
});
const attack = creature.attacks?.[0];
expect(attack).toBeDefined();
expect(attack?.segments[0]).toEqual(
expect.objectContaining({
type: "text",
value: expect.stringContaining(
"(agile, chaotic, magical, reach 10 feet)",
),
}),
);
});
});
describe("ability formatting", () => {
it("includes traits from abilities in the text", () => {
const [creature] = normalizePf2eBestiary({
creature: [
minimalCreature({
abilities: {
bot: [
{
name: "Change Shape",
activity: { number: 1, unit: "action" },
traits: [
"concentrate",
"divine",
"polymorph",
"transmutation",
],
entries: [
"The naunet can take the appearance of any creature.",
],
},
],
},
}),
],
});
const ability = creature.abilitiesBot?.[0];
expect(ability).toBeDefined();
expect(ability?.name).toBe("\u25C6 Change Shape");
expect(ability?.segments[0]).toEqual(
expect.objectContaining({
type: "text",
value: expect.stringContaining(
"(Concentrate, Divine, Polymorph, Transmutation)",
),
}),
);
});
it("shows free action icon", () => {
const [creature] = normalizePf2eBestiary({
creature: [
minimalCreature({
abilities: {
bot: [
{
name: "Adaptive Strike",
activity: { number: 1, unit: "free" },
entries: ["The naunet chooses adamantine."],
},
],
},
}),
],
});
expect(creature.abilitiesBot?.[0]?.name).toBe("\u25C7 Adaptive Strike");
});
it("shows reaction icon", () => {
const [creature] = normalizePf2eBestiary({
creature: [
minimalCreature({
abilities: {
mid: [
{
name: "Attack of Opportunity",
activity: { number: 1, unit: "reaction" },
entries: ["Trigger description."],
},
],
},
}),
],
});
expect(creature.abilitiesMid?.[0]?.name).toBe(
"\u21BA Attack of Opportunity",
);
});
it("shows multi-action icons", () => {
const [creature] = normalizePf2eBestiary({
creature: [
minimalCreature({
abilities: {
bot: [
{
name: "Breath Weapon",
activity: { number: 2, unit: "action" },
entries: ["Fire breath."],
},
],
},
}),
],
});
expect(creature.abilitiesBot?.[0]?.name).toBe(
"\u25C6\u25C6 Breath Weapon",
);
});
it("renders ability without activity or traits normally", () => {
const [creature] = normalizePf2eBestiary({
creature: [
minimalCreature({
abilities: {
bot: [
{
name: "Constrict",
entries: ["1d8+8 bludgeoning, DC 26"],
},
],
},
}),
],
});
const ability = creature.abilitiesBot?.[0];
expect(ability).toBeDefined();
expect(ability?.name).toBe("Constrict");
expect(ability?.segments[0]).toEqual(
expect.objectContaining({
type: "text",
value: "1d8+8 bludgeoning, DC 26",
}),
);
});
});
describe("resistances formatting", () => { describe("resistances formatting", () => {
it("formats resistance without amount", () => { it("formats resistance without amount", () => {
const [creature] = normalizePf2eBestiary({ const [creature] = normalizePf2eBestiary({
creature: [ creature: [
minimalCreature({ minimalCreature({
resistances: [{ name: "physical" }], defenses: {
resistances: [{ name: "physical" }],
},
}), }),
], ],
}); });
@@ -81,7 +314,9 @@ describe("normalizePf2eBestiary", () => {
const [creature] = normalizePf2eBestiary({ const [creature] = normalizePf2eBestiary({
creature: [ creature: [
minimalCreature({ minimalCreature({
resistances: [{ name: "fire", amount: 10 }], defenses: {
resistances: [{ name: "fire", amount: 10 }],
},
}), }),
], ],
}); });

View File

@@ -15,7 +15,7 @@ interface RawPf2eCreature {
level?: number; level?: number;
traits?: string[]; traits?: string[];
perception?: { std?: number }; perception?: { std?: number };
senses?: { name?: string; type?: string }[]; senses?: { name?: string; type?: string; range?: number }[];
languages?: { languages?: string[] }; languages?: { languages?: string[] };
skills?: Record<string, { std?: number }>; skills?: Record<string, { std?: number }>;
abilityMods?: Record<string, number>; abilityMods?: Record<string, number>;
@@ -46,6 +46,8 @@ interface RawDefenses {
interface RawAbility { interface RawAbility {
name?: string; name?: string;
activity?: { number?: number; unit?: string };
traits?: string[];
entries?: RawEntry[]; entries?: RawEntry[];
} }
@@ -79,6 +81,26 @@ function capitalize(s: string): string {
return s.charAt(0).toUpperCase() + s.slice(1); return s.charAt(0).toUpperCase() + s.slice(1);
} }
function formatActivityIcon(
activity: { number?: number; unit?: string } | undefined,
): string {
if (!activity) return "";
switch (activity.unit) {
case "free":
return "\u25C7 ";
case "reaction":
return "\u21BA ";
case "action":
return "\u25C6".repeat(activity.number ?? 1) + " ";
default:
return "";
}
}
function stripAngleBrackets(s: string): string {
return s.replaceAll(/<([^>]+)>/g, "$1");
}
function makeCreatureId(source: string, name: string): CreatureId { function makeCreatureId(source: string, name: string): CreatureId {
const slug = name const slug = name
.toLowerCase() .toLowerCase()
@@ -119,13 +141,19 @@ function formatSkills(
} }
function formatSenses( function formatSenses(
senses: readonly { name?: string; type?: string }[] | undefined, senses:
| readonly { name?: string; type?: string; range?: number }[]
| undefined,
): string | undefined { ): string | undefined {
if (!senses || senses.length === 0) return undefined; if (!senses || senses.length === 0) return undefined;
return senses return senses
.map((s) => { .map((s) => {
const label = s.name ?? s.type ?? ""; const label = stripTags(s.name ?? s.type ?? "");
return label ? capitalize(label) : ""; if (!label) return "";
const parts = [capitalize(label)];
if (s.type && s.name) parts.push(`(${s.type})`);
if (s.range != null) parts.push(`${s.range} feet`);
return parts.join(" ");
}) })
.filter(Boolean) .filter(Boolean)
.join(", "); .join(", ");
@@ -234,11 +262,29 @@ function normalizeAbilities(
.filter((a) => a.name) .filter((a) => a.name)
.map((a) => { .map((a) => {
const raw = a as Record<string, unknown>; const raw = a as Record<string, unknown>;
const icon = formatActivityIcon(a.activity);
const traits =
a.traits && a.traits.length > 0
? `(${a.traits.map((t) => capitalize(stripAngleBrackets(stripTags(t)))).join(", ")}) `
: "";
const body = Array.isArray(a.entries)
? segmentizeEntries(a.entries)
: formatAffliction(raw);
const name = icon + stripTags(a.name as string);
if (traits && body.length > 0 && body[0].type === "text") {
return {
name,
segments: [
{ type: "text" as const, value: traits + body[0].value },
...body.slice(1),
],
};
}
return { return {
name: stripTags(a.name as string), name,
segments: Array.isArray(a.entries) segments: traits
? segmentizeEntries(a.entries) ? [{ type: "text" as const, value: traits }, ...body]
: formatAffliction(raw), : body,
}; };
}); });
} }
@@ -253,9 +299,11 @@ function normalizeAttacks(
const attackMod = a.attack == null ? "" : ` +${a.attack}`; const attackMod = a.attack == null ? "" : ` +${a.attack}`;
const traits = const traits =
a.traits && a.traits.length > 0 a.traits && a.traits.length > 0
? ` (${a.traits.map((t) => stripTags(t)).join(", ")})` ? ` (${a.traits.map((t) => stripAngleBrackets(stripTags(t))).join(", ")})`
: ""; : "";
const damage = a.damage ? `, ${stripTags(a.damage)}` : ""; const damage = a.damage
? `, ${stripAngleBrackets(stripTags(a.damage))}`
: "";
return { return {
name: capitalize(stripTags(a.name)), name: capitalize(stripTags(a.name)),
segments: [ segments: [

View File

@@ -77,7 +77,7 @@ export const CONDITION_DEFINITIONS: readonly ConditionDefinition[] = [
description5e: description5e:
"Can't see. Auto-fail sight checks. Attacks have Disadvantage; attacks against have Advantage.", "Can't see. Auto-fail sight checks. Attacks have Disadvantage; attacks against have Advantage.",
descriptionPf2e: descriptionPf2e:
"Can't see. All terrain is difficult terrain. 4 status penalty to Perception checks involving sight. Immune to visual effects. Auto-fail checks requiring sight. Off-guard.", "Can't see. All terrain is difficult terrain. Auto-fail checks requiring sight. Immune to visual effects. Overrides dazzled.",
iconName: "EyeOff", iconName: "EyeOff",
color: "neutral", color: "neutral",
}, },
@@ -98,7 +98,7 @@ export const CONDITION_DEFINITIONS: readonly ConditionDefinition[] = [
description: "Can't hear. Auto-fail hearing checks.", description: "Can't hear. Auto-fail hearing checks.",
description5e: "Can't hear. Auto-fail hearing checks.", description5e: "Can't hear. Auto-fail hearing checks.",
descriptionPf2e: descriptionPf2e:
"Can't hear. 2 status penalty to Perception checks and Initiative. Auto-fail hearing checks. Immune to auditory effects.", "Can't hear. Auto-critically-fail hearing checks. 2 status penalty to Perception. Auditory actions require DC 5 flat check. Immune to auditory effects.",
iconName: "EarOff", iconName: "EarOff",
color: "neutral", color: "neutral",
}, },
@@ -166,7 +166,8 @@ export const CONDITION_DEFINITIONS: readonly ConditionDefinition[] = [
"Incapacitated. Can't move or speak. Auto-fail Str/Dex saves. Attacks against have Advantage. Hits within 5 ft. are critical.", "Incapacitated. Can't move or speak. Auto-fail Str/Dex saves. Attacks against have Advantage. Hits within 5 ft. are critical.",
description5e: description5e:
"Incapacitated. Can't move or speak. Auto-fail Str/Dex saves. Attacks against have Advantage. Hits within 5 ft. are critical.", "Incapacitated. Can't move or speak. Auto-fail Str/Dex saves. Attacks against have Advantage. Hits within 5 ft. are critical.",
descriptionPf2e: "Can't act. Off-guard. 4 status penalty to AC.", descriptionPf2e:
"Can't act. Off-guard. Can only Recall Knowledge or use mental actions.",
iconName: "ZapOff", iconName: "ZapOff",
color: "yellow", color: "yellow",
}, },
@@ -243,7 +244,7 @@ export const CONDITION_DEFINITIONS: readonly ConditionDefinition[] = [
description5e: description5e:
"Incapacitated. Can't move. Can speak only falteringly. Auto-fail Str/Dex saves. Attacks against have Advantage.", "Incapacitated. Can't move. Can speak only falteringly. Auto-fail Str/Dex saves. Attacks against have Advantage.",
descriptionPf2e: descriptionPf2e:
"Can't act. X value to actions per turn while the value counts down.", "Can't act. Lose X total actions across turns, then the condition ends. Overrides slowed.",
iconName: "Sparkles", iconName: "Sparkles",
color: "yellow", color: "yellow",
valued: true, valued: true,
@@ -256,7 +257,7 @@ export const CONDITION_DEFINITIONS: readonly ConditionDefinition[] = [
description5e: description5e:
"Incapacitated. Speed 0. Can't move or speak. Unaware of surroundings. Drops held items, falls Prone. Auto-fail Str/Dex saves. Attacks against have Advantage. Hits within 5 ft. are critical.", "Incapacitated. Speed 0. Can't move or speak. Unaware of surroundings. Drops held items, falls Prone. Auto-fail Str/Dex saves. Attacks against have Advantage. Hits within 5 ft. are critical.",
descriptionPf2e: descriptionPf2e:
"Can't act. Off-guard. 4 status penalty to AC. 3 to Perception. Fall prone, drop items.", "Can't act. Off-guard. Blinded. 4 status penalty to AC, Perception, and Reflex saves. Fall prone, drop items.",
iconName: "Moon", iconName: "Moon",
color: "indigo", color: "indigo",
}, },
@@ -290,7 +291,7 @@ export const CONDITION_DEFINITIONS: readonly ConditionDefinition[] = [
description: "", description: "",
description5e: "", description5e: "",
descriptionPf2e: descriptionPf2e:
"Off-guard. Can't Delay, Ready, or use reactions. GM determines targets randomly. Flat check DC 11 to act normally each turn.", "Off-guard. Can't Delay, Ready, or use reactions. Must Strike or cast offensive cantrips at random targets. DC 11 flat check when damaged to end.",
iconName: "CircleHelp", iconName: "CircleHelp",
color: "pink", color: "pink",
systems: ["pf2e"], systems: ["pf2e"],
@@ -335,7 +336,7 @@ export const CONDITION_DEFINITIONS: readonly ConditionDefinition[] = [
description: "", description: "",
description5e: "", description5e: "",
descriptionPf2e: descriptionPf2e:
"X status penalty to Con-based checks and DCs. Lose X × Hit Die in max HP. Decreases by 1 on full night's rest.", "X status penalty to Con-based checks and DCs. Lose X × level in max HP. Decreases by 1 on full night's rest.",
iconName: "Droplets", iconName: "Droplets",
color: "red", color: "red",
systems: ["pf2e"], systems: ["pf2e"],
@@ -359,7 +360,7 @@ export const CONDITION_DEFINITIONS: readonly ConditionDefinition[] = [
description: "", description: "",
description5e: "", description5e: "",
descriptionPf2e: descriptionPf2e:
"X status penalty to Str-based rolls, including melee attack and damage rolls.", "X status penalty to Str-based rolls and DCs, including melee attack and damage rolls and Athletics checks.",
iconName: "TrendingDown", iconName: "TrendingDown",
color: "amber", color: "amber",
systems: ["pf2e"], systems: ["pf2e"],
@@ -371,7 +372,7 @@ export const CONDITION_DEFINITIONS: readonly ConditionDefinition[] = [
description: "", description: "",
description5e: "", description5e: "",
descriptionPf2e: descriptionPf2e:
"2 status penalty to all checks. Can't use hostile actions. Ends if hostile action is used against you.", "2 status penalty to Perception and skill checks. Can't use concentrate actions unless related to the fascination. Ends if hostile action is used against you or allies.",
iconName: "Eye", iconName: "Eye",
color: "violet", color: "violet",
systems: ["pf2e"], systems: ["pf2e"],
@@ -404,7 +405,7 @@ export const CONDITION_DEFINITIONS: readonly ConditionDefinition[] = [
description: "", description: "",
description5e: "", description5e: "",
descriptionPf2e: descriptionPf2e:
"Immobilized. Off-guard. Can't use actions with the move trait unless to Break Grapple.", "Off-guard. Immobilized. Manipulate actions require DC 5 flat check or are wasted.",
iconName: "Hand", iconName: "Hand",
color: "neutral", color: "neutral",
systems: ["pf2e"], systems: ["pf2e"],
@@ -415,7 +416,7 @@ export const CONDITION_DEFINITIONS: readonly ConditionDefinition[] = [
description: "", description: "",
description5e: "", description5e: "",
descriptionPf2e: descriptionPf2e:
"Known location but can't be seen. DC 11 flat check to target. Can use Seek to find.", "Known location but can't be seen. Off-guard to that creature. DC 11 flat check to target or miss.",
iconName: "EyeOff", iconName: "EyeOff",
color: "slate", color: "slate",
systems: ["pf2e"], systems: ["pf2e"],
@@ -521,5 +522,7 @@ export function getConditionsForEdition(
): readonly ConditionDefinition[] { ): readonly ConditionDefinition[] {
return CONDITION_DEFINITIONS.filter( return CONDITION_DEFINITIONS.filter(
(d) => d.systems === undefined || d.systems.includes(edition), (d) => d.systems === undefined || d.systems.includes(edition),
); )
.slice()
.sort((a, b) => a.label.localeCompare(b.label));
} }