Exercise death-save edge cases in combat replay and scenario tests
Replay (scripts/combat/replay.js):
- Merchant now type=npc via asNpc (was stale isNpc)
- Death-save eligibility keyed on type (character|npc), not removed isNpc
- Rotate outcomes success/fail/nat1/nat20 (was success-only)
- Import stabilizeParticipant, reviveParticipant
- Between-round revive: dead -> reviveParticipant, stable -> heal,
inactive -> reactivate (heal was no-op on dead, leaving active-dead monsters)
- Fix deathSave return unwrap: it returns {enc, status}; callStep runner
must extract .enc or state corrupts (round undefined, combat auto-ends)
- describe() fix: death-save arg is outcome, not type
Scenario (src/tests/Combat.scenario.test.js):
- Import stabilizeParticipant, reviveParticipant
- applyDamage accepts options (crit at 0 HP)
- Add stabilizeAction, reviveAction helpers
- addAllCharacters now actually exercises batch add (was no-op)
- New deterministic edge-case test: monster death (dead+inactive),
NPC nat20, NPC nat1+damage+revive+heal, character stable->damage->crit,
massive damage
This commit is contained in:
+30
-17
@@ -16,6 +16,7 @@ const {
|
||||
startEncounter, nextTurn, togglePause, endEncounter,
|
||||
addParticipant, updateParticipant, removeParticipant,
|
||||
toggleParticipantActive, applyHpChange, deathSave,
|
||||
stabilizeParticipant, reviveParticipant,
|
||||
toggleCondition, reorderParticipants,
|
||||
} = shared;
|
||||
const { createServerStorage } = require('../../src/storage/server');
|
||||
@@ -51,7 +52,7 @@ function buildMonsters() {
|
||||
{ name: 'Goblin2', maxHp: 30, initMod: 2 },
|
||||
{ name: 'OrcBoss', maxHp: 120, initMod: 1 },
|
||||
{ name: 'Wolf', maxHp: 40, initMod: 3 },
|
||||
{ name: 'Merchant', maxHp: 30, initMod: 0, isNpc: true },
|
||||
{ name: 'Merchant', maxHp: 30, initMod: 0, asNpc: true },
|
||||
];
|
||||
}
|
||||
|
||||
@@ -221,7 +222,7 @@ module.exports = async function replay(args) {
|
||||
: `${a.target} took ${a.amount} damage`;
|
||||
case 'toggleCondition': return `${a.participant} ${a.condition} toggled`;
|
||||
case 'updateParticipant': return `${a.participant} edited`;
|
||||
case 'deathSave': return `${a.participant} death save (${a.type})`;
|
||||
case 'deathSave': return `${a.participant} death save (${a.outcome})`;
|
||||
case 'toggleParticipantActive':
|
||||
return a.revive ? `${a.participant} reactivated` : `${a.participant} toggled`;
|
||||
case 'togglePause': return a.to === 'paused' ? 'Combat paused' : 'Combat resumed';
|
||||
@@ -301,11 +302,14 @@ module.exports = async function replay(args) {
|
||||
(e) => updateParticipant(e, actor.id, { notes: `edited r${enc.round}` }, ctx))).enc;
|
||||
}
|
||||
|
||||
if (actor.status === 'dying' && !actor.isNpc) {
|
||||
if (VERBOSE) console.log(` deathSave ${actor.name} +1 success`);
|
||||
enc = (await callStep('deathSave',
|
||||
{ participant: actor.name, outcome: 'success' },
|
||||
(e) => deathSave(e, actor.id, 'success', ctx))).enc;
|
||||
if (actor.status === 'dying' && (actor.type === 'character' || actor.type === 'npc')) {
|
||||
const outcomes = ['success', 'fail', 'nat1', 'nat20'];
|
||||
const outcome = outcomes[totalTurns % outcomes.length];
|
||||
if (VERBOSE) console.log(` deathSave ${actor.name} ${outcome}`);
|
||||
const dsRes = await callStep('deathSave',
|
||||
{ participant: actor.name, outcome },
|
||||
async (e) => (await deathSave(e, actor.id, outcome, ctx)).enc);
|
||||
enc = dsRes.enc;
|
||||
}
|
||||
|
||||
if (totalTurns % 9 === 0) {
|
||||
@@ -376,19 +380,28 @@ module.exports = async function replay(args) {
|
||||
}
|
||||
console.log(`--- round ${enc.round} ---`);
|
||||
lastRound = enc.round;
|
||||
const dead = enc.participants.filter(p => p.currentHp <= 0 || p.isActive === false);
|
||||
for (const d of dead) {
|
||||
const down = enc.participants.filter(p => p.currentHp <= 0 || p.isActive === false);
|
||||
for (const d of down) {
|
||||
if (interrupted) break;
|
||||
if (d.isActive === false) {
|
||||
if (VERBOSE) console.log(` revive ${d.name}`);
|
||||
enc = (await callStep('toggleParticipantActive',
|
||||
if (d.status === 'dead') {
|
||||
if (VERBOSE) console.log(` revive dead ${d.name}`);
|
||||
enc = (await callStep('reviveParticipant',
|
||||
{ participant: d.name, revive: true },
|
||||
(e) => toggleParticipantActive(e, d.id, ctx))).enc;
|
||||
(e) => reviveParticipant(e, d.id, ctx))).enc;
|
||||
}
|
||||
if (d.status === 'stable') {
|
||||
if (VERBOSE) console.log(` heal stable ${d.name} +${d.maxHp}`);
|
||||
enc = (await callStep('applyHpChange',
|
||||
{ target: d.name, changeType: 'heal', amount: d.maxHp, revive: true },
|
||||
(e) => applyHpChange(e, d.id, 'heal', d.maxHp, ctx))).enc;
|
||||
}
|
||||
const latest = (enc.participants || []).find(p => p.id === d.id);
|
||||
if (latest && latest.isActive === false) {
|
||||
if (VERBOSE) console.log(` reactivate ${latest.name}`);
|
||||
enc = (await callStep('toggleParticipantActive',
|
||||
{ participant: latest.name, revive: true },
|
||||
(e) => toggleParticipantActive(e, latest.id, ctx))).enc;
|
||||
}
|
||||
if (VERBOSE) console.log(` heal ${d.name} +${d.maxHp}`);
|
||||
enc = (await callStep('applyHpChange',
|
||||
{ target: d.name, changeType: 'heal', amount: d.maxHp, revive: true },
|
||||
(e) => applyHpChange(e, d.id, 'heal', d.maxHp, ctx))).enc;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user