Enforce maximum values for PF2e numbered conditions

Cap dying (4), doomed (3), wounded (3), and slowed (3) at their
rule-defined maximums. The domain clamps values in setConditionValue
and the condition picker disables the [+] button at the cap.

Closes #31

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lukas
2026-04-09 00:04:47 +02:00
parent 1c107a500b
commit 553e09f280
5 changed files with 120 additions and 20 deletions

View File

@@ -169,6 +169,60 @@ describe("setConditionValue", () => {
);
expectDomainError(result, "unknown-condition");
});
it("clamps value to maxValue for capped conditions", () => {
const e = enc([makeCombatant("A")]);
const result = setConditionValue(e, combatantId("A"), "dying", 6);
if (isDomainError(result)) throw new Error(result.message);
expect(result.encounter.combatants[0].conditions).toEqual([
{ id: "dying", value: 4 },
]);
expect(result.events[0]).toMatchObject({
type: "ConditionAdded",
value: 4,
});
});
it("allows value at exactly the max", () => {
const e = enc([makeCombatant("A")]);
const result = setConditionValue(e, combatantId("A"), "doomed", 3);
if (isDomainError(result)) throw new Error(result.message);
expect(result.encounter.combatants[0].conditions).toEqual([
{ id: "doomed", value: 3 },
]);
});
it("allows value below the max", () => {
const e = enc([makeCombatant("A")]);
const result = setConditionValue(e, combatantId("A"), "wounded", 2);
if (isDomainError(result)) throw new Error(result.message);
expect(result.encounter.combatants[0].conditions).toEqual([
{ id: "wounded", value: 2 },
]);
});
it("does not cap conditions without a maxValue", () => {
const e = enc([makeCombatant("A")]);
const result = setConditionValue(e, combatantId("A"), "frightened", 10);
if (isDomainError(result)) throw new Error(result.message);
expect(result.encounter.combatants[0].conditions).toEqual([
{ id: "frightened", value: 10 },
]);
});
it("clamps when updating an existing capped condition", () => {
const e = enc([makeCombatant("A", [{ id: "slowed-pf2e", value: 2 }])]);
const result = setConditionValue(e, combatantId("A"), "slowed-pf2e", 5);
if (isDomainError(result)) throw new Error(result.message);
expect(result.encounter.combatants[0].conditions).toEqual([
{ id: "slowed-pf2e", value: 3 },
]);
});
});
describe("decrementCondition", () => {

View File

@@ -57,6 +57,8 @@ export interface ConditionDefinition {
/** When set, the condition only appears in these systems' pickers. */
readonly systems?: readonly RulesEdition[];
readonly valued?: boolean;
/** Rule-defined maximum value for PF2e valued conditions. */
readonly maxValue?: number;
}
export function getConditionDescription(
@@ -329,6 +331,7 @@ export const CONDITION_DEFINITIONS: readonly ConditionDefinition[] = [
color: "red",
systems: ["pf2e"],
valued: true,
maxValue: 3,
},
{
id: "drained",
@@ -353,6 +356,7 @@ export const CONDITION_DEFINITIONS: readonly ConditionDefinition[] = [
color: "red",
systems: ["pf2e"],
valued: true,
maxValue: 4,
},
{
id: "enfeebled",
@@ -475,6 +479,7 @@ export const CONDITION_DEFINITIONS: readonly ConditionDefinition[] = [
color: "sky",
systems: ["pf2e"],
valued: true,
maxValue: 3,
},
{
id: "stupefied",
@@ -510,6 +515,7 @@ export const CONDITION_DEFINITIONS: readonly ConditionDefinition[] = [
color: "red",
systems: ["pf2e"],
valued: true,
maxValue: 3,
},
] as const;

View File

@@ -92,7 +92,11 @@ export function setConditionValue(
const { combatant: target } = found;
const current = target.conditions ?? [];
if (value <= 0) {
const def = CONDITION_DEFINITIONS.find((d) => d.id === conditionId);
const clampedValue =
def?.maxValue === undefined ? value : Math.min(value, def.maxValue);
if (clampedValue <= 0) {
const filtered = current.filter((c) => c.id !== conditionId);
const newConditions = filtered.length > 0 ? filtered : undefined;
return {
@@ -106,7 +110,7 @@ export function setConditionValue(
const existing = current.find((c) => c.id === conditionId);
if (existing) {
const updated = current.map((c) =>
c.id === conditionId ? { ...c, value } : c,
c.id === conditionId ? { ...c, value: clampedValue } : c,
);
return {
encounter: applyConditions(encounter, combatantId, updated),
@@ -115,17 +119,25 @@ export function setConditionValue(
type: "ConditionAdded",
combatantId,
condition: conditionId,
value,
value: clampedValue,
},
],
};
}
const added = sortByDefinitionOrder([...current, { id: conditionId, value }]);
const added = sortByDefinitionOrder([
...current,
{ id: conditionId, value: clampedValue },
]);
return {
encounter: applyConditions(encounter, combatantId, added),
events: [
{ type: "ConditionAdded", combatantId, condition: conditionId, value },
{
type: "ConditionAdded",
combatantId,
condition: conditionId,
value: clampedValue,
},
],
};
}