const DIGITS_ONLY = /^\d+$/; function scanExisting( baseName: string, existingNames: readonly string[], ): { exactMatches: number[]; maxNumber: number } { const exactMatches: number[] = []; let maxNumber = 0; const prefix = `${baseName} `; for (let i = 0; i < existingNames.length; i++) { const name = existingNames[i]; if (name === baseName) { exactMatches.push(i); } else if (name.startsWith(prefix)) { const suffix = name.slice(prefix.length); if (DIGITS_ONLY.test(suffix)) { const num = Number.parseInt(suffix, 10); if (num > maxNumber) maxNumber = num; } } } return { exactMatches, maxNumber }; } /** * 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 }>; } { const { exactMatches, maxNumber } = scanExisting(baseName, existingNames); // 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: [] }; }