Upload files to "js"
This commit is contained in:
351
js/enemies.js
Normal file
351
js/enemies.js
Normal file
@@ -0,0 +1,351 @@
|
||||
// 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 || [];
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user