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 ? (
Knocked to 0 HP? Enter dying (adds your Wounded value{d.wounded > 0 ? ` of ${d.wounded}` : ''}).
) : (
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 (
{label}
{children}
); }