Files
DndGamePlayer/app/main/sceneTraps/sceneTrapsStore.ts
T
Ivan Fontosh 02d73ddf81 feat: scene traps, material legends, and scene editor window
Add trap overlays with session state, material legend editor/panel, and a dedicated scene editor window wired through IPC and project persistence.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 16:29:43 +08:00

135 lines
4.1 KiB
TypeScript

import {
defaultTrapRuntime,
type SceneTrapsEvent,
type SceneTrapsState,
type SceneTrapRuntime,
} from '../../shared/types';
function emptyState(): SceneTrapsState {
return {
revision: 1,
cacheKey: null,
byId: {},
lastActivation: null,
};
}
function cloneById(byId: Record<string, SceneTrapRuntime>): Record<string, SceneTrapRuntime> {
const out: Record<string, SceneTrapRuntime> = {};
for (const [k, v] of Object.entries(byId)) {
out[k] = { ...v };
}
return out;
}
export class SceneTrapsStore {
private state: SceneTrapsState = emptyState();
/** Кэш runtime по ключу сцены/ноды на время сессии показа. */
private cache = new Map<string, Record<string, SceneTrapRuntime>>();
private currentKey: string | null = null;
private activationToken = 0;
getState(): SceneTrapsState {
return {
...this.state,
byId: cloneById(this.state.byId),
lastActivation: this.state.lastActivation ? { ...this.state.lastActivation } : null,
};
}
resetSession(): void {
this.cache.clear();
this.currentKey = null;
this.state = emptyState();
}
/**
* Переключение сцены: сохраняем runtime в кэш и подгружаем/инициализируем для новых trapIds.
*/
switchScene(cacheKey: string | null, trapIds: readonly string[]): SceneTrapsState {
if (this.currentKey !== null) {
this.cache.set(this.currentKey, cloneById(this.state.byId));
}
this.currentKey = cacheKey;
const cached = cacheKey ? this.cache.get(cacheKey) : undefined;
const byId: Record<string, SceneTrapRuntime> = {};
for (const id of trapIds) {
byId[id] = cached?.[id] ? { ...cached[id]! } : defaultTrapRuntime();
}
if (cacheKey) {
this.cache.set(cacheKey, cloneById(byId));
}
this.state = {
revision: this.state.revision + 1,
cacheKey,
byId,
lastActivation: null,
};
return this.getState();
}
dispatch(event: SceneTrapsEvent): SceneTrapsState {
switch (event.kind) {
case 'syncTrapIds': {
const byId: Record<string, SceneTrapRuntime> = {};
for (const id of event.trapIds) {
byId[id] = this.state.byId[id] ? { ...this.state.byId[id]! } : defaultTrapRuntime();
}
if (this.currentKey) this.cache.set(this.currentKey, cloneById(byId));
this.state = {
...this.state,
revision: this.state.revision + 1,
byId,
};
return this.getState();
}
case 'reveal': {
const cur = this.state.byId[event.trapId];
if (!cur) return this.getState();
const byId = cloneById(this.state.byId);
byId[event.trapId] = { ...cur, revealed: true };
if (this.currentKey) this.cache.set(this.currentKey, cloneById(byId));
this.state = {
...this.state,
revision: this.state.revision + 1,
byId,
};
return this.getState();
}
case 'activate': {
const cur = this.state.byId[event.trapId];
if (!cur) return this.getState();
const byId = cloneById(this.state.byId);
byId[event.trapId] = { status: 'active', revealed: true };
this.activationToken += 1;
if (this.currentKey) this.cache.set(this.currentKey, cloneById(byId));
this.state = {
...this.state,
revision: this.state.revision + 1,
byId,
lastActivation: { trapId: event.trapId, token: this.activationToken },
};
return this.getState();
}
case 'disarm': {
const cur = this.state.byId[event.trapId];
if (!cur) return this.getState();
const byId = cloneById(this.state.byId);
byId[event.trapId] = { status: 'disarmed', revealed: true };
if (this.currentKey) this.cache.set(this.currentKey, cloneById(byId));
this.state = {
...this.state,
revision: this.state.revision + 1,
byId,
};
return this.getState();
}
default: {
const _x: never = event;
void _x;
return this.getState();
}
}
}
}