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:
Ivan Fontosh
2026-08-19 10:42:57 +08:00
parent 1a9f0f2557
commit 0f4bb76b41
35 changed files with 1189 additions and 259 deletions
@@ -80,6 +80,11 @@ void test('zipStore: normalizeScene defaults previewThumbAssetId for older proje
assert.match(src, /function normalizeScene\(/);
});
void test('zipStore: NPC parse defaults externalUrl', () => {
const src = fs.readFileSync(path.join(here, 'zipStore.ts'), 'utf8');
assert.match(src, /externalUrl: normalizeNpcExternalUrl/);
});
void test('zipStore: listProjects skips unreadable archives', () => {
const src = fs.readFileSync(path.join(here, 'zipStore.ts'), 'utf8');
assert.match(
+11
View File
@@ -66,6 +66,7 @@ import {
npcDispositionRingColor,
type NpcDisposition,
} from '../../shared/types/npcDisposition';
import { normalizeNpcExternalUrl } from '../../shared/types/npcExternalUrl';
import { DEFAULT_SCENE_GRID, normalizeSceneGrid } from '../../shared/types/sceneGrid';
import {
asPreviewRotationDeg,
@@ -1396,6 +1397,7 @@ export class ZipProjectStore {
npcId?: NpcId;
name: string;
description?: string;
externalUrl?: string;
filePath?: string;
groupId?: NpcGroupId | null;
ringColor?: string;
@@ -1477,6 +1479,9 @@ export class ZipProjectStore {
name,
avatarAssetId: nextAssetId ?? prev.avatarAssetId,
description: typeof input.description === 'string' ? input.description : prev.description,
...(input.externalUrl !== undefined
? { externalUrl: normalizeNpcExternalUrl(input.externalUrl) }
: {}),
groupId: resolveGroup(input.groupId, prev.groupId),
...(input.ringColor !== undefined
? { ringColor: normalizeHexColor(input.ringColor, DEFAULT_PLAYER_RING_COLOR) }
@@ -1503,6 +1508,7 @@ export class ZipProjectStore {
name,
avatarAssetId: nextAssetId,
description: typeof input.description === 'string' ? input.description : '',
externalUrl: normalizeNpcExternalUrl(input.externalUrl),
x: 80 + (count % 4) * 220,
y: 80 + Math.floor(count / 4) * 200,
groupId: resolveGroup(input.groupId, null),
@@ -1526,6 +1532,7 @@ export class ZipProjectStore {
patch: {
name?: string;
description?: string;
externalUrl?: string;
groupId?: NpcGroupId | null;
ringColor?: string;
disposition?: NpcDisposition;
@@ -1559,6 +1566,9 @@ export class ZipProjectStore {
...n,
...(name !== undefined ? { name } : {}),
...(typeof patch.description === 'string' ? { description: patch.description } : {}),
...(patch.externalUrl !== undefined
? { externalUrl: normalizeNpcExternalUrl(patch.externalUrl) }
: {}),
groupId,
disposition,
ringColor:
@@ -2650,6 +2660,7 @@ function normalizeProject(p: Project): Project {
name,
avatarAssetId: obj.avatarAssetId,
description: typeof obj.description === 'string' ? obj.description : '',
externalUrl: normalizeNpcExternalUrl((obj as { externalUrl?: unknown }).externalUrl),
x,
y,
groupId: resolveNpcGroupId(obj.groupId, groupIdSet),