f4c0ac1438
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>
96 lines
2.8 KiB
TypeScript
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;
|
|
}
|