12 Commits

Author SHA1 Message Date
Lukas
228a2603e8 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>
2026-03-25 00:31:41 +01:00
Lukas
27ff8ba1ad Collapse hover-only buttons to zero width when hidden
All checks were successful
CI / check (push) Successful in 1m8s
CI / build-image (push) Successful in 16s
Edit and add-condition buttons now take no space when not hovered,
eliminating the gap between name and condition icons. They slide in
smoothly on hover with a 150ms transition.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 00:19:47 +01:00
Lukas
4cfcefe6c3 Hide custom stat fields on mobile, fix action bar gap consistency
All checks were successful
CI / check (push) Successful in 1m7s
CI / build-image (push) Successful in 16s
Init/AC/MaxHP inputs are hidden on phones — users set these values
directly in the combatant row after adding. Fixes uneven spacing
between action bar elements by using consistent gap-3.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 23:41:18 +01:00
Lukas
8baccf3cd3 Merge 006-mobile-touch-targets: mobile foundation and bug fixes
All checks were successful
CI / check (push) Successful in 1m8s
CI / build-image (push) Successful in 16s
- iOS zoom fix (16px input font)
- Safe area insets for notched phones
- viewport-fit=cover
- Action bar flex-wrap for narrow screens
- Slightly increased row padding on mobile
- Fix stat block panel showing wrong creature on first open
- Skip auto-opening stat block when adding on mobile
2026-03-24 23:26:33 +01:00
Lukas
a9ca31e9bc Skip auto-opening stat block panel when adding creatures on mobile
On desktop the panel has room alongside the combatant list, but on
mobile it covers the screen and disrupts the add-combatant flow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 23:22:54 +01:00
Lukas
64a1f0b8db Add mobile foundation: iOS zoom fix, safe area insets, row padding
- Input base font 16px on mobile to prevent iOS Safari auto-zoom
- Safe area insets for notched phones (top/bottom bars)
- viewport-fit=cover to enable safe area env() values
- Action bar flex-wrap for custom stat field overflow
- Slightly increased row padding on mobile (py-3 sm:py-2)
- Removed redundant font-size classes from Input usages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 23:19:10 +01:00
Lukas
5e5812bcaa Fix stat block panel showing wrong creature on first open
useAutoStatBlock was overriding the user's creature selection when
the panel transitioned from closed to open. Now only auto-updates
when the active turn index changes (advance/retreat), not when the
panel mode changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 23:18:49 +01:00
Lukas
9e09c8ae2a Sync theme-color meta tag with active light/dark theme
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 17:59:13 +01:00
Lukas
4d0ec0c7b2 Add Open Graph meta tags for link previews in messengers
All checks were successful
CI / check (push) Successful in 1m8s
CI / build-image (push) Successful in 15s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 17:54:05 +01:00
Lukas
fe62f2eb2f Add PWA manifest, app icons, and favicon
D20-themed icon in app color scheme for home screen installation,
favicon, and apple-touch-icon. Standalone display mode makes the
app feel native when launched from home screen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 17:51:28 +01:00
Lukas
7092677273 Make layout full-width on mobile with docked top/bottom bars
Remove max-width constraint and horizontal padding on small screens
so content goes edge-to-edge. Turn navigation and action bar lose
rounded corners on mobile and dock flush to top/bottom edges.
Desktop layout (sm: and up) is unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 17:36:01 +01:00
Lukas
e1a06c9d59 Tighten combatant row grid on mobile for better name visibility
Reduce grid gap and initiative column width on small screens,
restoring full layout at the sm breakpoint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 17:35:42 +01:00
20 changed files with 208 additions and 24 deletions

View File

@@ -2,7 +2,16 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="theme-color" content="#0e1a2e" />
<link rel="manifest" href="/manifest.json" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<meta property="og:title" content="Initiative Tracker" />
<meta property="og:description" content="D&D combat initiative tracker" />
<meta property="og:image" content="https://initiative.dostulata.rocks/icon-512.png" />
<meta property="og:url" content="https://initiative.dostulata.rocks/" />
<meta property="og:type" content="website" />
<title>Initiative Tracker</title>
</head>
<body>

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,21 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<defs>
<linearGradient id="f" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#60a5fa"/>
<stop offset="100%" stop-color="#2563eb"/>
</linearGradient>
</defs>
<g transform="translate(16 15) scale(1.55)" fill="none" stroke="url(#f)" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
<polygon points="8.18 4.76 8.18 -4.74 -0.01 -9.49 -8.19 -4.74 -8.19 4.76 -0.01 9.51 8.18 4.76" fill="url(#f)" fill-opacity="0.15"/>
<polygon points="-0.01 -9.49 -4.75 -2.74 4.75 -2.74 -0.01 -9.49" fill="url(#f)" fill-opacity="0.25"/>
<polygon points="-0.01 5.44 8.18 4.76 4.75 -2.74 -0.01 5.44" fill="url(#f)" fill-opacity="0.2"/>
<polygon points="-0.01 5.44 -4.75 -2.74 -8.19 4.76 -0.01 5.44" fill="url(#f)" fill-opacity="0.1"/>
<line x1="-4.75" y1="-2.74" x2="-8.19" y2="-4.74"/>
<line x1="8.18" y1="-4.74" x2="4.75" y2="-2.74"/>
<line x1="-0.01" y1="5.44" x2="-0.01" y2="9.51"/>
<polygon points="8.18 4.76 8.18 -4.74 -0.01 -9.49 -8.19 -4.74 -8.19 4.76 -0.01 9.51 8.18 4.76"/>
<polygon points="-0.01 -9.49 -4.75 -2.74 4.75 -2.74 -0.01 -9.49"/>
<polygon points="-0.01 5.44 8.18 4.76 4.75 -2.74 -0.01 5.44"/>
<polygon points="-0.01 5.44 -4.75 -2.74 -8.19 4.76 -0.01 5.44"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

31
apps/web/public/icon.svg Normal file
View File

@@ -0,0 +1,31 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
<defs>
<radialGradient id="bg" cx="50%" cy="40%" r="70%">
<stop offset="0%" stop-color="#1a2e4a"/>
<stop offset="100%" stop-color="#0e1a2e"/>
</radialGradient>
<linearGradient id="d20fill" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#60a5fa"/>
<stop offset="100%" stop-color="#2563eb"/>
</linearGradient>
<linearGradient id="d20stroke" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#93c5fd"/>
<stop offset="100%" stop-color="#3b82f6"/>
</linearGradient>
</defs>
<rect width="512" height="512" rx="96" fill="url(#bg)"/>
<g transform="translate(256 256) scale(8.5)" fill="none" stroke="url(#d20stroke)" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="8.18 4.76 8.18 -4.74 -0.01 -9.49 -8.19 -4.74 -8.19 4.76 -0.01 9.51 8.18 4.76" fill="url(#d20fill)" fill-opacity="0.15"/>
<polygon points="-0.01 -9.49 -4.75 -2.74 4.75 -2.74 -0.01 -9.49" fill="url(#d20fill)" fill-opacity="0.25"/>
<polygon points="-0.01 5.44 8.18 4.76 4.75 -2.74 -0.01 5.44" fill="url(#d20fill)" fill-opacity="0.2"/>
<polygon points="-0.01 5.44 -4.75 -2.74 -8.19 4.76 -0.01 5.44" fill="url(#d20fill)" fill-opacity="0.1"/>
<line x1="-4.75" y1="-2.74" x2="-8.19" y2="-4.74"/>
<line x1="8.18" y1="-4.74" x2="4.75" y2="-2.74"/>
<line x1="-0.01" y1="5.44" x2="-0.01" y2="9.51"/>
<polygon points="8.18 4.76 8.18 -4.74 -0.01 -9.49 -8.19 -4.74 -8.19 4.76 -0.01 9.51 8.18 4.76"/>
<polygon points="-0.01 -9.49 -4.75 -2.74 4.75 -2.74 -0.01 -9.49"/>
<polygon points="-0.01 5.44 8.18 4.76 4.75 -2.74 -0.01 5.44"/>
<polygon points="-0.01 5.44 -4.75 -2.74 -8.19 4.76 -0.01 5.44"/>
</g>
<text x="256" y="278" text-anchor="middle" dominant-baseline="central" font-family="Inter, system-ui, sans-serif" font-weight="700" font-size="52" fill="#93c5fd" letter-spacing="1">20</text>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,21 @@
{
"name": "Initiative Tracker",
"short_name": "Initiative",
"description": "D&D combat initiative tracker",
"start_url": "/",
"display": "standalone",
"background_color": "#0e1a2e",
"theme_color": "#0e1a2e",
"icons": [
{
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

View File

@@ -43,10 +43,13 @@ export function App() {
return (
<div className="flex h-dvh flex-col">
<div className="relative mx-auto flex min-h-0 w-full max-w-2xl flex-1 flex-col gap-3 px-4">
<div className="relative mx-auto flex min-h-0 w-full flex-1 flex-col gap-3 sm:max-w-2xl sm:px-4">
{!!actionBarAnim.showTopBar && (
<div
className={cn("shrink-0 pt-8", actionBarAnim.topBarClass)}
className={cn(
"shrink-0 pt-[env(safe-area-inset-top)] sm:pt-[max(env(safe-area-inset-top),2rem)]",
actionBarAnim.topBarClass,
)}
onAnimationEnd={actionBarAnim.onTopBarExitEnd}
>
<TurnNavigation />
@@ -85,7 +88,10 @@ export function App() {
</div>
<div
className={cn("shrink-0 pb-8", actionBarAnim.settlingClass)}
className={cn(
"shrink-0 pb-[env(safe-area-inset-bottom)] sm:pb-[max(env(safe-area-inset-bottom),2rem)]",
actionBarAnim.settlingClass,
)}
onAnimationEnd={actionBarAnim.onSettleEnd}
>
<ActionBar

View File

@@ -279,7 +279,8 @@ export function ActionBar({
const handleAddFromBestiary = useCallback(
(result: SearchResult) => {
const creatureId = addFromBestiary(result);
if (creatureId && panelView.mode === "closed") {
const isDesktop = globalThis.matchMedia("(min-width: 1024px)").matches;
if (creatureId && panelView.mode === "closed" && isDesktop) {
showCreature(creatureId);
}
},
@@ -518,10 +519,10 @@ export function ActionBar({
});
return (
<div className="card-glow flex items-center gap-3 rounded-lg border border-border bg-card px-4 py-3">
<div className="card-glow flex items-center gap-3 border-border border-t bg-card px-4 py-3 sm:rounded-lg sm:border">
<form
onSubmit={handleAdd}
className="relative flex flex-1 items-center gap-2"
className="relative flex flex-1 flex-wrap items-center gap-3 sm:flex-nowrap"
>
<div className="flex-1">
<div className="relative max-w-xs">
@@ -605,7 +606,7 @@ export function ActionBar({
</div>
</div>
{!browseMode && nameInput.length >= 2 && !hasSuggestions && (
<div className="flex items-center gap-2">
<div className="hidden items-center gap-2 sm:flex">
<Input
type="text"
inputMode="numeric"

View File

@@ -112,7 +112,7 @@ function EditableName({
onClick={startEditing}
title="Rename"
aria-label="Rename"
className="inline-flex shrink-0 items-center rounded p-0.5 text-muted-foreground opacity-0 pointer-coarse:opacity-100 transition-colors transition-opacity hover:bg-hover-neutral-bg hover:text-hover-neutral focus:opacity-100 group-hover:opacity-100"
className="inline-flex pointer-coarse:w-auto w-0 shrink-0 items-center overflow-hidden pointer-coarse:overflow-visible rounded p-0.5 text-muted-foreground opacity-0 pointer-coarse:opacity-100 transition-all duration-150 hover:bg-hover-neutral-bg hover:text-hover-neutral focus:w-auto focus:overflow-visible focus:opacity-100 group-hover:w-auto group-hover:overflow-visible group-hover:opacity-100"
>
<Pencil size={14} />
</button>
@@ -157,7 +157,7 @@ function MaxHpDisplay({
inputMode="numeric"
value={draft}
placeholder="Max"
className="h-7 w-[7ch] text-center text-sm tabular-nums"
className="h-7 w-[7ch] text-center tabular-nums"
onChange={(e) => setDraft(e.target.value)}
onBlur={commit}
onKeyDown={(e) => {
@@ -272,7 +272,7 @@ function AcDisplay({
inputMode="numeric"
value={draft}
placeholder="AC"
className="h-7 w-[6ch] text-center text-sm tabular-nums"
className="h-7 w-[6ch] text-center tabular-nums"
onChange={(e) => setDraft(e.target.value)}
onBlur={commit}
onKeyDown={(e) => {
@@ -348,7 +348,7 @@ function InitiativeDisplay({
value={draft}
placeholder="--"
className={cn(
"h-7 w-full text-center text-sm tabular-nums",
"h-7 w-full text-center tabular-nums",
dimmed && "opacity-50",
)}
onChange={(e) => setDraft(e.target.value)}
@@ -520,7 +520,7 @@ export function CombatantRow({
isPulsing && "animate-concentration-pulse",
)}
>
<div className="grid grid-cols-[2rem_3.5rem_auto_1fr_auto_2rem] items-center gap-3 py-2">
<div className="grid grid-cols-[2rem_3rem_auto_1fr_auto_2rem] items-center gap-1.5 py-3 sm:grid-cols-[2rem_3.5rem_auto_1fr_auto_2rem] sm:gap-3 sm:py-2">
{/* Concentration */}
<button
type="button"

View File

@@ -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<string, LucideIcon> = {
Droplet,
ArrowDown,
Link,
ShieldMinus,
Snail,
Sparkles,
Moon,
};
@@ -56,6 +60,7 @@ const COLOR_CLASSES: Record<string, string> = {
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);

View File

@@ -18,7 +18,9 @@ import {
Link,
Moon,
Plus,
ShieldMinus,
Siren,
Snail,
Sparkles,
ZapOff,
} from "lucide-react";
@@ -40,6 +42,8 @@ const ICON_MAP: Record<string, LucideIcon> = {
Droplet,
ArrowDown,
Link,
ShieldMinus,
Snail,
Sparkles,
Moon,
};
@@ -55,6 +59,7 @@ const COLOR_CLASSES: Record<string, string> = {
slate: "text-slate-400",
green: "text-green-400",
indigo: "text-indigo-400",
sky: "text-sky-400",
};
interface ConditionTagsProps {
@@ -103,7 +108,7 @@ export function ConditionTags({
type="button"
title="Add condition"
aria-label="Add condition"
className="inline-flex items-center rounded p-0.5 text-muted-foreground opacity-0 pointer-coarse:opacity-100 transition-colors transition-opacity hover:bg-hover-neutral-bg hover:text-hover-neutral focus:opacity-100 group-hover:opacity-100"
className="inline-flex pointer-coarse:w-auto w-0 items-center overflow-hidden pointer-coarse:overflow-visible rounded p-0.5 text-muted-foreground opacity-0 pointer-coarse:opacity-100 transition-all duration-150 hover:bg-hover-neutral-bg hover:text-hover-neutral focus:w-auto focus:overflow-visible focus:opacity-100 group-hover:w-auto group-hover:overflow-visible group-hover:opacity-100"
onClick={(e) => {
e.stopPropagation();
onOpenPicker();

View File

@@ -106,7 +106,7 @@ export function HpAdjustPopover({
inputMode="numeric"
value={inputValue}
placeholder="HP"
className="h-7 w-[7ch] text-center text-sm tabular-nums"
className="h-7 w-[7ch] text-center tabular-nums"
onChange={(e) => {
const v = e.target.value;
if (v === "" || DIGITS_ONLY_REGEX.test(v)) {

View File

@@ -12,7 +12,7 @@ export function TurnNavigation() {
const activeCombatant = encounter.combatants[encounter.activeIndex];
return (
<div className="card-glow flex items-center gap-3 rounded-lg border border-border bg-card px-4 py-3">
<div className="card-glow flex items-center gap-3 border-border border-b bg-card px-4 py-3 sm:rounded-lg sm:border">
<Button
variant="ghost"
size="icon"

View File

@@ -12,7 +12,7 @@ export const Input = ({
<input
ref={ref}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-foreground text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary disabled:cursor-not-allowed disabled:opacity-50",
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base text-foreground shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary disabled:cursor-not-allowed disabled:opacity-50 sm:text-sm",
className,
)}
{...props}

View File

@@ -1,4 +1,4 @@
import { useEffect } from "react";
import { useEffect, useRef } from "react";
import { useEncounterContext } from "../contexts/encounter-context.js";
import { useSidePanelContext } from "../contexts/side-panel-context.js";
@@ -8,10 +8,20 @@ export function useAutoStatBlock(): void {
const activeCreatureId =
encounter.combatants[encounter.activeIndex]?.creatureId;
const prevActiveIndexRef = useRef(encounter.activeIndex);
useEffect(() => {
if (activeCreatureId && panelView.mode === "creature") {
const prevIndex = prevActiveIndexRef.current;
prevActiveIndexRef.current = encounter.activeIndex;
// Only auto-update when the active turn changes (advance/retreat),
// not when the panel mode changes (user clicking a different creature).
if (
encounter.activeIndex !== prevIndex &&
activeCreatureId &&
panelView.mode === "creature"
) {
updateCreature(activeCreatureId);
}
}, [activeCreatureId, panelView.mode, updateCreature]);
}, [encounter.activeIndex, activeCreatureId, panelView.mode, updateCreature]);
}

View File

@@ -39,6 +39,9 @@ function resolve(pref: ThemePreference): ResolvedTheme {
function applyTheme(resolved: ResolvedTheme): void {
document.documentElement.dataset.theme = resolved;
document
.querySelector('meta[name="theme-color"]')
?.setAttribute("content", resolved === "light" ? "#eeecea" : "#0e1a2e");
}
function notifyAll(): void {

View File

@@ -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");
});
});

View File

@@ -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<string> = 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,
);
}

View File

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