Files
Ivan Fontosh f4c0ac1438 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>
2026-07-20 13:19:22 +08:00

96 lines
2.8 KiB
TypeScript

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;
}