159 lines
4.3 KiB
JavaScript
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;
|