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:
@@ -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", () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user