Files
DndGamePlayer/app/main/npcs/npcsOverlayStore.ts
T
Ivan Fontosh 37ba855faf feat(npcs): add campaign NPCs with relation graph and session overlay
Add a dedicated NPC editor window, directed relations, control/presentation avatar overlay, and ru/en help for the new section.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 13:13:58 +08:00

110 lines
2.8 KiB
TypeScript

import {
clampNpcsLayout,
DEFAULT_NPCS_OVERLAY_LAYOUT,
type NpcId,
type NpcsOverlayEvent,
type NpcsOverlayLayout,
type NpcsOverlayState,
type NpcsZoomTool,
zoomNpcsLayoutAt,
} from '../../shared/types';
function emptyState(): NpcsOverlayState {
return {
revision: 1,
activeNpcId: null,
layout: { ...DEFAULT_NPCS_OVERLAY_LAYOUT },
zoomTool: null,
};
}
export class NpcsOverlayStore {
private state: NpcsOverlayState = emptyState();
getState(): NpcsOverlayState {
return this.state;
}
clear(): NpcsOverlayState {
if (this.state.activeNpcId === null && this.state.zoomTool === null) {
return this.state;
}
this.state = {
revision: this.state.revision + 1,
activeNpcId: null,
layout: { ...DEFAULT_NPCS_OVERLAY_LAYOUT },
zoomTool: null,
};
return this.state;
}
dispatch(event: NpcsOverlayEvent): NpcsOverlayState {
switch (event.kind) {
case 'hide':
return this.clear();
case 'show':
this.state = {
revision: this.state.revision + 1,
activeNpcId: event.npcId,
layout: { ...DEFAULT_NPCS_OVERLAY_LAYOUT },
zoomTool: this.state.zoomTool,
};
return this.state;
case 'toggle': {
if (this.state.activeNpcId === event.npcId) {
return this.clear();
}
this.state = {
revision: this.state.revision + 1,
activeNpcId: event.npcId,
layout: { ...DEFAULT_NPCS_OVERLAY_LAYOUT },
zoomTool: this.state.zoomTool,
};
return this.state;
}
case 'layout.set': {
if (this.state.activeNpcId === null) return this.state;
this.state = {
...this.state,
revision: this.state.revision + 1,
layout: clampNpcsLayout(event.layout),
};
return this.state;
}
case 'zoomTool.set': {
const tool: NpcsZoomTool = event.tool;
this.state = {
...this.state,
revision: this.state.revision + 1,
zoomTool: tool,
};
return this.state;
}
case 'zoomAt': {
if (this.state.activeNpcId === null || !this.state.zoomTool) return this.state;
const factor = this.state.zoomTool === 'zoomIn' ? 1.25 : 1 / 1.25;
const layout: NpcsOverlayLayout = zoomNpcsLayoutAt(
this.state.layout,
event.nx,
event.ny,
factor,
);
this.state = {
...this.state,
revision: this.state.revision + 1,
layout,
};
return this.state;
}
default:
return this.state;
}
}
ensureNpcStillExists(npcIds: ReadonlySet<NpcId>): NpcsOverlayState {
const active = this.state.activeNpcId;
if (active === null || npcIds.has(active)) return this.state;
return this.clear();
}
}