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>
This commit is contained in:
@@ -61,13 +61,15 @@ function minimalProject(overrides: Partial<Project> = {}): Project {
|
||||
updatedAt: '2020-01-01T00:00:00.000Z',
|
||||
createdWithAppVersion: '1',
|
||||
appVersion: '1',
|
||||
schemaVersion: 7,
|
||||
schemaVersion: 8,
|
||||
},
|
||||
scenes: {},
|
||||
sceneListOrder: [],
|
||||
assets: {},
|
||||
campaignAudios: [],
|
||||
materials: [],
|
||||
npcs: [],
|
||||
npcRelations: [],
|
||||
currentSceneId: null,
|
||||
currentGraphNodeId: null,
|
||||
sceneGraphNodes: [],
|
||||
|
||||
@@ -14,7 +14,15 @@ import type {
|
||||
SceneId,
|
||||
} from '../types';
|
||||
import type { AssetId, ProjectId } from '../types/ids';
|
||||
import { asAssetId, asGraphNodeId, asMaterialId, asProjectId, asSceneId } from '../types/ids';
|
||||
import {
|
||||
asAssetId,
|
||||
asGraphNodeId,
|
||||
asMaterialId,
|
||||
asNpcId,
|
||||
asNpcRelationId,
|
||||
asProjectId,
|
||||
asSceneId,
|
||||
} from '../types/ids';
|
||||
|
||||
export type StorylineKind = 'main' | 'side';
|
||||
|
||||
@@ -274,6 +282,8 @@ export function buildPartialExportProject(
|
||||
assets,
|
||||
campaignAudios: source.campaignAudios.map((a) => ({ ...a })),
|
||||
materials: (source.materials ?? []).map((m) => ({ ...m })),
|
||||
npcs: (source.npcs ?? []).map((n) => ({ ...n })),
|
||||
npcRelations: (source.npcRelations ?? []).map((r) => ({ ...r })),
|
||||
currentSceneId: mainStart?.sceneId ?? firstSide?.sceneId ?? null,
|
||||
currentGraphNodeId: mainStart?.id ?? firstSide?.id ?? null,
|
||||
};
|
||||
@@ -289,6 +299,7 @@ function collectReferencedAssetIdsForProject(p: Project): Set<AssetId> {
|
||||
}
|
||||
for (const au of p.campaignAudios) refs.add(au.assetId);
|
||||
for (const m of p.materials ?? []) refs.add(m.assetId);
|
||||
for (const n of p.npcs ?? []) refs.add(n.avatarAssetId);
|
||||
return refs;
|
||||
}
|
||||
|
||||
@@ -376,6 +387,7 @@ export function mergeStorylinesIntoProject(
|
||||
}
|
||||
for (const au of source.campaignAudios) neededAssetIds.add(au.assetId);
|
||||
for (const m of source.materials ?? []) neededAssetIds.add(m.assetId);
|
||||
for (const n of source.npcs ?? []) neededAssetIds.add(n.avatarAssetId);
|
||||
|
||||
const assetMap = new Map<AssetId, AssetId>();
|
||||
const targetSha = new Map<string, AssetId>();
|
||||
@@ -509,12 +521,66 @@ export function mergeStorylinesIntoProject(
|
||||
materialAssetIds.add(mapped);
|
||||
}
|
||||
|
||||
const npcs = [...(target.npcs ?? [])];
|
||||
const npcNameKeys = new Set(npcs.map((n) => n.name.trim().toLowerCase()));
|
||||
const npcAssetIds = new Set(npcs.map((n) => n.avatarAssetId));
|
||||
const npcIdMap = new Map<string, string>();
|
||||
for (const n of source.npcs ?? []) {
|
||||
const mappedAvatar = assetMap.get(n.avatarAssetId) ?? n.avatarAssetId;
|
||||
const existingByAsset = npcs.find((x) => x.avatarAssetId === mappedAvatar);
|
||||
if (existingByAsset || npcAssetIds.has(mappedAvatar)) {
|
||||
if (existingByAsset) npcIdMap.set(n.id, existingByAsset.id);
|
||||
continue;
|
||||
}
|
||||
let name = n.name.trim();
|
||||
const baseKey = name.toLowerCase();
|
||||
if (npcNameKeys.has(baseKey)) {
|
||||
let i = 2;
|
||||
while (npcNameKeys.has(`${baseKey} (${String(i)})`)) i += 1;
|
||||
name = `${name} (${String(i)})`;
|
||||
}
|
||||
const newId = asNpcId(`npc_${generateId()}`);
|
||||
npcIdMap.set(n.id, newId);
|
||||
npcs.push({
|
||||
id: newId,
|
||||
name,
|
||||
avatarAssetId: mappedAvatar,
|
||||
description: n.description ?? '',
|
||||
x: n.x,
|
||||
y: n.y,
|
||||
});
|
||||
npcNameKeys.add(name.toLowerCase());
|
||||
npcAssetIds.add(mappedAvatar);
|
||||
}
|
||||
|
||||
const npcRelations = [...(target.npcRelations ?? [])];
|
||||
for (const r of source.npcRelations ?? []) {
|
||||
const sourceId = npcIdMap.get(r.sourceNpcId) ?? r.sourceNpcId;
|
||||
const targetId = npcIdMap.get(r.targetNpcId) ?? r.targetNpcId;
|
||||
if (!npcs.some((n) => n.id === sourceId) || !npcs.some((n) => n.id === targetId)) continue;
|
||||
const label = r.label.trim();
|
||||
if (!label) continue;
|
||||
const dup = npcRelations.some(
|
||||
(x) =>
|
||||
x.label === label && x.sourceNpcId === sourceId && x.targetNpcId === targetId,
|
||||
);
|
||||
if (dup) continue;
|
||||
npcRelations.push({
|
||||
id: asNpcRelationId(`nrel_${generateId()}`),
|
||||
sourceNpcId: asNpcId(sourceId),
|
||||
targetNpcId: asNpcId(targetId),
|
||||
label,
|
||||
});
|
||||
}
|
||||
|
||||
let merged: Project = {
|
||||
...target,
|
||||
scenes,
|
||||
assets,
|
||||
campaignAudios,
|
||||
materials,
|
||||
npcs,
|
||||
npcRelations,
|
||||
sceneGraphNodes: [...target.sceneGraphNodes, ...newGraphNodes],
|
||||
sceneGraphEdges: [...target.sceneGraphEdges, ...newEdges],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user