From 228a2603e8315f37f777128d1c1b36307db77bf8 Mon Sep 17 00:00:00 2001 From: Lukas Date: Wed, 25 Mar 2026 00:31:41 +0100 Subject: [PATCH] Add Sapped and Slowed conditions for 5.5e weapon mastery 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) --- apps/web/src/components/condition-picker.tsx | 10 ++++- apps/web/src/components/condition-tags.tsx | 5 +++ .../domain/src/__tests__/conditions.test.ts | 42 ++++++++++++++++++- packages/domain/src/conditions.ts | 32 ++++++++++++++ packages/domain/src/index.ts | 1 + 5 files changed, 86 insertions(+), 4 deletions(-) diff --git a/apps/web/src/components/condition-picker.tsx b/apps/web/src/components/condition-picker.tsx index b85e427..513cb6b 100644 --- a/apps/web/src/components/condition-picker.tsx +++ b/apps/web/src/components/condition-picker.tsx @@ -1,7 +1,7 @@ import { - CONDITION_DEFINITIONS, type ConditionId, getConditionDescription, + getConditionsForEdition, } from "@initiative/domain"; import type { LucideIcon } from "lucide-react"; import { @@ -17,7 +17,9 @@ import { Heart, Link, Moon, + ShieldMinus, Siren, + Snail, Sparkles, ZapOff, } from "lucide-react"; @@ -41,6 +43,8 @@ const ICON_MAP: Record = { Droplet, ArrowDown, Link, + ShieldMinus, + Snail, Sparkles, Moon, }; @@ -56,6 +60,7 @@ const COLOR_CLASSES: Record = { slate: "text-slate-400", green: "text-green-400", indigo: "text-indigo-400", + sky: "text-sky-400", }; interface ConditionPickerProps { @@ -110,6 +115,7 @@ export function ConditionPicker({ }, [onClose]); const { edition } = useRulesEditionContext(); + const conditions = getConditionsForEdition(edition); const active = new Set(activeConditions ?? []); return createPortal( @@ -122,7 +128,7 @@ export function ConditionPicker({ : { visibility: "hidden" as const } } > - {CONDITION_DEFINITIONS.map((def) => { + {conditions.map((def) => { const Icon = ICON_MAP[def.iconName]; if (!Icon) return null; const isActive = active.has(def.id); diff --git a/apps/web/src/components/condition-tags.tsx b/apps/web/src/components/condition-tags.tsx index e691c80..dbb6050 100644 --- a/apps/web/src/components/condition-tags.tsx +++ b/apps/web/src/components/condition-tags.tsx @@ -18,7 +18,9 @@ import { Link, Moon, Plus, + ShieldMinus, Siren, + Snail, Sparkles, ZapOff, } from "lucide-react"; @@ -40,6 +42,8 @@ const ICON_MAP: Record = { Droplet, ArrowDown, Link, + ShieldMinus, + Snail, Sparkles, Moon, }; @@ -55,6 +59,7 @@ const COLOR_CLASSES: Record = { slate: "text-slate-400", green: "text-green-400", indigo: "text-indigo-400", + sky: "text-sky-400", }; interface ConditionTagsProps { diff --git a/packages/domain/src/__tests__/conditions.test.ts b/packages/domain/src/__tests__/conditions.test.ts index 50ddb60..11c4891 100644 --- a/packages/domain/src/__tests__/conditions.test.ts +++ b/packages/domain/src/__tests__/conditions.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import { CONDITION_DEFINITIONS, getConditionDescription, + getConditionsForEdition, } from "../conditions.js"; function findCondition(id: string) { @@ -25,13 +26,27 @@ describe("getConditionDescription", () => { ); }); - it("every condition has both descriptions", () => { - for (const def of CONDITION_DEFINITIONS) { + it("universal conditions have both descriptions", () => { + 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.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", () => { const blinded = findCondition("blinded"); expect(blinded.description).toBe(blinded.description5e); @@ -42,3 +57,26 @@ describe("getConditionDescription", () => { 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"); + }); +}); diff --git a/packages/domain/src/conditions.ts b/packages/domain/src/conditions.ts index e8c6050..8c6bdd0 100644 --- a/packages/domain/src/conditions.ts +++ b/packages/domain/src/conditions.ts @@ -12,6 +12,8 @@ export type ConditionId = | "poisoned" | "prone" | "restrained" + | "sapped" + | "slowed" | "stunned" | "unconscious"; @@ -24,6 +26,8 @@ export interface ConditionDefinition { readonly description5e: string; readonly iconName: string; readonly color: string; + /** When set, the condition only appears in this edition's picker. */ + readonly edition?: RulesEdition; } export function getConditionDescription( @@ -159,6 +163,26 @@ export const CONDITION_DEFINITIONS: readonly ConditionDefinition[] = [ iconName: "Link", 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", label: "Stunned", @@ -184,3 +208,11 @@ export const CONDITION_DEFINITIONS: readonly ConditionDefinition[] = [ export const VALID_CONDITION_IDS: ReadonlySet = new Set( CONDITION_DEFINITIONS.map((d) => d.id), ); + +export function getConditionsForEdition( + edition: RulesEdition, +): readonly ConditionDefinition[] { + return CONDITION_DEFINITIONS.filter( + (d) => d.edition === undefined || d.edition === edition, + ); +} diff --git a/packages/domain/src/index.ts b/packages/domain/src/index.ts index 4419192..1c9496e 100644 --- a/packages/domain/src/index.ts +++ b/packages/domain/src/index.ts @@ -11,6 +11,7 @@ export { type ConditionDefinition, type ConditionId, getConditionDescription, + getConditionsForEdition, type RulesEdition, VALID_CONDITION_IDS, } from "./conditions.js";