/** * Resolves a creature name against existing combatant names, * handling auto-numbering for duplicates. * * - No conflict: returns name as-is, no renames. * - First conflict (one existing match): renames existing to "Name 1", * new becomes "Name 2". * - Subsequent conflicts: new gets next number suffix. */ export function resolveCreatureName( baseName: string, existingNames: readonly string[], ): { newName: string; renames: ReadonlyArray<{ from: string; to: string }>; } { // Find exact matches and numbered matches (e.g., "Goblin 1", "Goblin 2") const exactMatches: number[] = []; let maxNumber = 0; for (let i = 0; i < existingNames.length; i++) { const name = existingNames[i]; if (name === baseName) { exactMatches.push(i); } else { const match = new RegExp(`^${escapeRegExp(baseName)} (\\d+)$`).exec(name); if (match) { const num = Number.parseInt(match[1], 10); if (num > maxNumber) maxNumber = num; } } } // No conflict at all if (exactMatches.length === 0 && maxNumber === 0) { return { newName: baseName, renames: [] }; } // First conflict: one exact match, no numbered ones yet if (exactMatches.length === 1 && maxNumber === 0) { return { newName: `${baseName} 2`, renames: [{ from: baseName, to: `${baseName} 1` }], }; } // Subsequent conflicts: append next number const nextNumber = Math.max(maxNumber, exactMatches.length) + 1; return { newName: `${baseName} ${nextNumber}`, renames: [] }; } function escapeRegExp(s: string): string { return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }