107 lines
3.4 KiB
TypeScript
107 lines
3.4 KiB
TypeScript
import type { Character } from "../types";
|
|
import HpBar from "./HpBar";
|
|
import TorchTimer from "./TorchTimer";
|
|
import styles from "./CharacterCard.module.css";
|
|
import { calculateAC } from "../utils/derived-ac";
|
|
import { getTalentHpBonus } from "../utils/talent-effects";
|
|
import { getEffectiveStat } from "../utils/talent-effects";
|
|
import { getModifier, formatModifier } from "../utils/modifiers";
|
|
|
|
function getAvatarUrl(character: Character): string {
|
|
const style = (character.overrides?.avatar_style as string) || "micah";
|
|
const seed = encodeURIComponent(character.name || "hero");
|
|
return `https://api.dicebear.com/9.x/${style}/svg?seed=${seed}`;
|
|
}
|
|
|
|
const STATS = ["STR", "DEX", "CON", "INT", "WIS", "CHA"];
|
|
|
|
interface CharacterCardProps {
|
|
character: Character;
|
|
onHpChange: (characterId: number, hp: number) => void;
|
|
onUpdate: (characterId: number, data: Partial<Character>) => void;
|
|
onClick: (characterId: number) => void;
|
|
canEdit?: boolean;
|
|
focusSpell?: string;
|
|
}
|
|
|
|
export default function CharacterCard({
|
|
character,
|
|
onHpChange,
|
|
onUpdate,
|
|
onClick,
|
|
canEdit = true,
|
|
focusSpell,
|
|
}: CharacterCardProps) {
|
|
return (
|
|
<div
|
|
className={styles.card}
|
|
onClick={() => onClick(character.id)}
|
|
style={{ borderLeftColor: character.color, borderLeftWidth: "3px" }}
|
|
>
|
|
<div className={styles.cardHeader}>
|
|
<img className={styles.avatar} src={getAvatarUrl(character)} alt="" />
|
|
<div className={styles.nameRow}>
|
|
<span className={styles.name}>
|
|
{character.name}
|
|
{character.title ? ` ${character.title}` : ""}
|
|
</span>
|
|
<span className={styles.level}>Lvl {character.level}</span>
|
|
</div>
|
|
</div>
|
|
<div className={styles.meta}>
|
|
{character.ancestry} {character.class}
|
|
</div>
|
|
{focusSpell && (
|
|
<div className={styles.focusIndicator}>
|
|
● Focusing: {focusSpell}
|
|
</div>
|
|
)}
|
|
|
|
<div className={styles.vitalsRow} onClick={(e) => e.stopPropagation()}>
|
|
<HpBar
|
|
current={character.hp_current}
|
|
max={character.hp_max + getTalentHpBonus(character)}
|
|
onChange={(hp) => onHpChange(character.id, hp)}
|
|
/>
|
|
<div className={styles.ac}>
|
|
<span className={styles.acLabel}>AC</span>
|
|
<span className={styles.acValue}>
|
|
{calculateAC(character).effective}
|
|
</span>
|
|
</div>
|
|
<span
|
|
className={styles.luck}
|
|
title={character.luck_token ? "Luck available" : "Luck spent"}
|
|
>
|
|
{character.luck_token ? "\u2605" : "\u2606"}
|
|
</span>
|
|
<TorchTimer
|
|
torchLitAt={character.torch_lit_at}
|
|
onToggle={() => {
|
|
const isLit = character.torch_lit_at !== null;
|
|
onUpdate(character.id, {
|
|
torch_lit_at: isLit ? null : new Date().toISOString(),
|
|
} as Partial<Character>);
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<div className={styles.modRow}>
|
|
{STATS.map((stat) => {
|
|
const value = getEffectiveStat(character, stat);
|
|
const mod = getModifier(value);
|
|
return (
|
|
<span key={stat} className={styles.mod}>
|
|
<span className={styles.modLabel}>{stat}</span>
|
|
<span className={styles.modValue}>{formatModifier(mod)}</span>
|
|
</span>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div className={styles.xp}>
|
|
XP {character.xp} / {character.level * 10}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|