Files
initiative/apps/web/src/components/turn-navigation.tsx
T
Lukas ef76b9c90b
CI / check (push) Successful in 1m13s
CI / build-image (push) Successful in 16s
Add encounter difficulty indicator (5.5e XP budget)
Live 3-bar difficulty indicator in the top bar showing encounter
difficulty (Trivial/Low/Moderate/High) based on the 2024 5.5e XP
budget system. Automatically derived from PC levels and bestiary
creature CRs.

- Add optional level field (1-20) to PlayerCharacter
- Add CR-to-XP and XP Budget per Character lookup tables in domain
- Add calculateEncounterDifficulty pure function
- Add DifficultyIndicator component with color-coded bars and tooltip
- Add useDifficulty hook composing encounter, PC, and bestiary contexts
- Indicator hidden when no PCs with levels or no bestiary-linked monsters
- Level field in PC create/edit forms, persisted in storage

Closes #18

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:55:48 +01:00

97 lines
2.6 KiB
TypeScript

import { Redo2, StepBack, StepForward, Trash2, Undo2 } from "lucide-react";
import { useEncounterContext } from "../contexts/encounter-context.js";
import { useDifficulty } from "../hooks/use-difficulty.js";
import { DifficultyIndicator } from "./difficulty-indicator.js";
import { Button } from "./ui/button.js";
import { ConfirmButton } from "./ui/confirm-button.js";
export function TurnNavigation() {
const {
encounter,
advanceTurn,
retreatTurn,
clearEncounter,
undo,
redo,
canUndo,
canRedo,
} = useEncounterContext();
const difficulty = useDifficulty();
const hasCombatants = encounter.combatants.length > 0;
const isAtStart = encounter.roundNumber === 1 && encounter.activeIndex === 0;
const activeCombatant = encounter.combatants[encounter.activeIndex];
return (
<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"
onClick={retreatTurn}
disabled={!hasCombatants || isAtStart}
title="Previous turn"
aria-label="Previous turn"
>
<StepBack className="h-5 w-5" />
</Button>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
onClick={undo}
disabled={!canUndo}
title="Undo"
aria-label="Undo"
>
<Undo2 className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={redo}
disabled={!canRedo}
title="Redo"
aria-label="Redo"
>
<Redo2 className="h-4 w-4" />
</Button>
</div>
<div className="flex min-w-0 flex-1 items-center justify-center gap-2 text-sm">
<span className="shrink-0 rounded-md bg-muted px-2 py-0.5 font-semibold text-foreground text-sm">
<span className="-mt-[3px] inline-block">
R{encounter.roundNumber}
</span>
</span>
{activeCombatant ? (
<span className="truncate font-medium">{activeCombatant.name}</span>
) : (
<span className="text-muted-foreground">No combatants</span>
)}
{difficulty && <DifficultyIndicator result={difficulty} />}
</div>
<div className="flex flex-shrink-0 items-center gap-3">
<ConfirmButton
icon={<Trash2 className="h-5 w-5" />}
label="Clear encounter"
onConfirm={clearEncounter}
disabled={!hasCombatants}
className="text-muted-foreground"
/>
<Button
variant="ghost"
size="icon"
onClick={advanceTurn}
disabled={!hasCombatants}
title="Next turn"
aria-label="Next turn"
>
<StepForward className="h-5 w-5" />
</Button>
</div>
</div>
);
}