186 lines
6.9 KiB
JavaScript
186 lines
6.9 KiB
JavaScript
// 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');
|
||
|
|
});
|
||
|
|
});
|