ef76b9c90b
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>
97 lines
2.6 KiB
TypeScript
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>
|
|
);
|
|
}
|