Add Crit Damage button for dying/stable participants

5e crit-at-0-HP rule: any crit damage while downed = +2 death-save failures.
Shared logic supported via applyHpChange(..., { isCriticalHit: true }) but UI
had no trigger. Normal Damage at 0 only added 1 fail.

handleCritDamage handler calls applyHpChange with isCriticalHit: true.
Button renders in HP block (right-aligned) for character/NPC participants
when dying or stable and combat started. Dead excluded (no-op).
This commit is contained in:
david raistrick
2026-07-06 18:38:49 -04:00
parent 6fe4a27dd5
commit 47056788e8
2 changed files with 16 additions and 8 deletions
-7
View File
@@ -6,13 +6,6 @@ Backlog of bugs + long-term items. Milestones live in REWORK_PLAN.md.
### FEAT: critical damage button for dying/stable participants
- Shared logic already supports `applyHpChange(..., { isCriticalHit:true })`.
- UI only has normal Damage, which adds 1 failure at 0 HP.
- Add button near Revive/Stabilize: **Crit Damage** / **Critical Hit**.
- Use only for dying/stable character/NPC at 0 HP; adds 2 death-save failures.
- Dead target remains dead; no action.
### dm list - keep active particpant in view (scroll) ### dm list - keep active particpant in view (scroll)
### combat.js doesnt seem to actually exercise the add characters. there are no characters in the campaign. only in the encounter.. ### combat.js doesnt seem to actually exercise the add characters. there are no characters in the campaign. only in the encounter..
+16 -1
View File
@@ -7,7 +7,7 @@ import {
Play as PlayIcon, Pause as PauseIcon, SkipForward as SkipForwardIcon, Play as PlayIcon, Pause as PauseIcon, SkipForward as SkipForwardIcon,
StopCircle as StopCircleIcon, Users2, Dices, ChevronUp, ChevronDown, ScrollText, StopCircle as StopCircleIcon, Users2, Dices, ChevronUp, ChevronDown, ScrollText,
Maximize2, Minimize2, Moon, Coffee, Clock, ChevronRight, X, Maximize2, Minimize2, Moon, Coffee, Clock, ChevronRight, X,
Undo2, Redo2 Undo2, Redo2, Crosshair
} from 'lucide-react'; } from 'lucide-react';
// ----- UI feedback: toast (transient) + info modal (persistent) ----- // ----- UI feedback: toast (transient) + info modal (persistent) -----
@@ -1153,6 +1153,16 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters, camp
} }
}; };
const handleCritDamage = async (participantId) => {
if (!db) return;
try {
// Any damage at 0 HP + isCriticalHit = 2 death-save failures (5e crit-at-0).
await combatApplyHpChange(encounter, participantId, 'damage', 1, { isCriticalHit: true }, buildCtx(encounterPath));
} catch (err) {
showToast(err.message);
}
};
const toggleCondition = async (participantId, conditionId) => { const toggleCondition = async (participantId, conditionId) => {
if (!db) return; if (!db) return;
try { try {
@@ -1464,6 +1474,11 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters, camp
/> />
</span> </span>
<span>HP: {p.currentHp}/{p.maxHp}</span> <span>HP: {p.currentHp}/{p.maxHp}</span>
{hasDeathSaves && (participantStatus === 'dying' || participantStatus === 'stable') && encounter.isStarted && (
<button onClick={() => handleCritDamage(p.id)} className="ml-auto px-2 py-0.5 text-xs rounded bg-red-900 hover:bg-red-800 text-red-100 border border-red-700" title="Critical hit at 0 HP: +2 death-save failures">
<Crosshair size={12} className="inline mr-1" />Crit
</button>
)}
</div> </div>
{participantStatus === 'dead' && ( {participantStatus === 'dead' && (