Files
tankwar_proj/js/managers/SpawnManager.js
T
2026-04-10 22:59:39 +08:00

159 lines
4.3 KiB
JavaScript

/**
* 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<EnemyTank>} 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;