Files
healer-man/src/ui/PlayerFrame.tsx
T
2026-08-20 14:05:50 -04:00

73 lines
2.4 KiB
TypeScript

import { classById, raceById } from "../app/characterCatalog";
import type { CharacterProfile } from "../app/types";
export interface PlayerFrameMeter {
current: number;
maximum: number;
label: string;
tone: string;
}
export interface PlayerFrameProps {
character: CharacterProfile | null;
health: PlayerFrameMeter;
resource: PlayerFrameMeter;
shield?: number;
dead?: boolean;
}
function safePercent(current: number, maximum: number): number {
if (!Number.isFinite(current) || !Number.isFinite(maximum) || maximum <= 0) return 0;
return Math.max(0, Math.min(100, (current / maximum) * 100));
}
function Meter({ current, maximum, label, tone }: PlayerFrameMeter) {
const percent = safePercent(current, maximum);
return (
<div className={`player-frame__meter player-frame__meter--${tone}`}>
<div className="player-frame__meter-label">
<span>{label}</span>
<strong>{Math.max(0, Math.round(current))} / {Math.max(0, Math.round(maximum))}</strong>
</div>
<div
className="player-frame__meter-track"
role="progressbar"
aria-label={`${label}: ${Math.round(percent)} percent`}
aria-valuemin={0}
aria-valuemax={Math.max(0, Math.round(maximum))}
aria-valuenow={Math.max(0, Math.round(current))}
>
<span style={{ width: `${percent}%` }} />
</div>
</div>
);
}
export function PlayerFrame({ character, health, resource, shield = 0, dead = false }: PlayerFrameProps) {
const race = character ? raceById(character.raceId) : null;
const characterClass = character ? classById(character.classId) : null;
return (
<section className={`player-frame ${dead ? "player-frame--dead" : ""}`} aria-label="Player status">
<div
className="player-frame__portrait"
style={{ borderColor: characterClass?.color ?? race?.accent }}
aria-hidden="true"
>
<span>{race?.sigil ?? "HM"}</span>
<b>{character?.level ?? 1}</b>
</div>
<div className="player-frame__body">
<header>
<strong>{character?.name ?? "Adventurer"}</strong>
<span>{characterClass?.name ?? "Explorer"}</span>
</header>
<Meter {...health} />
<Meter {...resource} />
{shield > 0 && <small className="player-frame__shield">+{Math.round(shield)} absorb</small>}
{dead && <small className="player-frame__dead-label">Defeated</small>}
</div>
</section>
);
}