import { useState } from 'react';
import { Minus, Plus, Skull, Star } from 'lucide-react';
import { Button } from '@/components/ui/Button';
import { NumberField } from '@/components/ui/NumberField';
import { maxDying, isDead, recoveryDc, knockOut, applyRecovery, heroPointRescue } from '@/lib/mechanics';
import type { Degree } from '@/lib/dice/check';
import type { Condition } from '@/lib/schemas/common';
import { SheetSection, type SectionProps } from './common';
function Pips({ count, filled, onToggle, label }: { count: number; filled: number; onToggle: (n: number) => void; label: string }) {
return (
{Array.from({ length: count }, (_, i) => (
);
}
/** PF2e dying/wounded/doomed with knock-out + human-rolled recovery checks. */
function Pf2eDefenses({ c, update }: SectionProps) {
const d = c.defenses;
const [fromCrit, setFromCrit] = useState(false);
const dead = isDead(d.dying, d.doomed);
const withUnconscious = (conds: Condition[]): Condition[] =>
conds.some((x) => x.name.trim().toLowerCase() === 'unconscious') ? conds : [...conds, { name: 'Unconscious' }];
const withoutUnconscious = (conds: Condition[]): Condition[] =>
conds.filter((x) => x.name.trim().toLowerCase() !== 'unconscious');
const setDefenses = (p: Partial) => update({ defenses: { ...d, ...p } });
// You stay Unconscious at 0 HP even after recovering out of dying — only healing
// above 0 HP (or the GM) wakes you. So the sync is: dying>0 OR hp<=0 ⇒ unconscious.
const syncUnconscious = (dying: number, hpCurrent: number) =>
dying > 0 || hpCurrent <= 0 ? withUnconscious(c.conditions) : withoutUnconscious(c.conditions);
/** Set dying and keep the Unconscious condition in sync. */
const setDying = (dying: number) =>
update({ defenses: { ...d, dying }, conditions: syncUnconscious(dying, c.hp.current) });
const doKnockOut = () => {
const { dying } = knockOut(d, fromCrit);
// Knocked out = at 0 HP and dying; drop HP to 0 so the sheet state is coherent.
update({ defenses: { ...d, dying }, conditions: withUnconscious(c.conditions), hp: { ...c.hp, current: Math.min(0, c.hp.current) } });
};
const doRecover = (degree: Degree) => {
const res = applyRecovery(d, degree);
update({ defenses: { ...d, ...res }, conditions: syncUnconscious(res.dying, c.hp.current) });
};
const doHeroRescue = () => {
const res = heroPointRescue(d);
if (!res) return;
update({ defenses: { ...d, ...res }, conditions: syncUnconscious(0, c.hp.current) });
};
return (
{dead && Dead}
setDefenses({ wounded: v })} aria-label="Wounded value" />
setDefenses({ doomed: v })} aria-label="Doomed value" />
{d.heroPoints}
{d.dying === 0 ? (
) : (
Recovery check
DC {recoveryDc(d.dying)}
Roll a flat d20 vs the DC, then record the result (success lowers Dying; reaching 0 makes you Wounded):
{d.heroPoints >= 1 && (
Heroic Recovery: spend ALL your Hero Points ({d.heroPoints}) to drop to Dying 0 and stabilize.
)}
)}
);
}
export function DefensesSection({ c, update }: SectionProps) {
const d = c.defenses;
const setD = (p: Partial) => update({ defenses: { ...d, ...p } });
return (
{c.system === '5e' ? (
Successes
setD({ deathSaves: { ...d.deathSaves, successes: n } })} />
Failures
setD({ deathSaves: { ...d.deathSaves, failures: n } })} />
setD({ exhaustion: v })} aria-label="Exhaustion level" />
{d.exhaustion >= 4 && Max HP halved}
) : (
)}
);
}
function Field({ label, children }: { label: string; children: React.ReactNode }) {
return (
);
}