Compare commits

..
12 Commits
Author SHA1 Message Date
LukasandClaude Opus 4.6 228a2603e8 Add Sapped and Slowed conditions for 5.5e weapon mastery
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
LukasandClaude Opus 4.6 27ff8ba1ad Collapse hover-only buttons to zero width when hidden
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
LukasandClaude Opus 4.6 4cfcefe6c3 Hide custom stat fields on mobile, fix action bar gap consistency
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
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
LukasandClaude Opus 4.6 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
LukasandClaude Opus 4.6 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
LukasandClaude Opus 4.6 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
LukasandClaude Opus 4.6 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
LukasandClaude Opus 4.6 4d0ec0c7b2 Add Open Graph meta tags for link previews in messengers
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
LukasandClaude Opus 4.6 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
LukasandClaude Opus 4.6 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
LukasandClaude Opus 4.6 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
+10 -1
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

+21
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
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

+21
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"
}
]
}
+9 -3
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
+5 -4
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"
+5 -5
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"
+8 -2
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);
+6 -1
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();
@@ -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)) {
+1 -1
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"
+1 -1
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}
+13 -3
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]);
}
+3
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 {
@@ -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");
});
});
+32
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,
);
}
+1
View File
@@ -11,6 +11,7 @@ export {
type ConditionDefinition,
type ConditionId,
getConditionDescription,
getConditionsForEdition,
type RulesEdition,
VALID_CONDITION_IDS,
} from "./conditions.js";