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:
Ivan Fontosh
2026-10-06 11:41:42 +08:00
parent f6c301a12b
commit 877fbaa8e7
43 changed files with 2086 additions and 72 deletions
+76 -1
View File
@@ -1,7 +1,13 @@
import crypto from 'node:crypto';
import { applyFieldEffectEraserStroke } from '../../shared/fieldEffectEraser';
import type { EffectsEvent, EffectsState, EffectToolState } from '../../shared/types';
import {
isPersistentFieldEffect,
type EffectInstance,
type EffectsEvent,
type EffectsState,
type EffectToolState,
} from '../../shared/types';
function nowMs(): number {
return Date.now();
@@ -23,6 +29,9 @@ export class EffectsStore {
tool: defaultTool(),
instances: [],
};
/** Постоянные эффекты по карточке (graphNodeId или sceneId) на время показа. */
private cache = new Map<string, EffectInstance[]>();
private currentKey: string | null = null;
getState(): EffectsState {
// Всегда обновляем serverNowMs при чтении — это наш "таймкод" для рендереров.
@@ -36,13 +45,70 @@ export class EffectsStore {
serverNowMs: nowMs(),
instances: [],
};
this.commitCurrent();
return this.state;
}
/** Новый показ: и текущая карточка, и кэш остальных. */
resetSession(): EffectsState {
this.cache.clear();
this.currentKey = null;
this.state = {
revision: this.state.revision + 1,
serverNowMs: nowMs(),
tool: this.state.tool,
instances: [],
};
return this.getState();
}
/**
* Смена карточки: туман/огонь/дождь/вода/лёд/пятно остаются на ушедшей
* и поднимаются на новой. Разовые эффекты действий сбрасываются.
*/
switchScene(cacheKey: string | null): EffectsState {
this.commitCurrent();
this.currentKey = cacheKey;
const instances = cacheKey ? [...(this.cache.get(cacheKey) ?? [])] : [];
this.state = {
...this.state,
revision: this.state.revision + 1,
serverNowMs: nowMs(),
instances,
};
return this.getState();
}
exportByCard(): Record<string, EffectInstance[]> {
this.commitCurrent();
const out: Record<string, EffectInstance[]> = {};
for (const [key, instances] of this.cache) {
const kept = instances.filter((i) => isPersistentFieldEffect(i.type));
if (kept.length > 0) out[key] = kept.map((i) => ({ ...i }));
}
return out;
}
importByCard(byCard: Record<string, EffectInstance[]>): void {
this.cache.clear();
this.currentKey = null;
this.state = {
...this.state,
revision: this.state.revision + 1,
serverNowMs: nowMs(),
instances: [],
};
for (const [key, instances] of Object.entries(byCard)) {
const kept = instances.filter((i) => isPersistentFieldEffect(i.type));
if (kept.length > 0) this.cache.set(key, kept.map((i) => ({ ...i })));
}
}
dispatch(event: EffectsEvent): EffectsState {
const s = this.state;
const next: EffectsState = applyEvent(s, event, (prefix) => this.makeId(prefix));
this.state = next;
this.commitCurrent();
return next;
}
@@ -78,9 +144,18 @@ export class EffectsStore {
serverNowMs: now,
instances: kept,
};
this.commitCurrent();
return true;
}
private commitCurrent(): void {
if (this.currentKey === null) return;
this.cache.set(
this.currentKey,
this.state.instances.filter((i) => isPersistentFieldEffect(i.type)).map((i) => ({ ...i })),
);
}
makeId(prefix: string): string {
return `${prefix}_${crypto.randomBytes(6).toString('hex')}_${String(nowMs())}`;
}