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>
73 lines
1.9 KiB
TypeScript
73 lines
1.9 KiB
TypeScript
import type { PlayerCharacter } from "@initiative/domain";
|
|
import { type RefObject, useImperativeHandle, useState } from "react";
|
|
import { usePlayerCharactersContext } from "../contexts/player-characters-context.js";
|
|
import { CreatePlayerModal } from "./create-player-modal.js";
|
|
import { PlayerManagement } from "./player-management.js";
|
|
|
|
export interface PlayerCharacterSectionHandle {
|
|
openManagement: () => void;
|
|
}
|
|
|
|
export const PlayerCharacterSection = function PlayerCharacterSectionInner({
|
|
ref,
|
|
}: {
|
|
ref?: RefObject<PlayerCharacterSectionHandle | null>;
|
|
}) {
|
|
const { characters, createCharacter, editCharacter, deleteCharacter } =
|
|
usePlayerCharactersContext();
|
|
|
|
const [managementOpen, setManagementOpen] = useState(false);
|
|
const [createOpen, setCreateOpen] = useState(false);
|
|
const [editingPlayer, setEditingPlayer] = useState<
|
|
PlayerCharacter | undefined
|
|
>();
|
|
|
|
useImperativeHandle(ref, () => ({
|
|
openManagement: () => setManagementOpen(true),
|
|
}));
|
|
|
|
return (
|
|
<>
|
|
<CreatePlayerModal
|
|
open={createOpen}
|
|
onClose={() => {
|
|
setCreateOpen(false);
|
|
setEditingPlayer(undefined);
|
|
setManagementOpen(true);
|
|
}}
|
|
onSave={(name, ac, maxHp, color, icon, level) => {
|
|
if (editingPlayer) {
|
|
editCharacter(editingPlayer.id, {
|
|
name,
|
|
ac,
|
|
maxHp,
|
|
color: color ?? null,
|
|
icon: icon ?? null,
|
|
level: level ?? null,
|
|
});
|
|
} else {
|
|
createCharacter(name, ac, maxHp, color, icon, level);
|
|
}
|
|
}}
|
|
playerCharacter={editingPlayer}
|
|
/>
|
|
<PlayerManagement
|
|
open={managementOpen}
|
|
onClose={() => setManagementOpen(false)}
|
|
characters={characters}
|
|
onEdit={(pc) => {
|
|
setEditingPlayer(pc);
|
|
setCreateOpen(true);
|
|
setManagementOpen(false);
|
|
}}
|
|
onDelete={(id) => deleteCharacter(id)}
|
|
onCreate={() => {
|
|
setEditingPlayer(undefined);
|
|
setCreateOpen(true);
|
|
setManagementOpen(false);
|
|
}}
|
|
/>
|
|
</>
|
|
);
|
|
};
|