feat(npcs): show multiple character overlays in session playback
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>
This commit is contained in:
@@ -12,12 +12,17 @@ import {
|
||||
function emptyState(): NpcsOverlayState {
|
||||
return {
|
||||
revision: 1,
|
||||
activeNpcId: null,
|
||||
layout: { ...DEFAULT_NPCS_OVERLAY_LAYOUT },
|
||||
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();
|
||||
|
||||
@@ -26,13 +31,14 @@ export class NpcsOverlayStore {
|
||||
}
|
||||
|
||||
clear(): NpcsOverlayState {
|
||||
if (this.state.activeNpcId === null && this.state.zoomTool === null) {
|
||||
if (this.state.activeNpcIds.length === 0 && this.state.zoomTool === null) {
|
||||
return this.state;
|
||||
}
|
||||
this.state = {
|
||||
revision: this.state.revision + 1,
|
||||
activeNpcId: null,
|
||||
layout: { ...DEFAULT_NPCS_OVERLAY_LAYOUT },
|
||||
activeNpcIds: [],
|
||||
layouts: {},
|
||||
focusNpcId: null,
|
||||
zoomTool: null,
|
||||
};
|
||||
return this.state;
|
||||
@@ -42,32 +48,67 @@ export class NpcsOverlayStore {
|
||||
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();
|
||||
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,
|
||||
activeNpcId: event.npcId,
|
||||
layout: { ...DEFAULT_NPCS_OVERLAY_LAYOUT },
|
||||
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.activeNpcId === null) return this.state;
|
||||
if (!this.state.activeNpcIds.includes(event.npcId)) return this.state;
|
||||
this.state = {
|
||||
...this.state,
|
||||
revision: this.state.revision + 1,
|
||||
layout: clampNpcsLayout(event.layout),
|
||||
focusNpcId: event.npcId,
|
||||
layouts: {
|
||||
...this.state.layouts,
|
||||
[event.npcId]: clampNpcsLayout(event.layout),
|
||||
},
|
||||
};
|
||||
return this.state;
|
||||
}
|
||||
@@ -81,10 +122,16 @@ export class NpcsOverlayStore {
|
||||
return this.state;
|
||||
}
|
||||
case 'zoomAt': {
|
||||
if (this.state.activeNpcId === null || !this.state.zoomTool) return this.state;
|
||||
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(
|
||||
this.state.layout,
|
||||
layoutFor(this.state, targetId),
|
||||
event.nx,
|
||||
event.ny,
|
||||
factor,
|
||||
@@ -92,7 +139,11 @@ export class NpcsOverlayStore {
|
||||
this.state = {
|
||||
...this.state,
|
||||
revision: this.state.revision + 1,
|
||||
layout,
|
||||
focusNpcId: targetId,
|
||||
layouts: {
|
||||
...this.state.layouts,
|
||||
[targetId]: layout,
|
||||
},
|
||||
};
|
||||
return this.state;
|
||||
}
|
||||
@@ -102,8 +153,25 @@ export class NpcsOverlayStore {
|
||||
}
|
||||
|
||||
ensureNpcStillExists(npcIds: ReadonlySet<NpcId>): NpcsOverlayState {
|
||||
const active = this.state.activeNpcId;
|
||||
if (active === null || npcIds.has(active)) return this.state;
|
||||
return this.clear();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user