Phase 29: Gameplay Systems (5/5) - Structure interaction, NPCs, Enemies, Quests, Map
This commit is contained in:
274
src/systems/BiomeEnemySystem.js
Normal file
274
src/systems/BiomeEnemySystem.js
Normal file
@@ -0,0 +1,274 @@
|
||||
/**
|
||||
* 👹 BIOME ENEMY SYSTEM
|
||||
* Spawns biome-specific enemies across the world
|
||||
* - Different enemies per biome
|
||||
* - Difficulty scaling
|
||||
* - Loot drops
|
||||
* - Combat integration
|
||||
*/
|
||||
|
||||
class BiomeEnemySystem {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
|
||||
// All enemies in the world
|
||||
this.enemies = [];
|
||||
|
||||
// Enemy types per biome
|
||||
this.enemyTypes = {
|
||||
'Grassland': [
|
||||
{ type: 'wolf', hp: 30, damage: 5, speed: 1.5, loot: ['meat', 'fur'], color: 0x8B4513 },
|
||||
{ type: 'boar', hp: 40, damage: 7, speed: 1.2, loot: ['meat', 'tusk'], color: 0x654321 },
|
||||
{ type: 'bandit', hp: 50, damage: 10, speed: 1.0, loot: ['gold', 'sword'], color: 0x696969 }
|
||||
],
|
||||
'Forest': [
|
||||
{ type: 'goblin', hp: 25, damage: 6, speed: 1.3, loot: ['gold', 'dagger'], color: 0x228B22 },
|
||||
{ type: 'spider', hp: 20, damage: 4, speed: 1.8, loot: ['web', 'poison'], color: 0x2F4F4F },
|
||||
{ type: 'ent', hp: 80, damage: 15, speed: 0.8, loot: ['wood', 'seed'], color: 0x8B4513 }
|
||||
],
|
||||
'Desert': [
|
||||
{ type: 'scorpion', hp: 35, damage: 8, speed: 1.4, loot: ['poison', 'chitin'], color: 0xD2691E },
|
||||
{ type: 'mummy', hp: 60, damage: 12, speed: 0.9, loot: ['bandage', 'ancient_coin'], color: 0xDEB887 },
|
||||
{ type: 'sand_worm', hp: 100, damage: 20, speed: 0.7, loot: ['scales', 'tooth'], color: 0xC0B090 }
|
||||
],
|
||||
'Mountain': [
|
||||
{ type: 'troll', hp: 90, damage: 18, speed: 0.8, loot: ['stone', 'club'], color: 0x708090 },
|
||||
{ type: 'golem', hp: 120, damage: 25, speed: 0.6, loot: ['ore', 'core'], color: 0x696969 },
|
||||
{ type: 'harpy', hp: 45, damage: 10, speed: 2.0, loot: ['feather', 'talon'], color: 0x9370DB }
|
||||
],
|
||||
'Swamp': [
|
||||
{ type: 'zombie', hp: 50, damage: 10, speed: 0.9, loot: ['bone', 'rotten_flesh'], color: 0x556B2F },
|
||||
{ type: 'will_o_wisp', hp: 30, damage: 8, speed: 2.5, loot: ['essence', 'spark'], color: 0x00FFFF },
|
||||
{ type: 'swamp_dragon', hp: 150, damage: 30, speed: 0.7, loot: ['scale', 'heart'], color: 0x2F4F2F }
|
||||
]
|
||||
};
|
||||
|
||||
// Spawn density per biome
|
||||
this.spawnDensity = {
|
||||
'Grassland': 0.02, // 2% per tile
|
||||
'Forest': 0.03, // 3%
|
||||
'Desert': 0.015, // 1.5%
|
||||
'Mountain': 0.025, // 2.5%
|
||||
'Swamp': 0.035 // 3.5%
|
||||
};
|
||||
|
||||
console.log('👹 BiomeEnemySystem initialized');
|
||||
}
|
||||
|
||||
// Generate enemy spawns across the world
|
||||
generateSpawns(biomeSystem) {
|
||||
if (!biomeSystem) return;
|
||||
|
||||
let enemiesSpawned = 0;
|
||||
const sampleRate = 10; // Check every 10th tile
|
||||
|
||||
for (let x = 0; x < 500; x += sampleRate) {
|
||||
for (let y = 0; y < 500; y += sampleRate) {
|
||||
const biome = biomeSystem.getBiome(x, y);
|
||||
const density = this.spawnDensity[biome] || 0.02;
|
||||
|
||||
if (Math.random() < density) {
|
||||
this.spawnEnemy(x, y, biome);
|
||||
enemiesSpawned++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ Spawned ${enemiesSpawned} enemies across world`);
|
||||
}
|
||||
|
||||
// Spawn single enemy
|
||||
spawnEnemy(x, y, biome) {
|
||||
const enemyList = this.enemyTypes[biome] || this.enemyTypes['Grassland'];
|
||||
const enemyTemplate = enemyList[Math.floor(Math.random() * enemyList.length)];
|
||||
|
||||
const enemy = {
|
||||
x,
|
||||
y,
|
||||
type: enemyTemplate.type,
|
||||
biome,
|
||||
hp: enemyTemplate.hp,
|
||||
maxHp: enemyTemplate.hp,
|
||||
damage: enemyTemplate.damage,
|
||||
speed: enemyTemplate.speed,
|
||||
loot: [...enemyTemplate.loot],
|
||||
color: enemyTemplate.color,
|
||||
alive: true,
|
||||
sprite: null,
|
||||
lastMoveTime: 0
|
||||
};
|
||||
|
||||
this.enemies.push(enemy);
|
||||
return enemy;
|
||||
}
|
||||
|
||||
// Create enemy sprite when chunk loads
|
||||
createEnemySprite(enemy, chunk) {
|
||||
if (enemy.sprite || !enemy.alive) return;
|
||||
|
||||
const worldX = enemy.x * 48 + 24;
|
||||
const worldY = enemy.y * 48 + 24;
|
||||
|
||||
// Simple circle sprite
|
||||
const sprite = this.scene.add.circle(worldX, worldY, 15, enemy.color);
|
||||
sprite.setDepth(10 + worldY);
|
||||
|
||||
// HP bar
|
||||
const hpBar = this.scene.add.rectangle(worldX, worldY - 25, 30, 4, 0xFF0000);
|
||||
hpBar.setOrigin(0, 0.5);
|
||||
hpBar.setDepth(10 + worldY);
|
||||
|
||||
const hpFill = this.scene.add.rectangle(worldX, worldY - 25, 30, 4, 0x00FF00);
|
||||
hpFill.setOrigin(0, 0.5);
|
||||
hpFill.setDepth(10 + worldY);
|
||||
|
||||
enemy.sprite = sprite;
|
||||
enemy.hpBar = hpBar;
|
||||
enemy.hpFill = hpFill;
|
||||
|
||||
if (chunk) {
|
||||
chunk.sprites.push(sprite);
|
||||
chunk.sprites.push(hpBar);
|
||||
chunk.sprites.push(hpFill);
|
||||
}
|
||||
}
|
||||
|
||||
// Update enemies (AI, movement)
|
||||
update(time, delta, playerX, playerY) {
|
||||
for (const enemy of this.enemies) {
|
||||
if (!enemy.alive || !enemy.sprite) continue;
|
||||
|
||||
// Simple AI: Move towards player if nearby
|
||||
const dist = Math.sqrt((enemy.x - playerX) ** 2 + (enemy.y - playerY) ** 2);
|
||||
|
||||
if (dist < 10 && time > enemy.lastMoveTime + 500) {
|
||||
// Move towards player
|
||||
const dx = playerX - enemy.x;
|
||||
const dy = playerY - enemy.y;
|
||||
const len = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (len > 0) {
|
||||
enemy.x += (dx / len) * enemy.speed * 0.1;
|
||||
enemy.y += (dy / len) * enemy.speed * 0.1;
|
||||
|
||||
// Update sprite position
|
||||
enemy.sprite.setPosition(enemy.x * 48 + 24, enemy.y * 48 + 24);
|
||||
if (enemy.hpBar) {
|
||||
enemy.hpBar.setPosition(enemy.x * 48 + 24, enemy.y * 48 - 1);
|
||||
enemy.hpFill.setPosition(enemy.x * 48 + 24, enemy.y * 48 - 1);
|
||||
}
|
||||
}
|
||||
|
||||
enemy.lastMoveTime = time;
|
||||
}
|
||||
|
||||
// Attack player if very close
|
||||
if (dist < 1.5 && time > enemy.lastAttackTime + 2000) {
|
||||
this.attackPlayer(enemy);
|
||||
enemy.lastAttackTime = time;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enemy attacks player
|
||||
attackPlayer(enemy) {
|
||||
if (this.scene.player) {
|
||||
console.log(`👹 ${enemy.type} attacks for ${enemy.damage} damage!`);
|
||||
// TODO: Integrate with player health system
|
||||
|
||||
// Visual feedback
|
||||
if (this.scene.cameras) {
|
||||
this.scene.cameras.main.shake(200, 0.005);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Player attacks enemy
|
||||
damageEnemy(enemy, damage) {
|
||||
if (!enemy.alive) return;
|
||||
|
||||
enemy.hp -= damage;
|
||||
|
||||
// Update HP bar
|
||||
if (enemy.hpFill) {
|
||||
const hpPercent = Math.max(0, enemy.hp / enemy.maxHp);
|
||||
enemy.hpFill.setScale(hpPercent, 1);
|
||||
}
|
||||
|
||||
console.log(`⚔️ ${enemy.type} takes ${damage} damage! (${enemy.hp}/${enemy.maxHp} HP)`);
|
||||
|
||||
// Death
|
||||
if (enemy.hp <= 0) {
|
||||
this.killEnemy(enemy);
|
||||
}
|
||||
}
|
||||
|
||||
// Kill enemy and drop loot
|
||||
killEnemy(enemy) {
|
||||
enemy.alive = false;
|
||||
|
||||
console.log(`💀 ${enemy.type} died!`);
|
||||
|
||||
// Drop loot
|
||||
if (this.scene.inventorySystem && enemy.loot.length > 0) {
|
||||
const lootItem = enemy.loot[Math.floor(Math.random() * enemy.loot.length)];
|
||||
const amount = Math.floor(Math.random() * 3) + 1;
|
||||
|
||||
this.scene.inventorySystem.addItem(lootItem, amount);
|
||||
console.log(` 💰 Dropped: ${amount}x ${lootItem}`);
|
||||
}
|
||||
|
||||
// Destroy sprite
|
||||
if (enemy.sprite) {
|
||||
enemy.sprite.destroy();
|
||||
if (enemy.hpBar) enemy.hpBar.destroy();
|
||||
if (enemy.hpFill) enemy.hpFill.destroy();
|
||||
enemy.sprite = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Find nearest enemy to point
|
||||
findNearestEnemy(x, y, maxDistance = 2) {
|
||||
let nearest = null;
|
||||
let minDist = maxDistance;
|
||||
|
||||
for (const enemy of this.enemies) {
|
||||
if (!enemy.alive) continue;
|
||||
|
||||
const dist = Math.sqrt((enemy.x - x) ** 2 + (enemy.y - y) ** 2);
|
||||
if (dist < minDist) {
|
||||
minDist = dist;
|
||||
nearest = enemy;
|
||||
}
|
||||
}
|
||||
|
||||
return nearest;
|
||||
}
|
||||
|
||||
// Get statistics
|
||||
getStats() {
|
||||
const alive = this.enemies.filter(e => e.alive).length;
|
||||
const byBiome = {};
|
||||
|
||||
for (const enemy of this.enemies) {
|
||||
if (!enemy.alive) continue;
|
||||
byBiome[enemy.biome] = (byBiome[enemy.biome] || 0) + 1;
|
||||
}
|
||||
|
||||
return {
|
||||
totalEnemies: this.enemies.length,
|
||||
alive,
|
||||
dead: this.enemies.length - alive,
|
||||
byBiome
|
||||
};
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.enemies.forEach(enemy => {
|
||||
if (enemy.sprite) enemy.sprite.destroy();
|
||||
if (enemy.hpBar) enemy.hpBar.destroy();
|
||||
if (enemy.hpFill) enemy.hpFill.destroy();
|
||||
});
|
||||
this.enemies = [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user