1829191410
Toggle NPCs independently from the characters window, keep one shared dim/close-all, and scroll descriptions for all selected characters. Co-authored-by: Cursor <cursoragent@cursor.com>
178 lines
5.3 KiB
TypeScript
178 lines
5.3 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,
|
|
activeNpcIds: [],
|
|
layouts: {},
|
|
focusNpcId: null,
|
|
zoomTool: null,
|
|
};
|
|
}
|
|
|
|
function layoutFor(state: NpcsOverlayState, npcId: NpcId): NpcsOverlayLayout {
|
|
return state.layouts[npcId] ?? { ...DEFAULT_NPCS_OVERLAY_LAYOUT };
|
|
}
|
|
|
|
export class NpcsOverlayStore {
|
|
private state: NpcsOverlayState = emptyState();
|
|
|
|
getState(): NpcsOverlayState {
|
|
return this.state;
|
|
}
|
|
|
|
clear(): NpcsOverlayState {
|
|
if (this.state.activeNpcIds.length === 0 && this.state.zoomTool === null) {
|
|
return this.state;
|
|
}
|
|
this.state = {
|
|
revision: this.state.revision + 1,
|
|
activeNpcIds: [],
|
|
layouts: {},
|
|
focusNpcId: null,
|
|
zoomTool: null,
|
|
};
|
|
return this.state;
|
|
}
|
|
|
|
dispatch(event: NpcsOverlayEvent): NpcsOverlayState {
|
|
switch (event.kind) {
|
|
case 'hide':
|
|
return this.clear();
|
|
case 'show': {
|
|
if (this.state.activeNpcIds.includes(event.npcId)) {
|
|
this.state = {
|
|
...this.state,
|
|
revision: this.state.revision + 1,
|
|
focusNpcId: event.npcId,
|
|
};
|
|
return this.state;
|
|
}
|
|
this.state = {
|
|
revision: this.state.revision + 1,
|
|
activeNpcIds: [...this.state.activeNpcIds, event.npcId],
|
|
layouts: {
|
|
...this.state.layouts,
|
|
[event.npcId]: { ...DEFAULT_NPCS_OVERLAY_LAYOUT },
|
|
},
|
|
focusNpcId: event.npcId,
|
|
zoomTool: this.state.zoomTool,
|
|
};
|
|
return this.state;
|
|
}
|
|
case 'toggle': {
|
|
if (this.state.activeNpcIds.includes(event.npcId)) {
|
|
const activeNpcIds = this.state.activeNpcIds.filter((id) => id !== event.npcId);
|
|
const layouts = { ...this.state.layouts };
|
|
delete layouts[event.npcId];
|
|
const focusNpcId =
|
|
this.state.focusNpcId === event.npcId
|
|
? (activeNpcIds[activeNpcIds.length - 1] ?? null)
|
|
: this.state.focusNpcId;
|
|
this.state = {
|
|
revision: this.state.revision + 1,
|
|
activeNpcIds,
|
|
layouts,
|
|
focusNpcId,
|
|
zoomTool: activeNpcIds.length === 0 ? null : this.state.zoomTool,
|
|
};
|
|
return this.state;
|
|
}
|
|
this.state = {
|
|
revision: this.state.revision + 1,
|
|
activeNpcIds: [...this.state.activeNpcIds, event.npcId],
|
|
layouts: {
|
|
...this.state.layouts,
|
|
[event.npcId]: { ...DEFAULT_NPCS_OVERLAY_LAYOUT },
|
|
},
|
|
focusNpcId: event.npcId,
|
|
zoomTool: this.state.zoomTool,
|
|
};
|
|
return this.state;
|
|
}
|
|
case 'layout.set': {
|
|
if (!this.state.activeNpcIds.includes(event.npcId)) return this.state;
|
|
this.state = {
|
|
...this.state,
|
|
revision: this.state.revision + 1,
|
|
focusNpcId: event.npcId,
|
|
layouts: {
|
|
...this.state.layouts,
|
|
[event.npcId]: 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.activeNpcIds.length === 0 || !this.state.zoomTool) return this.state;
|
|
const targetId =
|
|
(event.npcId && this.state.activeNpcIds.includes(event.npcId) ? event.npcId : null) ??
|
|
this.state.focusNpcId ??
|
|
this.state.activeNpcIds[this.state.activeNpcIds.length - 1] ??
|
|
null;
|
|
if (!targetId) return this.state;
|
|
const factor = this.state.zoomTool === 'zoomIn' ? 1.25 : 1 / 1.25;
|
|
const layout: NpcsOverlayLayout = zoomNpcsLayoutAt(
|
|
layoutFor(this.state, targetId),
|
|
event.nx,
|
|
event.ny,
|
|
factor,
|
|
);
|
|
this.state = {
|
|
...this.state,
|
|
revision: this.state.revision + 1,
|
|
focusNpcId: targetId,
|
|
layouts: {
|
|
...this.state.layouts,
|
|
[targetId]: layout,
|
|
},
|
|
};
|
|
return this.state;
|
|
}
|
|
default:
|
|
return this.state;
|
|
}
|
|
}
|
|
|
|
ensureNpcStillExists(npcIds: ReadonlySet<NpcId>): NpcsOverlayState {
|
|
const activeNpcIds = this.state.activeNpcIds.filter((id) => npcIds.has(id));
|
|
if (activeNpcIds.length === this.state.activeNpcIds.length) return this.state;
|
|
if (activeNpcIds.length === 0) return this.clear();
|
|
const layouts: Record<string, NpcsOverlayLayout> = {};
|
|
for (const id of activeNpcIds) {
|
|
const layout = this.state.layouts[id];
|
|
if (layout) layouts[id] = layout;
|
|
}
|
|
const focusNpcId =
|
|
this.state.focusNpcId && activeNpcIds.includes(this.state.focusNpcId)
|
|
? this.state.focusNpcId
|
|
: (activeNpcIds[activeNpcIds.length - 1] ?? null);
|
|
this.state = {
|
|
revision: this.state.revision + 1,
|
|
activeNpcIds,
|
|
layouts,
|
|
focusNpcId,
|
|
zoomTool: this.state.zoomTool,
|
|
};
|
|
return this.state;
|
|
}
|
|
}
|