/** * SpawnManager.js * Manages enemy tank spawning: timing, spawn points, composition, and limits. */ const EnemyTank = require('../entities/EnemyTank'); const { TANK_TYPE, MAX_ENEMIES_ON_SCREEN, ENEMY_SPAWN_INTERVAL, TILE_SIZE, MAP_OFFSET_X, MAP_OFFSET_Y, GRID_COLS, } = require('../base/GameGlobal'); class SpawnManager { constructor() { /** @type {Array<{col: number, row: number}>} */ this._spawnPoints = []; this._currentSpawnIndex = 0; // Spawn queue this._spawnQueue = []; // array of TANK_TYPE values this._spawnTimer = 0; this._spawnInterval = ENEMY_SPAWN_INTERVAL; this._totalSpawned = 0; this._totalEnemies = 0; // Level info this._levelNum = 1; // Power-up enemy indices (which enemies drop power-ups) this._powerUpIndices = new Set(); } /** * Initialize for a new level. * @param {object} levelData - Level configuration from LevelData. */ init(levelData) { this._spawnPoints = levelData.spawnPoints || [ { col: 0, row: 0 }, { col: Math.floor(GRID_COLS / 2), row: 0 }, { col: GRID_COLS - 1, row: 0 }, ]; this._currentSpawnIndex = 0; this._spawnTimer = 0; this._totalSpawned = 0; this._levelNum = levelData.id || 1; this._speedMultiplier = levelData.speedMultiplier || 1; // Build spawn queue from composition this._spawnQueue = []; const comp = levelData.enemies.composition; this._totalEnemies = levelData.enemies.total; // Add enemies by type for (let i = 0; i < (comp.boss || 0); i++) this._spawnQueue.push(TANK_TYPE.ENEMY_BOSS); for (let i = 0; i < (comp.armor || 0); i++) this._spawnQueue.push(TANK_TYPE.ENEMY_ARMOR); for (let i = 0; i < (comp.fast || 0); i++) this._spawnQueue.push(TANK_TYPE.ENEMY_FAST); for (let i = 0; i < (comp.normal || 0); i++) this._spawnQueue.push(TANK_TYPE.ENEMY_NORMAL); // Shuffle the queue for variety this._shuffleArray(this._spawnQueue); // Determine which enemies drop power-ups (roughly every 4-5 enemies) this._powerUpIndices.clear(); const numPowerUps = Math.max(1, Math.floor(this._totalEnemies / 5)); const indices = new Set(); while (indices.size < numPowerUps) { indices.add(Math.floor(Math.random() * this._totalEnemies)); } this._powerUpIndices = indices; // Spawn first batch immediately this._spawnTimer = this._spawnInterval; } /** * Update spawn timer and spawn enemies as needed. * @param {number} dt - Delta time in seconds. * @param {Array} activeEnemies - Currently alive enemies. * @returns {EnemyTank|null} Newly spawned enemy, or null. */ update(dt, activeEnemies) { if (this._spawnQueue.length === 0) return null; const aliveCount = activeEnemies.filter((e) => e.alive).length; if (aliveCount >= MAX_ENEMIES_ON_SCREEN) return null; this._spawnTimer += dt * 1000; if (this._spawnTimer < this._spawnInterval) return null; this._spawnTimer = 0; return this._spawnNext(); } /** * Spawn the next enemy from the queue. * @private * @returns {EnemyTank|null} */ _spawnNext() { if (this._spawnQueue.length === 0) return null; const type = this._spawnQueue.shift(); const spawnPoint = this._spawnPoints[this._currentSpawnIndex % this._spawnPoints.length]; this._currentSpawnIndex++; const hasPowerUp = this._powerUpIndices.has(this._totalSpawned); this._totalSpawned++; const enemy = new EnemyTank({ type, col: spawnPoint.col, row: spawnPoint.row, levelNum: this._levelNum, hasPowerUp, speedMultiplier: this._speedMultiplier, }); return enemy; } /** * Fisher-Yates shuffle. * @private */ _shuffleArray(arr) { for (let i = arr.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [arr[i], arr[j]] = [arr[j], arr[i]]; } } /** Number of enemies remaining to spawn. */ get remainingToSpawn() { return this._spawnQueue.length; } /** Total enemies for this level. */ get totalEnemies() { return this._totalEnemies; } /** Total spawned so far. */ get totalSpawned() { return this._totalSpawned; } /** Whether all enemies have been spawned. */ get allSpawned() { return this._spawnQueue.length === 0; } } module.exports = SpawnManager;