SYSTEMS 4-6/9 COMPLETE: ZombieScoutSkills (skill tree, active/passive abilities), NomadRaiderAI (state machine, pathfinding, loot stealing), FarmRaidSystem (wave spawning, difficulty scaling, rewards). Progress: 6/9 systems (67%).
This commit is contained in:
348
src/systems/NomadRaiderAI.js
Normal file
348
src/systems/NomadRaiderAI.js
Normal file
@@ -0,0 +1,348 @@
|
||||
/**
|
||||
* NOMAD RAIDER AI SYSTEM
|
||||
* Enemy AI for raiding bandits
|
||||
* Pathfinding, combat, loot stealing
|
||||
*/
|
||||
|
||||
export class NomadRaiderAI {
|
||||
constructor(scene, raider) {
|
||||
this.scene = scene;
|
||||
this.raider = raider;
|
||||
|
||||
// AI state
|
||||
this.state = 'idle'; // idle, patrol, attack, steal, flee
|
||||
this.target = null;
|
||||
this.homePosition = { x: raider.x, y: raider.y };
|
||||
|
||||
// AI parameters
|
||||
this.detectionRange = 200;
|
||||
this.attackRange = 40;
|
||||
this.fleeHealthThreshold = 0.3; // Flee at 30% HP
|
||||
|
||||
// Behavior timers
|
||||
this.stateTimer = 0;
|
||||
this.decisionInterval = 1000; // Make decision every 1s
|
||||
|
||||
// Loot targeting
|
||||
this.targetedLoot = null;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Start AI loop
|
||||
this.aiLoop = this.scene.time.addEvent({
|
||||
delay: this.decisionInterval,
|
||||
callback: () => this.makeDecision(),
|
||||
loop: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Main AI decision-making
|
||||
*/
|
||||
makeDecision() {
|
||||
if (!this.raider.active) return;
|
||||
|
||||
// Check health for flee condition
|
||||
const healthPercent = this.raider.health / this.raider.maxHealth;
|
||||
if (healthPercent < this.fleeHealthThreshold && this.state !== 'flee') {
|
||||
this.setState('flee');
|
||||
return;
|
||||
}
|
||||
|
||||
// State machine
|
||||
switch (this.state) {
|
||||
case 'idle':
|
||||
this.idleBehavior();
|
||||
break;
|
||||
case 'patrol':
|
||||
this.patrolBehavior();
|
||||
break;
|
||||
case 'attack':
|
||||
this.attackBehavior();
|
||||
break;
|
||||
case 'steal':
|
||||
this.stealBehavior();
|
||||
break;
|
||||
case 'flee':
|
||||
this.fleeBehavior();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* STATE: Idle - Look for targets
|
||||
*/
|
||||
idleBehavior() {
|
||||
// Check for player
|
||||
const player = this.scene.player;
|
||||
if (this.isInRange(player, this.detectionRange)) {
|
||||
this.target = player;
|
||||
this.setState('attack');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for Zombie Scout
|
||||
const scout = this.scene.zombieScout;
|
||||
if (scout && this.isInRange(scout, this.detectionRange)) {
|
||||
this.target = scout;
|
||||
this.setState('attack');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for stealable loot (crops, chests)
|
||||
const loot = this.findNearestLoot();
|
||||
if (loot) {
|
||||
this.targetedLoot = loot;
|
||||
this.setState('steal');
|
||||
return;
|
||||
}
|
||||
|
||||
// Default: patrol
|
||||
this.setState('patrol');
|
||||
}
|
||||
|
||||
/**
|
||||
* STATE: Patrol - Wander around
|
||||
*/
|
||||
patrolBehavior() {
|
||||
// Random wandering
|
||||
if (this.stateTimer <= 0) {
|
||||
const randomX = this.homePosition.x + Phaser.Math.Between(-150, 150);
|
||||
const randomY = this.homePosition.y + Phaser.Math.Between(-150, 150);
|
||||
|
||||
this.moveToward(randomX, randomY);
|
||||
this.stateTimer = Phaser.Math.Between(2000, 4000); // Patrol for 2-4s
|
||||
}
|
||||
|
||||
this.stateTimer -= this.decisionInterval;
|
||||
|
||||
// Check for threats while patrolling
|
||||
if (this.detectThreat()) {
|
||||
this.setState('attack');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* STATE: Attack - Combat with player/scout
|
||||
*/
|
||||
attackBehavior() {
|
||||
if (!this.target || !this.target.active) {
|
||||
this.setState('idle');
|
||||
return;
|
||||
}
|
||||
|
||||
const distance = Phaser.Math.Distance.Between(
|
||||
this.raider.x, this.raider.y,
|
||||
this.target.x, this.target.y
|
||||
);
|
||||
|
||||
// Move toward target
|
||||
if (distance > this.attackRange) {
|
||||
this.moveToward(this.target.x, this.target.y);
|
||||
} else {
|
||||
// In range: attack
|
||||
this.performAttack();
|
||||
}
|
||||
|
||||
// Lost target
|
||||
if (distance > this.detectionRange * 1.5) {
|
||||
this.target = null;
|
||||
this.setState('idle');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* STATE: Steal - Take loot and escape
|
||||
*/
|
||||
stealBehavior() {
|
||||
if (!this.targetedLoot) {
|
||||
this.setState('idle');
|
||||
return;
|
||||
}
|
||||
|
||||
const distance = Phaser.Math.Distance.Between(
|
||||
this.raider.x, this.raider.y,
|
||||
this.targetedLoot.x, this.targetedLoot.y
|
||||
);
|
||||
|
||||
if (distance > 30) {
|
||||
// Move toward loot
|
||||
this.moveToward(this.targetedLoot.x, this.targetedLoot.y);
|
||||
} else {
|
||||
// Steal loot
|
||||
this.stealLoot(this.targetedLoot);
|
||||
this.targetedLoot = null;
|
||||
this.setState('flee'); // Escape after stealing
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* STATE: Flee - Escape when low health or after stealing
|
||||
*/
|
||||
fleeBehavior() {
|
||||
// Run away from player
|
||||
const player = this.scene.player;
|
||||
if (player) {
|
||||
const angle = Phaser.Math.Angle.Between(
|
||||
player.x, player.y,
|
||||
this.raider.x, this.raider.y
|
||||
);
|
||||
|
||||
const fleeX = this.raider.x + Math.cos(angle) * 200;
|
||||
const fleeY = this.raider.y + Math.sin(angle) * 200;
|
||||
|
||||
this.moveToward(fleeX, fleeY);
|
||||
}
|
||||
|
||||
// Check if safe to stop fleeing
|
||||
const distanceFromPlayer = Phaser.Math.Distance.Between(
|
||||
this.raider.x, this.raider.y,
|
||||
player.x, player.y
|
||||
);
|
||||
|
||||
if (distanceFromPlayer > this.detectionRange * 2) {
|
||||
// Safe: despawn or return to base
|
||||
this.setState('idle');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change AI state
|
||||
*/
|
||||
setState(newState) {
|
||||
console.log(`Raider AI: ${this.state} → ${newState}`);
|
||||
this.state = newState;
|
||||
this.stateTimer = 0;
|
||||
|
||||
// State entry actions
|
||||
switch (newState) {
|
||||
case 'attack':
|
||||
this.raider.setTint(0xff0000); // Red tint when aggressive
|
||||
break;
|
||||
case 'flee':
|
||||
this.raider.setTint(0xffff00); // Yellow when fleeing
|
||||
break;
|
||||
default:
|
||||
this.raider.clearTint();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Movement
|
||||
*/
|
||||
moveToward(targetX, targetY) {
|
||||
const angle = Phaser.Math.Angle.Between(
|
||||
this.raider.x, this.raider.y,
|
||||
targetX, targetY
|
||||
);
|
||||
|
||||
const speed = this.state === 'flee' ? 150 : 100; // Faster when fleeing
|
||||
|
||||
this.raider.setVelocity(
|
||||
Math.cos(angle) * speed,
|
||||
Math.sin(angle) * speed
|
||||
);
|
||||
|
||||
// Flip sprite
|
||||
this.raider.flipX = targetX < this.raider.x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attack execution
|
||||
*/
|
||||
performAttack() {
|
||||
if (!this.target) return;
|
||||
|
||||
// Attack cooldown check
|
||||
const now = Date.now();
|
||||
if (this.raider.lastAttack && now - this.raider.lastAttack < 1500) return;
|
||||
|
||||
this.raider.lastAttack = now;
|
||||
|
||||
// Deal damage
|
||||
this.target.takeDamage?.(this.raider.attackDamage || 10);
|
||||
|
||||
// VFX
|
||||
this.scene.vfxSystem?.playEffect('raider_attack', this.target.x, this.target.y);
|
||||
this.scene.soundSystem?.play('raider_hit');
|
||||
|
||||
console.log(`Raider attacked ${this.target.name || 'target'} for ${this.raider.attackDamage || 10} damage`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loot stealing
|
||||
*/
|
||||
stealLoot(loot) {
|
||||
// Determine loot value
|
||||
const stolenValue = loot.value || Phaser.Math.Between(50, 200);
|
||||
|
||||
// Remove loot from world
|
||||
loot.destroy?.() || loot.setVisible(false);
|
||||
|
||||
// Notification
|
||||
this.scene.uiSystem?.showNotification(
|
||||
`Raider stole ${stolenValue} worth of loot!`,
|
||||
'warning'
|
||||
);
|
||||
|
||||
// VFX
|
||||
this.scene.vfxSystem?.playEffect('loot_stolen', loot.x, loot.y);
|
||||
|
||||
console.log(`Raider stole loot worth ${stolenValue}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detection helpers
|
||||
*/
|
||||
isInRange(target, range) {
|
||||
if (!target) return false;
|
||||
|
||||
const distance = Phaser.Math.Distance.Between(
|
||||
this.raider.x, this.raider.y,
|
||||
target.x, target.y
|
||||
);
|
||||
|
||||
return distance <= range;
|
||||
}
|
||||
|
||||
detectThreat() {
|
||||
const player = this.scene.player;
|
||||
const scout = this.scene.zombieScout;
|
||||
|
||||
return this.isInRange(player, this.detectionRange) ||
|
||||
this.isInRange(scout, this.detectionRange);
|
||||
}
|
||||
|
||||
findNearestLoot() {
|
||||
// Find crops or storage
|
||||
const lootables = this.scene.crops?.filter(c => c.isHarvestable) || [];
|
||||
|
||||
let nearest = null;
|
||||
let minDist = 300; // Max search range
|
||||
|
||||
lootables.forEach(loot => {
|
||||
const dist = Phaser.Math.Distance.Between(
|
||||
this.raider.x, this.raider.y,
|
||||
loot.x, loot.y
|
||||
);
|
||||
|
||||
if (dist < minDist) {
|
||||
minDist = dist;
|
||||
nearest = loot;
|
||||
}
|
||||
});
|
||||
|
||||
return nearest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup
|
||||
*/
|
||||
destroy() {
|
||||
if (this.aiLoop) {
|
||||
this.aiLoop.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user