Support the 2014 DMG encounter difficulty as an alternative to the 5.5e system behind the existing Rules Edition toggle. The 2014 system uses Easy/Medium/Hard/Deadly thresholds, an encounter multiplier based on monster count, and party size adjustment (×0.5–×5 range). - Extract RulesEdition to its own domain module - Refactor DifficultyTier to abstract numeric values (0–3) - Restructure DifficultyResult with thresholds array - Add 2014 XP thresholds table and encounter multiplier logic - Wire edition from context into difficulty hooks - Edition-aware labels in indicator and breakdown panel - Show multiplier, adjusted XP, and party size note for 2014 - Rename settings label from "Conditions" to "Rules Edition" - Update spec 008 with issue #23 requirements Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
120 lines
3.4 KiB
TypeScript
120 lines
3.4 KiB
TypeScript
import { Redo2, StepBack, StepForward, Trash2, Undo2 } from "lucide-react";
|
|
import { useState } from "react";
|
|
import { useEncounterContext } from "../contexts/encounter-context.js";
|
|
import { useRulesEditionContext } from "../contexts/rules-edition-context.js";
|
|
import { useDifficulty } from "../hooks/use-difficulty.js";
|
|
import { DifficultyBreakdownPanel } from "./difficulty-breakdown-panel.js";
|
|
import {
|
|
DifficultyIndicator,
|
|
TIER_LABELS_5_5E,
|
|
TIER_LABELS_2014,
|
|
} 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 { edition } = useRulesEditionContext();
|
|
const tierLabels = edition === "5e" ? TIER_LABELS_2014 : TIER_LABELS_5_5E;
|
|
const [showBreakdown, setShowBreakdown] = useState(false);
|
|
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 grid grid-cols-[1fr_minmax(0,auto)_1fr] items-center border-border border-b bg-card px-2 py-3 sm:rounded-lg sm:border sm:px-4">
|
|
{/* Left zone: navigation + history + round */}
|
|
<div className="flex items-center gap-1">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={retreatTurn}
|
|
disabled={!hasCombatants || isAtStart}
|
|
title="Previous turn"
|
|
aria-label="Previous turn"
|
|
>
|
|
<StepBack className="h-5 w-5" />
|
|
</Button>
|
|
<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>
|
|
<span className="ml-1 rounded-md bg-muted px-2 py-0.5 font-semibold text-foreground text-sm tabular-nums">
|
|
R{encounter.roundNumber}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Center zone: active combatant name */}
|
|
<div className="min-w-0 px-2 text-center text-sm">
|
|
{activeCombatant ? (
|
|
<span className="truncate font-medium">{activeCombatant.name}</span>
|
|
) : (
|
|
<span className="text-muted-foreground">No combatants</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Right zone: difficulty + destructive + forward */}
|
|
<div className="flex items-center justify-end gap-1">
|
|
{difficulty && (
|
|
<div className="relative mr-1">
|
|
<DifficultyIndicator
|
|
result={difficulty}
|
|
labels={tierLabels}
|
|
onClick={() => setShowBreakdown((prev) => !prev)}
|
|
/>
|
|
{showBreakdown ? (
|
|
<DifficultyBreakdownPanel
|
|
onClose={() => setShowBreakdown(false)}
|
|
/>
|
|
) : null}
|
|
</div>
|
|
)}
|
|
<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>
|
|
);
|
|
}
|