feat(npcs): inspect cards, Show overlay, and optional external link
Clicking an NPC tile opens description cards (multi-select accordion); Show puts avatars on screen. Optional URL opens a webview tab. Avatar pan in the editor commits on pointer up. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -16,6 +16,8 @@ function emptyState(): NpcsOverlayState {
|
||||
layouts: {},
|
||||
focusNpcId: null,
|
||||
zoomTool: null,
|
||||
inspectNpcIds: [],
|
||||
inspectFocusNpcId: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,6 +25,15 @@ function layoutFor(state: NpcsOverlayState, npcId: NpcId): NpcsOverlayLayout {
|
||||
return state.layouts[npcId] ?? { ...DEFAULT_NPCS_OVERLAY_LAYOUT };
|
||||
}
|
||||
|
||||
function addInspect(ids: NpcId[], npcId: NpcId): NpcId[] {
|
||||
if (ids.includes(npcId)) return ids;
|
||||
return [...ids, npcId];
|
||||
}
|
||||
|
||||
function removeInspect(ids: NpcId[], npcId: NpcId): NpcId[] {
|
||||
return ids.filter((id) => id !== npcId);
|
||||
}
|
||||
|
||||
export class NpcsOverlayStore {
|
||||
private state: NpcsOverlayState = emptyState();
|
||||
|
||||
@@ -30,11 +41,29 @@ export class NpcsOverlayStore {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
/** Полный сброс (закрытие проекта / конец сессии). */
|
||||
clear(): NpcsOverlayState {
|
||||
const s = this.state;
|
||||
if (
|
||||
s.activeNpcIds.length === 0 &&
|
||||
s.zoomTool === null &&
|
||||
s.inspectNpcIds.length === 0 &&
|
||||
s.inspectFocusNpcId === null
|
||||
) {
|
||||
return s;
|
||||
}
|
||||
this.state = emptyState();
|
||||
this.state.revision = s.revision + 1;
|
||||
return this.state;
|
||||
}
|
||||
|
||||
/** Снять аватары с экрана, карточки описания оставить. */
|
||||
hideAvatars(): NpcsOverlayState {
|
||||
if (this.state.activeNpcIds.length === 0 && this.state.zoomTool === null) {
|
||||
return this.state;
|
||||
}
|
||||
this.state = {
|
||||
...this.state,
|
||||
revision: this.state.revision + 1,
|
||||
activeNpcIds: [],
|
||||
layouts: {},
|
||||
@@ -47,7 +76,7 @@ export class NpcsOverlayStore {
|
||||
dispatch(event: NpcsOverlayEvent): NpcsOverlayState {
|
||||
switch (event.kind) {
|
||||
case 'hide':
|
||||
return this.clear();
|
||||
return this.hideAvatars();
|
||||
case 'show': {
|
||||
if (this.state.activeNpcIds.includes(event.npcId)) {
|
||||
this.state = {
|
||||
@@ -58,6 +87,7 @@ export class NpcsOverlayStore {
|
||||
return this.state;
|
||||
}
|
||||
this.state = {
|
||||
...this.state,
|
||||
revision: this.state.revision + 1,
|
||||
activeNpcIds: [...this.state.activeNpcIds, event.npcId],
|
||||
layouts: {
|
||||
@@ -65,7 +95,6 @@ export class NpcsOverlayStore {
|
||||
[event.npcId]: { ...DEFAULT_NPCS_OVERLAY_LAYOUT },
|
||||
},
|
||||
focusNpcId: event.npcId,
|
||||
zoomTool: this.state.zoomTool,
|
||||
};
|
||||
return this.state;
|
||||
}
|
||||
@@ -79,6 +108,7 @@ export class NpcsOverlayStore {
|
||||
? (activeNpcIds[activeNpcIds.length - 1] ?? null)
|
||||
: this.state.focusNpcId;
|
||||
this.state = {
|
||||
...this.state,
|
||||
revision: this.state.revision + 1,
|
||||
activeNpcIds,
|
||||
layouts,
|
||||
@@ -88,6 +118,7 @@ export class NpcsOverlayStore {
|
||||
return this.state;
|
||||
}
|
||||
this.state = {
|
||||
...this.state,
|
||||
revision: this.state.revision + 1,
|
||||
activeNpcIds: [...this.state.activeNpcIds, event.npcId],
|
||||
layouts: {
|
||||
@@ -95,7 +126,38 @@ export class NpcsOverlayStore {
|
||||
[event.npcId]: { ...DEFAULT_NPCS_OVERLAY_LAYOUT },
|
||||
},
|
||||
focusNpcId: event.npcId,
|
||||
zoomTool: this.state.zoomTool,
|
||||
};
|
||||
return this.state;
|
||||
}
|
||||
case 'inspect.toggle': {
|
||||
if (this.state.inspectNpcIds.includes(event.npcId)) {
|
||||
const inspectNpcIds = removeInspect(this.state.inspectNpcIds, event.npcId);
|
||||
const inspectFocusNpcId =
|
||||
this.state.inspectFocusNpcId === event.npcId
|
||||
? (inspectNpcIds[inspectNpcIds.length - 1] ?? null)
|
||||
: this.state.inspectFocusNpcId;
|
||||
this.state = {
|
||||
...this.state,
|
||||
revision: this.state.revision + 1,
|
||||
inspectNpcIds,
|
||||
inspectFocusNpcId,
|
||||
};
|
||||
return this.state;
|
||||
}
|
||||
this.state = {
|
||||
...this.state,
|
||||
revision: this.state.revision + 1,
|
||||
inspectNpcIds: addInspect(this.state.inspectNpcIds, event.npcId),
|
||||
inspectFocusNpcId: event.npcId,
|
||||
};
|
||||
return this.state;
|
||||
}
|
||||
case 'inspect.show': {
|
||||
this.state = {
|
||||
...this.state,
|
||||
revision: this.state.revision + 1,
|
||||
inspectNpcIds: addInspect(this.state.inspectNpcIds, event.npcId),
|
||||
inspectFocusNpcId: event.npcId,
|
||||
};
|
||||
return this.state;
|
||||
}
|
||||
@@ -157,8 +219,19 @@ export class NpcsOverlayStore {
|
||||
|
||||
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 inspectNpcIds = this.state.inspectNpcIds.filter((id) => npcIds.has(id));
|
||||
const sameActive = activeNpcIds.length === this.state.activeNpcIds.length;
|
||||
const sameInspect = inspectNpcIds.length === this.state.inspectNpcIds.length;
|
||||
if (sameActive && sameInspect) return this.state;
|
||||
if (activeNpcIds.length === 0 && inspectNpcIds.length === 0) {
|
||||
const zoomTool = this.state.zoomTool;
|
||||
this.state = {
|
||||
...emptyState(),
|
||||
revision: this.state.revision + 1,
|
||||
zoomTool,
|
||||
};
|
||||
return this.state;
|
||||
}
|
||||
const layouts: Record<string, NpcsOverlayLayout> = {};
|
||||
for (const id of activeNpcIds) {
|
||||
const layout = this.state.layouts[id];
|
||||
@@ -168,12 +241,18 @@ export class NpcsOverlayStore {
|
||||
this.state.focusNpcId && activeNpcIds.includes(this.state.focusNpcId)
|
||||
? this.state.focusNpcId
|
||||
: (activeNpcIds[activeNpcIds.length - 1] ?? null);
|
||||
const inspectFocusNpcId =
|
||||
this.state.inspectFocusNpcId && inspectNpcIds.includes(this.state.inspectFocusNpcId)
|
||||
? this.state.inspectFocusNpcId
|
||||
: (inspectNpcIds[inspectNpcIds.length - 1] ?? null);
|
||||
this.state = {
|
||||
revision: this.state.revision + 1,
|
||||
activeNpcIds,
|
||||
layouts,
|
||||
focusNpcId,
|
||||
zoomTool: this.state.zoomTool,
|
||||
zoomTool: activeNpcIds.length === 0 ? null : this.state.zoomTool,
|
||||
inspectNpcIds,
|
||||
inspectFocusNpcId,
|
||||
};
|
||||
return this.state;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user