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,104 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user