349 lines
8.9 KiB
JavaScript
349 lines
8.9 KiB
JavaScript
/**
|
|
* 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();
|
|
}
|
|
}
|
|
}
|