- Add status-driven death-save model: - status: conscious | dying | stable | dead - deathSaveSuccesses/deathSaveFailures as display counters - remove old death-save fields as source of truth - Add death-save actions: - success, fail, nat1, nat20 - stabilize participant - revive dead participant to 0 HP, stable, unconscious - Apply 5e HP transition rules: - characters and NPCs at 0 HP become dying - stable/dying participants gain unconscious condition - healing clears death saves and returns conscious - massive damage kills - non-NPC monsters at 0 HP become dead and inactive - Split NPCs from monsters with type="npc": - NPCs use death saves like characters - NPCs remain DM-controlled/monster-colored in UI - new NPCs no longer persist isNpc flag - Keep dead PCs/NPCs in encounter and initiative until DM removes or deactivates - Allow DM active/inactive toggle for dead participants - Hide inactive participants from player display - Improve DM and player death-state UI: - show Dying/Dead/Unconscious states consistently - add revive button for dead participants - distinguish dead visual from inactive visual in DM view - Fix initiative drag/reorder behavior: - downward drag now changes order instead of no-op - paused combat can reorder across current turn pointer - turnOrderIds stay synced to participants order - Persist campaign collapse state in localStorage - Update death-save docs and encounter builder docs - Add/expand tests for: - death saves and HP transitions - dead/active/inactive behavior - NPC death-save behavior - player display visibility - drag reorder semantics - logging expectations Tests: - npm run test:all - app: 94 passed - shared: 158 passed, 5 skipped - server: 32 passed
74 lines
2.5 KiB
JavaScript
74 lines
2.5 KiB
JavaScript
// Test helper: in-memory mock storage. Records all writes (addDoc/updateDoc).
|
|
// Captures encounter patches + log entries so tests assert persistence.
|
|
function createMockStorage() {
|
|
const docs = new Map(); // path -> data
|
|
const calls = []; // ordered {fn, path, data}
|
|
|
|
return {
|
|
calls,
|
|
docs,
|
|
async getDoc(path) { return docs.has(path) ? { ...docs.get(path) } : null; },
|
|
async setDoc(path, data) {
|
|
calls.push({ fn: 'setDoc', path, data });
|
|
docs.set(path, { ...data });
|
|
},
|
|
async addDoc(path, data) {
|
|
calls.push({ fn: 'addDoc', path, data });
|
|
},
|
|
async updateDoc(path, patch) {
|
|
calls.push({ fn: 'updateDoc', path, data: patch });
|
|
if (!docs.has(path)) docs.set(path, {});
|
|
docs.set(path, { ...docs.get(path), ...patch });
|
|
},
|
|
async deleteDoc(path) {
|
|
calls.push({ fn: 'deleteDoc', path });
|
|
docs.delete(path);
|
|
},
|
|
async getCollection() { return []; },
|
|
async batchWrite(ops) {
|
|
for (const op of ops) {
|
|
if (op.type === 'delete') await this.deleteDoc(op.path);
|
|
else await this.updateDoc(op.path, op.data);
|
|
}
|
|
},
|
|
async undo() {},
|
|
// filters
|
|
updatesFor(prefix) { return calls.filter(c => c.fn === 'updateDoc' && c.path.startsWith(prefix)); },
|
|
logs() { return calls.filter(c => c.fn === 'addDoc').map(c => c.data); },
|
|
getLogs() { return calls.filter(c => c.fn === 'addDoc').map(c => c.data); },
|
|
};
|
|
}
|
|
|
|
// Standard ctx for tests. encPath + logPath + displayPath preset.
|
|
// Returns { storage, ctx } so tests destructure both in one call:
|
|
// const { storage, ctx } = mockCtx();
|
|
function mockCtx(storage) {
|
|
storage = storage || createMockStorage();
|
|
const ctx = {
|
|
storage,
|
|
encPath: 'encounters/e1',
|
|
logPath: 'logs',
|
|
displayPath: 'activeDisplay/status',
|
|
};
|
|
return { storage, ctx };
|
|
}
|
|
|
|
// Standard 2-participant ready encounter.
|
|
function readyEnc(opts = {}) {
|
|
return {
|
|
id: 'e1',
|
|
name: opts.name || 'TestEnc',
|
|
round: 0,
|
|
isStarted: false,
|
|
isPaused: false,
|
|
currentTurnParticipantId: null,
|
|
turnOrderIds: [],
|
|
participants: opts.participants || [
|
|
{ id: 'p1', name: 'Bob', type: 'character', initiative: 10, maxHp: 10, currentHp: 10, isActive: true, conditions: [], status: 'conscious', deathSaveSuccesses: 0, deathSaveFailures: 0 },
|
|
{ id: 'p2', name: 'Mob', type: 'monster', initiative: 5, maxHp: 10, currentHp: 10, isActive: true, conditions: [], status: 'conscious', deathSaveSuccesses: 0, deathSaveFailures: 0 },
|
|
],
|
|
};
|
|
}
|
|
|
|
module.exports = { createMockStorage, mockCtx, readyEnc };
|