Add Sapped and Slowed conditions for 5.5e weapon mastery
All checks were successful
CI / check (push) Successful in 1m8s
CI / build-image (push) Successful in 15s

These D&D 2024 weapon mastery conditions are edition-gated: they only
appear in the condition picker when 5.5e rules are selected. Applied
conditions still render correctly regardless of edition setting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lukas
2026-03-25 00:31:41 +01:00
parent 27ff8ba1ad
commit 228a2603e8
5 changed files with 86 additions and 4 deletions

View File

@@ -1,7 +1,7 @@
import { import {
CONDITION_DEFINITIONS,
type ConditionId, type ConditionId,
getConditionDescription, getConditionDescription,
getConditionsForEdition,
} from "@initiative/domain"; } from "@initiative/domain";
import type { LucideIcon } from "lucide-react"; import type { LucideIcon } from "lucide-react";
import { import {
@@ -17,7 +17,9 @@ import {
Heart, Heart,
Link, Link,
Moon, Moon,
ShieldMinus,
Siren, Siren,
Snail,
Sparkles, Sparkles,
ZapOff, ZapOff,
} from "lucide-react"; } from "lucide-react";
@@ -41,6 +43,8 @@ const ICON_MAP: Record<string, LucideIcon> = {
Droplet, Droplet,
ArrowDown, ArrowDown,
Link, Link,
ShieldMinus,
Snail,
Sparkles, Sparkles,
Moon, Moon,
}; };
@@ -56,6 +60,7 @@ const COLOR_CLASSES: Record<string, string> = {
slate: "text-slate-400", slate: "text-slate-400",
green: "text-green-400", green: "text-green-400",
indigo: "text-indigo-400", indigo: "text-indigo-400",
sky: "text-sky-400",
}; };
interface ConditionPickerProps { interface ConditionPickerProps {
@@ -110,6 +115,7 @@ export function ConditionPicker({
}, [onClose]); }, [onClose]);
const { edition } = useRulesEditionContext(); const { edition } = useRulesEditionContext();
const conditions = getConditionsForEdition(edition);
const active = new Set(activeConditions ?? []); const active = new Set(activeConditions ?? []);
return createPortal( return createPortal(
@@ -122,7 +128,7 @@ export function ConditionPicker({
: { visibility: "hidden" as const } : { visibility: "hidden" as const }
} }
> >
{CONDITION_DEFINITIONS.map((def) => { {conditions.map((def) => {
const Icon = ICON_MAP[def.iconName]; const Icon = ICON_MAP[def.iconName];
if (!Icon) return null; if (!Icon) return null;
const isActive = active.has(def.id); const isActive = active.has(def.id);

View File

@@ -18,7 +18,9 @@ import {
Link, Link,
Moon, Moon,
Plus, Plus,
ShieldMinus,
Siren, Siren,
Snail,
Sparkles, Sparkles,
ZapOff, ZapOff,
} from "lucide-react"; } from "lucide-react";
@@ -40,6 +42,8 @@ const ICON_MAP: Record<string, LucideIcon> = {
Droplet, Droplet,
ArrowDown, ArrowDown,
Link, Link,
ShieldMinus,
Snail,
Sparkles, Sparkles,
Moon, Moon,
}; };
@@ -55,6 +59,7 @@ const COLOR_CLASSES: Record<string, string> = {
slate: "text-slate-400", slate: "text-slate-400",
green: "text-green-400", green: "text-green-400",
indigo: "text-indigo-400", indigo: "text-indigo-400",
sky: "text-sky-400",
}; };
interface ConditionTagsProps { interface ConditionTagsProps {

View File

@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import { import {
CONDITION_DEFINITIONS, CONDITION_DEFINITIONS,
getConditionDescription, getConditionDescription,
getConditionsForEdition,
} from "../conditions.js"; } from "../conditions.js";
function findCondition(id: string) { function findCondition(id: string) {
@@ -25,13 +26,27 @@ describe("getConditionDescription", () => {
); );
}); });
it("every condition has both descriptions", () => { it("universal conditions have both descriptions", () => {
for (const def of CONDITION_DEFINITIONS) { const universal = CONDITION_DEFINITIONS.filter(
(d) => d.edition === undefined,
);
expect(universal.length).toBeGreaterThan(0);
for (const def of universal) {
expect(def.description).toBeTruthy(); expect(def.description).toBeTruthy();
expect(def.description5e).toBeTruthy(); expect(def.description5e).toBeTruthy();
} }
}); });
it("edition-specific conditions have their edition description", () => {
const sapped = findCondition("sapped");
expect(sapped.description).toBeTruthy();
expect(sapped.edition).toBe("5.5e");
const slowed = findCondition("slowed");
expect(slowed.description).toBeTruthy();
expect(slowed.edition).toBe("5.5e");
});
it("conditions with identical rules share the same text", () => { it("conditions with identical rules share the same text", () => {
const blinded = findCondition("blinded"); const blinded = findCondition("blinded");
expect(blinded.description).toBe(blinded.description5e); expect(blinded.description).toBe(blinded.description5e);
@@ -42,3 +57,26 @@ describe("getConditionDescription", () => {
expect(exhaustion.description).not.toBe(exhaustion.description5e); expect(exhaustion.description).not.toBe(exhaustion.description5e);
}); });
}); });
describe("getConditionsForEdition", () => {
it("includes sapped and slowed for 5.5e", () => {
const conditions = getConditionsForEdition("5.5e");
const ids = conditions.map((d) => d.id);
expect(ids).toContain("sapped");
expect(ids).toContain("slowed");
});
it("excludes sapped and slowed for 5e", () => {
const conditions = getConditionsForEdition("5e");
const ids = conditions.map((d) => d.id);
expect(ids).not.toContain("sapped");
expect(ids).not.toContain("slowed");
});
it("includes universal conditions for both editions", () => {
const ids5e = getConditionsForEdition("5e").map((d) => d.id);
const ids55e = getConditionsForEdition("5.5e").map((d) => d.id);
expect(ids5e).toContain("blinded");
expect(ids55e).toContain("blinded");
});
});

View File

@@ -12,6 +12,8 @@ export type ConditionId =
| "poisoned" | "poisoned"
| "prone" | "prone"
| "restrained" | "restrained"
| "sapped"
| "slowed"
| "stunned" | "stunned"
| "unconscious"; | "unconscious";
@@ -24,6 +26,8 @@ export interface ConditionDefinition {
readonly description5e: string; readonly description5e: string;
readonly iconName: string; readonly iconName: string;
readonly color: string; readonly color: string;
/** When set, the condition only appears in this edition's picker. */
readonly edition?: RulesEdition;
} }
export function getConditionDescription( export function getConditionDescription(
@@ -159,6 +163,26 @@ export const CONDITION_DEFINITIONS: readonly ConditionDefinition[] = [
iconName: "Link", iconName: "Link",
color: "neutral", color: "neutral",
}, },
{
id: "sapped",
label: "Sapped",
description:
"Disadvantage on next attack roll before the start of your next turn. (Weapon Mastery: Sap)",
description5e: "",
iconName: "ShieldMinus",
color: "amber",
edition: "5.5e",
},
{
id: "slowed",
label: "Slowed",
description:
"Speed reduced by 10 ft. until the start of your next turn. (Weapon Mastery: Slow)",
description5e: "",
iconName: "Snail",
color: "sky",
edition: "5.5e",
},
{ {
id: "stunned", id: "stunned",
label: "Stunned", label: "Stunned",
@@ -184,3 +208,11 @@ export const CONDITION_DEFINITIONS: readonly ConditionDefinition[] = [
export const VALID_CONDITION_IDS: ReadonlySet<string> = new Set( export const VALID_CONDITION_IDS: ReadonlySet<string> = new Set(
CONDITION_DEFINITIONS.map((d) => d.id), CONDITION_DEFINITIONS.map((d) => d.id),
); );
export function getConditionsForEdition(
edition: RulesEdition,
): readonly ConditionDefinition[] {
return CONDITION_DEFINITIONS.filter(
(d) => d.edition === undefined || d.edition === edition,
);
}

View File

@@ -11,6 +11,7 @@ export {
type ConditionDefinition, type ConditionDefinition,
type ConditionId, type ConditionId,
getConditionDescription, getConditionDescription,
getConditionsForEdition,
type RulesEdition, type RulesEdition,
VALID_CONDITION_IDS, VALID_CONDITION_IDS,
} from "./conditions.js"; } from "./conditions.js";