877fbaa8e7
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>
105 lines
3.8 KiB
TypeScript
105 lines
3.8 KiB
TypeScript
import { computePathPlaybackSample } from '../types/tokenPathPlayback';
|
|
import type { GameSaveBody } from '../types/gameSave';
|
|
import { GAME_SAVE_SCHEMA_VERSION } from '../types/gameSave';
|
|
import type { Project } from '../types/domain';
|
|
import type { TokenPath } from '../types/tokenPath';
|
|
import type { TokenPathPlaybackEntry } from '../types/tokenPathSession';
|
|
|
|
export function defaultGameSaveName(now = new Date()): string {
|
|
const p = (n: number) => String(n).padStart(2, '0');
|
|
return `${p(now.getDate())}.${p(now.getMonth() + 1)}.${now.getFullYear()} ${p(now.getHours())}:${p(now.getMinutes())}`;
|
|
}
|
|
|
|
export function canonicalGameSave(body: GameSaveBody): string {
|
|
return JSON.stringify(sortValue(body));
|
|
}
|
|
|
|
function sortValue(value: unknown): unknown {
|
|
if (Array.isArray(value)) return value.map(sortValue);
|
|
if (value && typeof value === 'object') {
|
|
const src = value as Record<string, unknown>;
|
|
const out: Record<string, unknown> = {};
|
|
for (const key of Object.keys(src).sort()) out[key] = sortValue(src[key]);
|
|
return out;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
export function pathForPlacement(project: Project, kind: 'token' | 'npcToken', placementId: string): TokenPath | null {
|
|
for (const scene of Object.values(project.scenes)) {
|
|
const list = kind === 'token' ? scene.tokens : scene.npcTokens;
|
|
const found = (list ?? []).find((token) => String(token.id) === placementId);
|
|
if (found?.path && found.path.points.length >= 2) return found.path;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** Движение и задержка становятся паузой в текущей точке пути. */
|
|
export function freezeTokenPathPlayback(
|
|
playback: Record<string, TokenPathPlaybackEntry>,
|
|
pathFor: (entry: TokenPathPlaybackEntry) => TokenPath | null,
|
|
nowMs: number,
|
|
): Record<string, TokenPathPlaybackEntry> {
|
|
const out: Record<string, TokenPathPlaybackEntry> = {};
|
|
for (const [key, entry] of Object.entries(playback)) {
|
|
out[key] = freezeEntry(entry, pathFor(entry), nowMs);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function freezeEntry(entry: TokenPathPlaybackEntry, path: TokenPath | null, nowMs: number): TokenPathPlaybackEntry {
|
|
if (entry.phase === 'stopped' || entry.phase === 'done' || !path) {
|
|
const phase = entry.phase === 'moving' || entry.phase === 'delay' ? 'stopped' : entry.phase;
|
|
return { ...entry, phase, rejoinDist: null, segmentStartedAtMs: 0 };
|
|
}
|
|
const sample = computePathPlaybackSample({ path, entry, nowMs });
|
|
if (!sample) {
|
|
return { ...entry, phase: 'stopped', rejoinDist: null, segmentStartedAtMs: 0 };
|
|
}
|
|
const phase = sample.markDone || sample.phase === 'done' ? 'done' : 'stopped';
|
|
return {
|
|
...entry,
|
|
phase,
|
|
baseDist: sample.sample.dist,
|
|
rejoinDist: null,
|
|
segmentStartedAtMs: 0,
|
|
direction: 1,
|
|
};
|
|
}
|
|
|
|
export function emptyGameSaveBody(projectId: string): GameSaveBody {
|
|
return {
|
|
schemaVersion: GAME_SAVE_SCHEMA_VERSION,
|
|
projectId,
|
|
graphNodeId: null,
|
|
sceneId: null,
|
|
darknessByCard: {},
|
|
markersByCard: {},
|
|
trapsByCard: {},
|
|
effectsByCard: {},
|
|
sceneTokensByPlacementId: {},
|
|
npcTokens: { byPlacementId: {}, scale: 1, spawned: {} },
|
|
tokenPaths: { presentationVisible: {}, playback: {} },
|
|
timer: {
|
|
durationMs: 0,
|
|
remainingMs: 0,
|
|
visibleToPlayers: false,
|
|
layout: { cx: 0, cy: 0, scale: 1, rotationDeg: 0 },
|
|
},
|
|
playerTokens: { selectedPlayerIds: [], visible: false, byPlayerId: {} },
|
|
camera: { scale: 1, ox: 0.5, oy: 0.5 },
|
|
materials: { activeMaterialIds: [], layouts: {}, legendLayouts: {}, focusMaterialId: null },
|
|
npcAvatars: { activeNpcIds: [], layouts: {}, focusNpcId: null },
|
|
initiative: {
|
|
enabled: false,
|
|
entries: {},
|
|
order: [],
|
|
reserve: [],
|
|
activeId: null,
|
|
conditions: {},
|
|
playersVisible: false,
|
|
},
|
|
tokenGridSnap: false,
|
|
};
|
|
}
|