// Generic (non-5e) ruleset: no death saves, negative HP allowed, down status. const shared = require('@ttrpg/shared'); const { mockCtx } = require('./_helpers'); const { buildCharacterParticipant, buildMonsterParticipant, startEncounter, applyHpChange, deathSave, markDead, reviveParticipant, } = shared; function char(id, hp = 100) { const { participant } = buildCharacterParticipant({ id: `orig-${id}`, name: id, defaultMaxHp: hp, defaultInitMod: 2 }); participant.id = id; return participant; } function mon(id, hp = 100) { const { participant } = buildMonsterParticipant({ name: id, maxHp: hp, initMod: 2 }); participant.id = id; return participant; } function enc(ps, ruleset = 'generic') { return { name: 't', participants: ps, isStarted: false, isPaused: false, round: 0, currentTurnParticipantId: null, turnOrderIds: [], ruleset, }; } const snap = (e) => JSON.parse(JSON.stringify(e)); describe('generic ruleset', () => { test('damage past 0 = negative HP, status down (character)', async () => { const { ctx } = mockCtx(); let e = enc([char('a', 10)]); e = await startEncounter(e, ctx); const newEnc = await applyHpChange(e, 'a', 'damage', 15, ctx); const p = newEnc.participants.find(x => x.id === 'a'); expect(p.currentHp).toBe(-5); expect(p.status).toBe('down'); }); test('damage exactly to 0 = status down', async () => { const { ctx } = mockCtx(); let e = enc([char('a', 10)]); e = await startEncounter(e, ctx); const newEnc = await applyHpChange(e, 'a', 'damage', 10, ctx); const p = newEnc.participants.find(x => x.id === 'a'); expect(p.currentHp).toBe(0); expect(p.status).toBe('down'); }); test('heal from negative = conscious', async () => { const { ctx } = mockCtx(); let e = enc([char('a', 10)]); e = await startEncounter(e, ctx); e = await applyHpChange(e, 'a', 'damage', 15, ctx); // -5 const newEnc = await applyHpChange(e, 'a', 'heal', 10, ctx); const p = newEnc.participants.find(x => x.id === 'a'); expect(p.currentHp).toBe(5); expect(p.status).toBe('conscious'); }); test('heal caps at maxHp', async () => { const { ctx } = mockCtx(); let e = enc([char('a', 10)]); e = await startEncounter(e, ctx); e = await applyHpChange(e, 'a', 'damage', 5, ctx); // 5 const newEnc = await applyHpChange(e, 'a', 'heal', 20, ctx); const p = newEnc.participants.find(x => x.id === 'a'); expect(p.currentHp).toBe(10); expect(p.status).toBe('conscious'); }); test('monster death = auto-inactive (generic)', async () => { const { ctx } = mockCtx(); let e = enc([mon('m', 10)]); e = await startEncounter(e, ctx); const newEnc = await applyHpChange(e, 'm', 'damage', 20, ctx); const p = newEnc.participants.find(x => x.id.startsWith('m')); expect(p.currentHp).toBe(-10); expect(p.status).toBe('dead'); expect(p.isActive).toBe(false); }); test('monster at exactly 0 = dead + inactive (generic)', async () => { const { ctx } = mockCtx(); let e = enc([mon('m', 10)]); e = await startEncounter(e, ctx); const newEnc = await applyHpChange(e, 'm', 'damage', 10, ctx); const p = newEnc.participants.find(x => x.id.startsWith('m')); expect(p.currentHp).toBe(0); expect(p.status).toBe('dead'); expect(p.isActive).toBe(false); }); test('deathSave throws in generic mode', async () => { const { ctx } = mockCtx(); let e = enc([char('a', 10)]); e = await startEncounter(e, ctx); e = await applyHpChange(e, 'a', 'damage', 10, ctx); // down await expect(deathSave(e, 'a', 'fail', ctx)).rejects.toThrow(); }); test('revive in generic mode: dead→conscious (no stable)', async () => { const { ctx } = mockCtx(); let e = enc([char('a', 10)]); e = await startEncounter(e, ctx); e = await applyHpChange(e, 'a', 'damage', 10, ctx); // down e = await markDead(e, 'a', ctx); // dead const newEnc = await reviveParticipant(e, 'a', ctx); const p = newEnc.participants.find(x => x.id === 'a'); expect(p.status).toBe('conscious'); expect(p.currentHp).toBe(0); // HP unchanged from down }); test('revive generic reactivates if inactive', async () => { const { ctx } = mockCtx(); let e = enc([mon('m', 10)]); e = await startEncounter(e, ctx); e = await markDead(e, e.participants[0].id, ctx); // dead+inactive const newEnc = await reviveParticipant(e, e.participants[0].id, ctx); const p = newEnc.participants[0]; expect(p.status).toBe('conscious'); expect(p.isActive).toBe(true); }); test('markDead sets status dead (character, stays active)', async () => { const { ctx } = mockCtx(); let e = enc([char('a', 10)]); e = await startEncounter(e, ctx); e = await applyHpChange(e, 'a', 'damage', 10, ctx); // down const newEnc = await markDead(e, 'a', ctx); const p = newEnc.participants.find(x => x.id === 'a'); expect(p.status).toBe('dead'); expect(p.isActive).toBe(true); }); test('markDead on monster = dead + inactive', async () => { const { ctx } = mockCtx(); let e = enc([mon('m', 10)]); e = await startEncounter(e, ctx); e = await applyHpChange(e, 'm', 'damage', 5, ctx); // down (5hp) const newEnc = await markDead(e, e.participants[0].id, ctx); const p = newEnc.participants[0]; expect(p.status).toBe('dead'); expect(p.isActive).toBe(false); }); test('markDead on conscious = dead', async () => { const { ctx } = mockCtx(); let e = enc([char('a', 10)]); e = await startEncounter(e, ctx); const newEnc = await markDead(e, 'a', ctx); const p = newEnc.participants.find(x => x.id === 'a'); expect(p.status).toBe('dead'); }); test('damage while down = more negative (no death saves)', async () => { const { ctx } = mockCtx(); let e = enc([char('a', 10)]); e = await startEncounter(e, ctx); e = await applyHpChange(e, 'a', 'damage', 15, ctx); // -5 down const newEnc = await applyHpChange(e, 'a', 'damage', 5, ctx); const p = newEnc.participants.find(x => x.id === 'a'); expect(p.currentHp).toBe(-10); expect(p.status).toBe('down'); }); test('no unconscious condition auto-added (generic)', async () => { const { ctx } = mockCtx(); let e = enc([char('a', 10)]); e = await startEncounter(e, ctx); const newEnc = await applyHpChange(e, 'a', 'damage', 10, ctx); const p = newEnc.participants.find(x => x.id === 'a'); expect(p.conditions).not.toContain('unconscious'); }); test('down→conscious clears dead? no, dead is separate', async () => { // sanity: down heal to conscious, dead untouched const { ctx } = mockCtx(); let e = enc([char('a', 10)]); e = await startEncounter(e, ctx); e = await applyHpChange(e, 'a', 'damage', 10, ctx); // down const newEnc = await applyHpChange(e, 'a', 'heal', 10, ctx); const p = newEnc.participants.find(x => x.id === 'a'); expect(p.status).toBe('conscious'); }); });