feat: save games and edit scene darkness
Persist show state to local saves; author map darkness in scene editor with bake-to-scene and help updates. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
|
||||
import { EffectsStore } from './effectsStore';
|
||||
import { GameSavesStore } from '../gameSave/gameSavesStore';
|
||||
import { canonicalGameSave, emptyGameSaveBody, freezeTokenPathPlayback } from '../../shared/gameSave/buildSnapshot';
|
||||
import type { EffectInstance } from '../../shared/types';
|
||||
import type { TokenPath } from '../../shared/types/tokenPath';
|
||||
import type { TokenPathPlaybackEntry } from '../../shared/types/tokenPathSession';
|
||||
|
||||
function fog(): EffectInstance {
|
||||
return {
|
||||
id: 'fog1',
|
||||
type: 'fog',
|
||||
seed: 1,
|
||||
createdAtMs: 1,
|
||||
points: [{ x: 0.2, y: 0.2, tMs: 1 }],
|
||||
radiusN: 0.1,
|
||||
opacity: 0.4,
|
||||
lifetimeMs: null,
|
||||
};
|
||||
}
|
||||
|
||||
function bolt(): EffectInstance {
|
||||
return {
|
||||
id: 'lt1',
|
||||
type: 'lightning',
|
||||
seed: 2,
|
||||
createdAtMs: 1,
|
||||
start: { x: 0, y: 0 },
|
||||
end: { x: 1, y: 1 },
|
||||
widthN: 0.02,
|
||||
intensity: 1,
|
||||
lifetimeMs: 800,
|
||||
};
|
||||
}
|
||||
|
||||
void test('effects stay on the card and action effects do not', () => {
|
||||
const store = new EffectsStore();
|
||||
store.switchScene('card-a');
|
||||
store.dispatch({ kind: 'instance.add', instance: fog() });
|
||||
store.dispatch({ kind: 'instance.add', instance: bolt() });
|
||||
store.switchScene('card-b');
|
||||
assert.equal(store.getState().instances.length, 0);
|
||||
store.switchScene('card-a');
|
||||
assert.deepEqual(
|
||||
store.getState().instances.map((i) => i.type),
|
||||
['fog'],
|
||||
);
|
||||
store.dispatch({ kind: 'instances.clear' });
|
||||
store.switchScene('card-b');
|
||||
store.switchScene('card-a');
|
||||
assert.equal(store.getState().instances.length, 0);
|
||||
});
|
||||
|
||||
void test('freezeTokenPathPlayback parks a moving token', () => {
|
||||
const pathLine: TokenPath = {
|
||||
points: [
|
||||
{ nx: 0, ny: 0.5 },
|
||||
{ nx: 1, ny: 0.5 },
|
||||
],
|
||||
closed: false,
|
||||
loopMode: 'once',
|
||||
durationSec: 2,
|
||||
startMode: 'onEnter',
|
||||
delaySec: 0,
|
||||
facingMode: 'tangentSmooth',
|
||||
fixedRotationDeg: 0,
|
||||
};
|
||||
const entry: TokenPathPlaybackEntry = {
|
||||
kind: 'token',
|
||||
placementId: 't1',
|
||||
phase: 'moving',
|
||||
segmentStartedAtMs: 0,
|
||||
baseDist: 0,
|
||||
direction: 1,
|
||||
rejoinDist: null,
|
||||
durationSec: 2,
|
||||
pathLength: 1,
|
||||
};
|
||||
const frozen = freezeTokenPathPlayback({ 'token:t1': entry }, () => pathLine, 1000);
|
||||
const next = frozen['token:t1'];
|
||||
assert.ok(next);
|
||||
assert.equal(next.phase, 'stopped');
|
||||
assert.ok(next.baseDist > 0.4 && next.baseDist < 0.6);
|
||||
});
|
||||
|
||||
void test('game save file keeps id on overwrite and lists one campaign', async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'dnd-saves-'));
|
||||
const store = new GameSavesStore(root);
|
||||
const body = emptyGameSaveBody('p_one');
|
||||
const first = await store.write('p_one', body, { id: null, name: 'Вечер' });
|
||||
const again = await store.write('p_one', body, { id: first.id, name: 'Вечер 2' });
|
||||
assert.equal(again.id, first.id);
|
||||
assert.equal(again.name, 'Вечер 2');
|
||||
const list = await store.list('p_one');
|
||||
assert.equal(list.length, 1);
|
||||
assert.equal(list[0]?.name, 'Вечер 2');
|
||||
const other = emptyGameSaveBody('p_two');
|
||||
await store.write('p_two', other, { id: null, name: 'Другая' });
|
||||
assert.equal((await store.list('p_one')).length, 1);
|
||||
const loaded = await store.read('p_one', first.id);
|
||||
const { id: _id, name: _name, savedAt: _savedAt, ...rest } = loaded;
|
||||
assert.equal(canonicalGameSave(rest), canonicalGameSave(body));
|
||||
});
|
||||
Reference in New Issue
Block a user