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)); });