Files
WebFactory/js/enemies.js
2026-01-13 18:32:05 +00:00

352 lines
11 KiB
JavaScript

// Enemy System
const Enemies = {
list: [],
wave: 0,
waveTimer: 0,
timeBetweenWaves: 120, // seconds between waves
waveActive: false,
enemiesRemaining: 0,
spawnQueue: [],
spawnTimer: 0,
// Enemy types
TYPES: {
'biter': {
health: 50,
maxHealth: 50,
speed: 30, // pixels per second
damage: 10,
attackSpeed: 1, // attacks per second
range: 40, // attack range in pixels
color: '#44aa44',
colorDark: '#227722',
size: 0.6, // tile fraction
xp: 10
},
'spitter': {
health: 30,
maxHealth: 30,
speed: 25,
damage: 15,
attackSpeed: 0.5,
range: 120,
color: '#aa44aa',
colorDark: '#772277',
size: 0.5,
xp: 15
},
'big_biter': {
health: 200,
maxHealth: 200,
speed: 20,
damage: 30,
attackSpeed: 0.8,
range: 50,
color: '#228822',
colorDark: '#115511',
size: 0.9,
xp: 50
},
'tank_biter': {
health: 500,
maxHealth: 500,
speed: 15,
damage: 50,
attackSpeed: 0.5,
range: 60,
color: '#116611',
colorDark: '#0a440a',
size: 1.2,
xp: 100
}
},
// Initialize
init() {
this.list = [];
this.wave = 0;
this.waveTimer = this.timeBetweenWaves * 0.5; // First wave comes sooner
this.waveActive = false;
this.enemiesRemaining = 0;
this.spawnQueue = [];
this.spawnTimer = 0;
},
// Update enemies
update(dt) {
// Wave timer
if (!this.waveActive) {
this.waveTimer -= dt;
if (this.waveTimer <= 0) {
this.startWave();
}
}
// Spawn queued enemies
if (this.spawnQueue.length > 0) {
this.spawnTimer -= dt;
if (this.spawnTimer <= 0) {
this.spawnTimer = 0.5; // Spawn every 0.5 seconds
const spawn = this.spawnQueue.shift();
this.spawnEnemy(spawn.type, spawn.x, spawn.y);
}
}
// Update each enemy
for (let i = this.list.length - 1; i >= 0; i--) {
const enemy = this.list[i];
this.updateEnemy(enemy, dt);
// Remove dead enemies
if (enemy.health <= 0) {
this.list.splice(i, 1);
this.enemiesRemaining--;
Resources.add('xp', enemy.xp || 10);
Audio.playEnemyDeath();
}
}
// Check if wave is complete
if (this.waveActive && this.enemiesRemaining <= 0 && this.spawnQueue.length === 0) {
this.waveActive = false;
this.waveTimer = this.timeBetweenWaves;
Audio.playWaveComplete();
}
},
// Update single enemy
updateEnemy(enemy, dt) {
const type = this.TYPES[enemy.type];
// Find target (nearest building or base center)
const target = this.findTarget(enemy);
if (target) {
const dx = target.x - enemy.x;
const dy = target.y - enemy.y;
const dist = Math.sqrt(dx * dx + dy * dy);
// Move toward target if not in range
if (dist > type.range) {
const moveX = (dx / dist) * type.speed * dt;
const moveY = (dy / dist) * type.speed * dt;
enemy.x += moveX;
enemy.y += moveY;
enemy.angle = Math.atan2(dy, dx);
} else {
// Attack
enemy.attackCooldown = (enemy.attackCooldown || 0) - dt;
if (enemy.attackCooldown <= 0) {
enemy.attackCooldown = 1 / type.attackSpeed;
this.attack(enemy, target);
}
}
}
// Animation
enemy.animTime = (enemy.animTime || 0) + dt * 5;
},
// Find nearest target for enemy
findTarget(enemy) {
let nearest = null;
let nearestDist = Infinity;
// Check buildings
Buildings.list.forEach(b => {
const size = Utils.getBuildingSize(b.type);
const bx = (b.x + size.w / 2) * CONFIG.TILE_SIZE;
const by = (b.y + size.h / 2) * CONFIG.TILE_SIZE;
const dist = Math.sqrt((bx - enemy.x) ** 2 + (by - enemy.y) ** 2);
if (dist < nearestDist) {
nearestDist = dist;
nearest = { x: bx, y: by, building: b };
}
});
// Check towers
Towers.list.forEach(t => {
const tx = (t.x + 0.5) * CONFIG.TILE_SIZE;
const ty = (t.y + 0.5) * CONFIG.TILE_SIZE;
const dist = Math.sqrt((tx - enemy.x) ** 2 + (ty - enemy.y) ** 2);
if (dist < nearestDist) {
nearestDist = dist;
nearest = { x: tx, y: ty, tower: t };
}
});
// If no targets, go to map center
if (!nearest) {
nearest = {
x: CONFIG.MAP_WIDTH * CONFIG.TILE_SIZE / 2,
y: CONFIG.MAP_HEIGHT * CONFIG.TILE_SIZE / 2
};
}
return nearest;
},
// Enemy attacks target
attack(enemy, target) {
const type = this.TYPES[enemy.type];
if (target.building) {
target.building.health = (target.building.health || 100) - type.damage;
Audio.playBuildingDamage();
if (target.building.health <= 0) {
Buildings.remove(target.building.x, target.building.y);
Audio.playBuildingDestroyed();
}
} else if (target.tower) {
target.tower.health -= type.damage;
Audio.playBuildingDamage();
if (target.tower.health <= 0) {
Towers.remove(target.tower.x, target.tower.y);
Audio.playBuildingDestroyed();
}
}
// Visual feedback
enemy.attacking = true;
setTimeout(() => enemy.attacking = false, 100);
},
// Start a new wave
startWave() {
this.wave++;
this.waveActive = true;
Audio.playWaveStart();
// Calculate wave composition
const baseCount = 5 + this.wave * 3;
const spawnPoints = this.getSpawnPoints();
// Distribute enemies across spawn points
spawnPoints.forEach(point => {
let count = Math.ceil(baseCount / spawnPoints.length);
for (let i = 0; i < count; i++) {
let type = 'biter';
// Higher waves get tougher enemies
if (this.wave >= 3 && Math.random() < 0.2) type = 'spitter';
if (this.wave >= 5 && Math.random() < 0.15) type = 'big_biter';
if (this.wave >= 8 && Math.random() < 0.1) type = 'tank_biter';
// Add some randomness to spawn position
const offsetX = (Math.random() - 0.5) * 200;
const offsetY = (Math.random() - 0.5) * 200;
this.spawnQueue.push({
type,
x: point.x + offsetX,
y: point.y + offsetY
});
this.enemiesRemaining++;
}
});
},
// Get spawn points at map edges
getSpawnPoints() {
const points = [];
const margin = 2 * CONFIG.TILE_SIZE;
const mapW = CONFIG.MAP_WIDTH * CONFIG.TILE_SIZE;
const mapH = CONFIG.MAP_HEIGHT * CONFIG.TILE_SIZE;
// Pick 1-4 spawn points based on wave number
const numPoints = Math.min(4, 1 + Math.floor(this.wave / 3));
const edges = [
{ x: margin, y: mapH / 2 }, // Left
{ x: mapW - margin, y: mapH / 2 }, // Right
{ x: mapW / 2, y: margin }, // Top
{ x: mapW / 2, y: mapH - margin } // Bottom
];
// Shuffle and pick
for (let i = edges.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[edges[i], edges[j]] = [edges[j], edges[i]];
}
return edges.slice(0, numPoints);
},
// Spawn a single enemy
spawnEnemy(type, x, y) {
const enemyType = this.TYPES[type];
this.list.push({
type,
x,
y,
health: enemyType.maxHealth,
maxHealth: enemyType.maxHealth,
angle: 0,
animTime: 0,
attackCooldown: 0,
xp: enemyType.xp
});
},
// Damage an enemy
damage(enemy, amount) {
enemy.health -= amount;
enemy.damageFlash = 0.1;
Audio.playEnemyHit();
},
// Get enemies in radius
getInRadius(x, y, radius) {
return this.list.filter(e => {
const dist = Math.sqrt((e.x - x) ** 2 + (e.y - y) ** 2);
return dist <= radius;
});
},
// Get nearest enemy to position
getNearest(x, y, maxRange = Infinity) {
let nearest = null;
let nearestDist = maxRange;
this.list.forEach(e => {
const dist = Math.sqrt((e.x - x) ** 2 + (e.y - y) ** 2);
if (dist < nearestDist) {
nearestDist = dist;
nearest = e;
}
});
return nearest;
},
// Get time until next wave
getTimeUntilWave() {
return this.waveActive ? 0 : Math.max(0, this.waveTimer);
},
// Get data for saving
getData() {
return {
list: this.list.map(e => ({ ...e })),
wave: this.wave,
waveTimer: this.waveTimer,
waveActive: this.waveActive,
enemiesRemaining: this.enemiesRemaining,
spawnQueue: [...this.spawnQueue]
};
},
// Load data
setData(data) {
this.list = data.list?.map(e => ({ ...e })) || [];
this.wave = data.wave || 0;
this.waveTimer = data.waveTimer || this.timeBetweenWaves;
this.waveActive = data.waveActive || false;
this.enemiesRemaining = data.enemiesRemaining || 0;
this.spawnQueue = data.spawnQueue || [];
}
};