import type { CollectionPreview, LaunchAvailability } from '../deployment-plan.js'; import { parseServerName, selectedZombieMap, type ZombieGame } from '../map-aliases.js'; const gameLabels: Record = { t5: 'Black Ops I', t6: 'Black Ops II' }; export interface CollectionMapRow { key: string; mapName: string; target: number; existing: number; create: number; remove: number; } export interface CollectionMapGroup { game: ZombieGame | null; label: string; rows: CollectionMapRow[]; } export interface CollectionKeyCoverage { game: ZombieGame; label: string; required: number; /** Reviewed creations covered by their game pool, not the pool's total size. */ available: number; /** Creations without lobby identities have not been checked against the pool. */ unassigned: number; } /** Keep pool coverage separate for each game, including creations without a free lobby. */ export function collectionKeyCoverage(preview: CollectionPreview): CollectionKeyCoverage[] { const coverage = new Map(); for (const server of preview.servers) { const game = preview.entries[server.entryIndex].recipe.game; const pool = coverage.get(game) ?? { game, label: gameLabels[game], required: 0, available: 0, unassigned: 0 }; pool.required++; if (!server.name) pool.unassigned++; else if (server.hasSavedKey) pool.available++; coverage.set(game, pool); } return [...coverage.values()].sort((a, b) => a.game.localeCompare(b.game)); } function removalMap(name: string): { game: ZombieGame | null; key: string; mapName: string } { const parsed = parseServerName(name); const game = parsed?.game.toLowerCase(); const supportedGame = game === 't5' || game === 't6' ? game : null; if (parsed && supportedGame && (!parsed.gameMode || parsed.gameMode === 'zombie')) { // Resolve friendly legacy aliases without merging T6 variants that share an engine map. const map = selectedZombieMap(supportedGame, parsed.mapCanonical, parsed.mapSlug); if (map) return { game: supportedGame, key: `${supportedGame}:${map.slug}`, mapName: map.friendly }; } // A raw identity is preferable to assigning an unknown server to the wrong map. return { game: supportedGame, key: `${supportedGame ?? 'other'}:server:${name}`, mapName: name }; } /** Summarize affected inventory, including exact-mode removals outside the saved collection. */ export function collectionMapGroups(preview: CollectionPreview): CollectionMapGroup[] { const groups = new Map(); const rows = new Map(); const existingNames = new Map>(); const serverRows = new Map(); const addRow = (game: ZombieGame | null, row: CollectionMapRow) => { const group = groups.get(game) ?? { game, label: game ? gameLabels[game] : 'Other servers', rows: [] }; group.rows.push(row); groups.set(game, group); rows.set(row.key, row); existingNames.set(row.key, new Set()); return row; }; for (const entry of preview.entries) { const { game, mapSlug, mapName } = entry.recipe; const row = addRow(game, { key: `${game}:${mapSlug}`, mapName, target: entry.count, existing: entry.existing, create: entry.missing, remove: 0 }); for (const name of entry.existingServers) { existingNames.get(row.key)!.add(name); // Backend coverage knows stored map selections even when a name is nonstandard. serverRows.set(name, row); } } if (preview.mode === 'exact') { for (const { name } of preview.removals) { const identity = removalMap(name); const row = serverRows.get(name) ?? rows.get(identity.key) ?? addRow(identity.game, { key: identity.key, mapName: identity.mapName, target: 0, existing: 0, create: 0, remove: 0, }); row.remove++; const counted = existingNames.get(row.key)!; // Coverage excludes pending removals. Include those identities in displayed inventory // while counting ordinary excess servers only once. if (!counted.has(name)) { row.existing++; counted.add(name); } } } return [...groups.values()].sort((a, b) => (a.game ?? 'z').localeCompare(b.game ?? 'z')); } /** Capacity remains an estimate: insufficient capacity can wait in the startup queue. */ export function collectionCapacitySummary(availability?: LaunchAvailability): string { if (availability?.requested === 0) return 'No new server capacity needed.'; if (!availability || availability.estimatedAvailable === null) return 'Capacity estimate unavailable; launches can wait for capacity.'; if (availability.estimatedAvailable >= availability.requested) { return availability.requested === 1 ? 'Estimated capacity for 1 server.' : `Estimated capacity for all ${availability.requested} servers.`; } return `Estimated capacity for ${availability.estimatedAvailable} of ${availability.requested} server${availability.requested === 1 ? '' : 's'}; launches can wait for capacity.`; }