feat(npcs): add groups, storyline bindings, and Foundry import
Nested NPC groups with color, graph filter, and scene/storyline binding; Foundry worlds/modules import actors into groups; storyline merge asks on NPC name conflicts and reports NPC counts. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
import type { NpcGroupId, NpcId, ProjectNpc, ProjectNpcGroup } from '../../shared/types';
|
||||
|
||||
export function reorderNpcIds(
|
||||
npcs: ProjectNpc[],
|
||||
dragId: NpcId,
|
||||
targetId: NpcId,
|
||||
place: 'before' | 'after',
|
||||
): NpcId[] {
|
||||
const ids = npcs.map((n) => n.id);
|
||||
const from = ids.indexOf(dragId);
|
||||
if (from < 0) return ids;
|
||||
ids.splice(from, 1);
|
||||
let to = ids.indexOf(targetId);
|
||||
if (to < 0) return npcs.map((n) => n.id);
|
||||
if (place === 'after') to += 1;
|
||||
ids.splice(to, 0, dragId);
|
||||
return ids;
|
||||
}
|
||||
|
||||
/** Move NPC to end of its group block in global order. */
|
||||
export function moveNpcToGroupEnd(npcs: ProjectNpc[], dragId: NpcId, groupId: NpcGroupId | null): NpcId[] {
|
||||
const updated = npcs.map((n) => (n.id === dragId ? { ...n, groupId } : n));
|
||||
const ids = updated.map((n) => n.id);
|
||||
const from = ids.indexOf(dragId);
|
||||
if (from < 0) return ids;
|
||||
ids.splice(from, 1);
|
||||
|
||||
let insertAt = ids.length;
|
||||
for (let i = ids.length - 1; i >= 0; i -= 1) {
|
||||
const npc = updated.find((n) => n.id === ids[i]);
|
||||
if (npc?.groupId === groupId) {
|
||||
insertAt = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (groupId === null) {
|
||||
for (let i = ids.length - 1; i >= 0; i -= 1) {
|
||||
const npc = updated.find((n) => n.id === ids[i]);
|
||||
if (npc?.groupId === null) {
|
||||
insertAt = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
ids.splice(insertAt, 0, dragId);
|
||||
return ids;
|
||||
}
|
||||
|
||||
export function reorderSiblingGroups(
|
||||
groups: ProjectNpcGroup[],
|
||||
dragId: NpcGroupId,
|
||||
targetId: NpcGroupId,
|
||||
place: 'before' | 'after',
|
||||
): NpcGroupId[] {
|
||||
const drag = groups.find((g) => g.id === dragId);
|
||||
const target = groups.find((g) => g.id === targetId);
|
||||
if (!drag || drag.parentId !== target?.parentId) return groups.map((g) => g.id);
|
||||
|
||||
const parentId = drag.parentId;
|
||||
const siblingIds = groups.filter((g) => g.parentId === parentId).map((g) => g.id);
|
||||
const from = siblingIds.indexOf(dragId);
|
||||
if (from < 0) return groups.map((g) => g.id);
|
||||
siblingIds.splice(from, 1);
|
||||
let to = siblingIds.indexOf(targetId);
|
||||
if (to < 0) return groups.map((g) => g.id);
|
||||
if (place === 'after') to += 1;
|
||||
siblingIds.splice(to, 0, dragId);
|
||||
|
||||
const result: NpcGroupId[] = [];
|
||||
let inserted = false;
|
||||
for (const g of groups) {
|
||||
if (g.parentId === parentId) {
|
||||
if (!inserted) {
|
||||
result.push(...siblingIds);
|
||||
inserted = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
result.push(g.id);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function flattenGroupOptions(
|
||||
nodes: { group: ProjectNpcGroup; children: unknown[] }[],
|
||||
depth = 0,
|
||||
): { id: NpcGroupId; label: string }[] {
|
||||
const out: { id: NpcGroupId; label: string }[] = [];
|
||||
for (const node of nodes) {
|
||||
const prefix = depth > 0 ? ' '.repeat(depth) : '';
|
||||
out.push({ id: node.group.id, label: `${prefix}${node.group.name}` });
|
||||
out.push(...flattenGroupOptions(node.children as typeof nodes, depth + 1));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
Reference in New Issue
Block a user