158 lines
5.9 KiB
JavaScript
158 lines
5.9 KiB
JavaScript
// Character writeback on endEncounter: sync maxHp/ac to campaign players.
|
|||
|
|
const shared = require('@ttrpg/shared');
|
||
|
|
const { endEncounter } = shared;
|
||
|
|
|
||
|
|
function makeMockStorage() {
|
||
|
|
const docs = new Map();
|
||
|
|
const writes = [];
|
||
|
|
return {
|
||
|
|
getDoc: jest.fn(async (p) => docs.get(p) || null),
|
||
|
|
setDoc: jest.fn(async (p, d) => { docs.set(p, d); }),
|
||
|
|
updateDoc: jest.fn(async (p, patch) => {
|
||
|
|
writes.push({ path: p, patch });
|
||
|
|
const cur = docs.get(p) || {};
|
||
|
|
docs.set(p, { ...cur, ...patch });
|
||
|
|
}),
|
||
|
|
addDoc: jest.fn(async (p, data) => {
|
||
|
|
writes.push({ path: p, patch: data });
|
||
|
|
docs.set(p, data);
|
||
|
|
}),
|
||
|
|
deleteDoc: jest.fn(async () => {}),
|
||
|
|
getCollection: jest.fn(async () => []),
|
||
|
|
_docs: docs,
|
||
|
|
_writes: writes,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function makeEncounter(overrides = {}) {
|
||
|
|
return {
|
||
|
|
id: 'enc1',
|
||
|
|
name: 'Test',
|
||
|
|
campaignId: 'camp1',
|
||
|
|
isStarted: true,
|
||
|
|
isPaused: false,
|
||
|
|
currentTurnParticipantId: 'p1',
|
||
|
|
round: 3,
|
||
|
|
turnOrderIds: ['p1', 'p2'],
|
||
|
|
participants: [
|
||
|
|
{ id: 'p1', name: 'Fighter', type: 'character', originalCharacterId: 'char1', initiative: 15, maxHp: 28, currentHp: 20, ac: 18 },
|
||
|
|
{ id: 'p2', name: 'Goblin', type: 'monster', originalCharacterId: null, initiative: 12, maxHp: 7, currentHp: 7, ac: 13 },
|
||
|
|
{ id: 'p3', name: 'Cleric', type: 'character', originalCharacterId: 'char2', initiative: 10, maxHp: 22, currentHp: 22, ac: 16 },
|
||
|
|
],
|
||
|
|
...overrides,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('character writeback on endEncounter', () => {
|
||
|
|
test('no writeback when syncCharacters false/missing', async () => {
|
||
|
|
const storage = makeMockStorage();
|
||
|
|
storage._docs.set('campaigns/camp1', {
|
||
|
|
id: 'camp1', name: 'Camp', players: [
|
||
|
|
{ id: 'char1', name: 'Fighter', defaultMaxHp: 30, defaultAc: 17 },
|
||
|
|
],
|
||
|
|
});
|
||
|
|
const enc = makeEncounter();
|
||
|
|
const ctx = {
|
||
|
|
storage,
|
||
|
|
encounterPath: 'campaigns/camp1/encounters/enc1',
|
||
|
|
logPath: 'logs/log1',
|
||
|
|
logCollection: 'logs',
|
||
|
|
displayPath: 'activeDisplay/status',
|
||
|
|
};
|
||
|
|
await endEncounter(enc, ctx);
|
||
|
|
// no campaign write
|
||
|
|
expect(storage._writes.find(w => w.path === 'campaigns/camp1')).toBeUndefined();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('writeback updates char maxHp + ac + currentHp when syncCharacters true', async () => {
|
||
|
|
const storage = makeMockStorage();
|
||
|
|
const players = [
|
||
|
|
{ id: 'char1', name: 'Fighter', defaultMaxHp: 30, defaultAc: 17, defaultCurrentHp: 30 },
|
||
|
|
{ id: 'char2', name: 'Cleric', defaultMaxHp: 20, defaultAc: 14, defaultCurrentHp: 20 },
|
||
|
|
];
|
||
|
|
storage._docs.set('campaigns/camp1', { id: 'camp1', name: 'Camp', players, syncCharacters: true });
|
||
|
|
const enc = makeEncounter();
|
||
|
|
const ctx = {
|
||
|
|
storage,
|
||
|
|
encounterPath: 'campaigns/camp1/encounters/enc1',
|
||
|
|
logPath: 'logs/log1',
|
||
|
|
logCollection: 'logs',
|
||
|
|
displayPath: 'activeDisplay/status',
|
||
|
|
};
|
||
|
|
await endEncounter(enc, ctx);
|
||
|
|
const campWrite = storage._writes.find(w => w.path === 'campaigns/camp1');
|
||
|
|
expect(campWrite).toBeDefined();
|
||
|
|
const updated = campWrite.patch.players;
|
||
|
|
const fighter = updated.find(p => p.id === 'char1');
|
||
|
|
expect(fighter.defaultMaxHp).toBe(28);
|
||
|
|
expect(fighter.defaultAc).toBe(18);
|
||
|
|
expect(fighter.defaultCurrentHp).toBe(20);
|
||
|
|
const cleric = updated.find(p => p.id === 'char2');
|
||
|
|
expect(cleric.defaultMaxHp).toBe(22);
|
||
|
|
expect(cleric.defaultAc).toBe(16);
|
||
|
|
expect(cleric.defaultCurrentHp).toBe(22);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('writeback skips monsters (no originalCharacterId)', async () => {
|
||
|
|
const storage = makeMockStorage();
|
||
|
|
const players = [{ id: 'char1', name: 'Fighter', defaultMaxHp: 30, defaultAc: 17 }];
|
||
|
|
storage._docs.set('campaigns/camp1', { id: 'camp1', name: 'Camp', players, syncCharacters: true });
|
||
|
|
const enc = makeEncounter();
|
||
|
|
const ctx = {
|
||
|
|
storage,
|
||
|
|
encounterPath: 'campaigns/camp1/encounters/enc1',
|
||
|
|
logPath: 'logs/log1',
|
||
|
|
logCollection: 'logs',
|
||
|
|
displayPath: 'activeDisplay/status',
|
||
|
|
};
|
||
|
|
await endEncounter(enc, ctx);
|
||
|
|
const campWrite = storage._writes.find(w => w.path === 'campaigns/camp1');
|
||
|
|
// players array still 1 entry, goblin not added
|
||
|
|
expect(campWrite.patch.players.length).toBe(1);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('undo payload includes old char values for restore', async () => {
|
||
|
|
const storage = makeMockStorage();
|
||
|
|
const players = [
|
||
|
|
{ id: 'char1', name: 'Fighter', defaultMaxHp: 30, defaultAc: 17, defaultCurrentHp: 30 },
|
||
|
|
{ id: 'char2', name: 'Cleric', defaultMaxHp: 20, defaultAc: 14, defaultCurrentHp: 20 },
|
||
|
|
];
|
||
|
|
storage._docs.set('campaigns/camp1', { id: 'camp1', name: 'Camp', players, syncCharacters: true });
|
||
|
|
storage._docs.set('logs/log1', null);
|
||
|
|
const enc = makeEncounter();
|
||
|
|
const ctx = {
|
||
|
|
storage,
|
||
|
|
encounterPath: 'campaigns/camp1/encounters/enc1',
|
||
|
|
logPath: 'logs/log1',
|
||
|
|
logCollection: 'logs',
|
||
|
|
displayPath: 'activeDisplay/status',
|
||
|
|
};
|
||
|
|
await endEncounter(enc, ctx);
|
||
|
|
const logWrite = storage._writes.find(w => w.path === 'logs/log1');
|
||
|
|
expect(logWrite).toBeDefined();
|
||
|
|
const log = logWrite.patch;
|
||
|
|
expect(log.undo.characterWriteback).toBeDefined();
|
||
|
|
expect(log.undo.characterWriteback).toHaveLength(2);
|
||
|
|
const fighterOld = log.undo.characterWriteback.find(c => c.id === 'char1');
|
||
|
|
expect(fighterOld.defaultMaxHp).toBe(30);
|
||
|
|
expect(fighterOld.defaultAc).toBe(17);
|
||
|
|
expect(fighterOld.defaultCurrentHp).toBe(30);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('writeback no-op if character missing from campaign', async () => {
|
||
|
|
const storage = makeMockStorage();
|
||
|
|
storage._docs.set('campaigns/camp1', { id: 'camp1', name: 'Camp', players: [], syncCharacters: true });
|
||
|
|
const enc = makeEncounter();
|
||
|
|
const ctx = {
|
||
|
|
storage,
|
||
|
|
encounterPath: 'campaigns/camp1/encounters/enc1',
|
||
|
|
logPath: 'logs/log1',
|
||
|
|
logCollection: 'logs',
|
||
|
|
displayPath: 'activeDisplay/status',
|
||
|
|
};
|
||
|
|
await endEncounter(enc, ctx);
|
||
|
|
// no writeback (no matching chars = no change)
|
||
|
|
expect(storage._writes.find(w => w.path === 'campaigns/camp1')).toBeUndefined();
|
||
|
|
});
|
||
|
|
});
|