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:
392
src/systems/FarmRaidSystem.js
Normal file
392
src/systems/FarmRaidSystem.js
Normal file
@@ -0,0 +1,392 @@
|
||||
/**
|
||||
* FARM RAID SYSTEM
|
||||
* Spawn raider waves, defend farm, rewards
|
||||
* Integrates with DefenseSystem and NomadRaiderAI
|
||||
*/
|
||||
|
||||
export class FarmRaidSystem {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
|
||||
// Raid state
|
||||
this.isRaidActive = false;
|
||||
this.currentWave = 0;
|
||||
this.totalWaves = 0;
|
||||
this.raidDifficulty = 1;
|
||||
|
||||
// Spawn management
|
||||
this.activeRaiders = [];
|
||||
this.spawnPoints = [];
|
||||
|
||||
// Raid triggers
|
||||
this.fameThreshold = 500; // Trigger raid at 500 fame
|
||||
this.resourceThreshold = 5000; // Or 5000 resources
|
||||
this.timeBetweenRaids = 600000; // 10 minutes minimum
|
||||
this.lastRaidTime = 0;
|
||||
|
||||
// Rewards
|
||||
this.defenseRewards = new Map();
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.initializeSpawnPoints();
|
||||
this.initializeRewards();
|
||||
|
||||
// Check for raid triggers periodically
|
||||
this.scene.time.addEvent({
|
||||
delay: 30000, // Check every 30s
|
||||
callback: () => this.checkRaidTriggers(),
|
||||
loop: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize raid spawn points around farm
|
||||
*/
|
||||
initializeSpawnPoints() {
|
||||
const farmCenter = { x: 400, y: 300 }; // Adjust to actual farm center
|
||||
const spawnDistance = 400;
|
||||
|
||||
// 8 spawn points around farm (N, NE, E, SE, S, SW, W, NW)
|
||||
for (let angle = 0; angle < 360; angle += 45) {
|
||||
const rad = Phaser.Math.DegToRad(angle);
|
||||
this.spawnPoints.push({
|
||||
x: farmCenter.x + Math.cos(rad) * spawnDistance,
|
||||
y: farmCenter.y + Math.sin(rad) * spawnDistance,
|
||||
angle: angle
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize defense rewards
|
||||
*/
|
||||
initializeRewards() {
|
||||
this.defenseRewards.set(1, { xp: 200, currency: 500, items: ['raider_loot_common'] });
|
||||
this.defenseRewards.set(2, { xp: 400, currency: 1000, items: ['raider_loot_common', 'raider_weapon'] });
|
||||
this.defenseRewards.set(3, { xp: 800, currency: 2000, items: ['raider_loot_rare', 'raider_armor'] });
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if raid should trigger
|
||||
*/
|
||||
checkRaidTriggers() {
|
||||
if (this.isRaidActive) return;
|
||||
|
||||
const now = Date.now();
|
||||
const timeSinceLastRaid = now - this.lastRaidTime;
|
||||
|
||||
// Cooldown check
|
||||
if (timeSinceLastRaid < this.timeBetweenRaids) return;
|
||||
|
||||
const fame = this.scene.gameState?.fame || 0;
|
||||
const totalResources = this.scene.inventorySystem?.getTotalResourceValue() || 0;
|
||||
|
||||
// Trigger conditions
|
||||
if (fame >= this.fameThreshold || totalResources >= this.resourceThreshold) {
|
||||
this.startRaid();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a raid
|
||||
*/
|
||||
startRaid(difficulty = null) {
|
||||
if (this.isRaidActive) return;
|
||||
|
||||
this.isRaidActive = true;
|
||||
this.currentWave = 0;
|
||||
this.raidDifficulty = difficulty || this.calculateDifficulty();
|
||||
this.totalWaves = 2 + this.raidDifficulty; // 3-5 waves
|
||||
this.lastRaidTime = Date.now();
|
||||
|
||||
// Notification
|
||||
this.scene.uiSystem?.showNotification(
|
||||
`RAID INCOMING! ${this.totalWaves} waves approaching!`,
|
||||
'danger',
|
||||
{ priority: 'high' }
|
||||
);
|
||||
|
||||
// SFX: Raid horn
|
||||
this.scene.soundSystem?.play('raid_horn');
|
||||
|
||||
// VFX: Red screen flash
|
||||
this.scene.cameras.main.flash(1000, 255, 0, 0);
|
||||
|
||||
// Start first wave
|
||||
this.scene.time.delayedCall(3000, () => this.spawnWave());
|
||||
|
||||
console.log(`🚨 RAID STARTED: Difficulty ${this.raidDifficulty}, ${this.totalWaves} waves`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate raid difficulty based on game progress
|
||||
*/
|
||||
calculateDifficulty() {
|
||||
const fame = this.scene.gameState?.fame || 0;
|
||||
const population = this.scene.gameState?.population || 0;
|
||||
|
||||
// Scale difficulty
|
||||
return Math.min(5, Math.floor((fame / 500) + (population / 20)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn a wave of raiders
|
||||
*/
|
||||
spawnWave() {
|
||||
this.currentWave++;
|
||||
|
||||
const raiderCount = 3 + (this.raidDifficulty * 2); // 5-13 raiders
|
||||
const waveRaiders = [];
|
||||
|
||||
// Spawn raiders
|
||||
for (let i = 0; i < raiderCount; i++) {
|
||||
const spawnPoint = Phaser.Utils.Array.GetRandom(this.spawnPoints);
|
||||
const raider = this.spawnRaider(spawnPoint.x, spawnPoint.y);
|
||||
waveRaiders.push(raider);
|
||||
}
|
||||
|
||||
this.activeRaiders.push(...waveRaiders);
|
||||
|
||||
// Notification
|
||||
this.scene.uiSystem?.showNotification(
|
||||
`Wave ${this.currentWave}/${this.totalWaves}: ${raiderCount} raiders!`,
|
||||
'warning'
|
||||
);
|
||||
|
||||
console.log(`🔥 Wave ${this.currentWave}: Spawned ${raiderCount} raiders`);
|
||||
|
||||
// Check for next wave
|
||||
this.scene.time.delayedCall(5000, () => this.checkWaveProgress());
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn individual raider
|
||||
*/
|
||||
spawnRaider(x, y) {
|
||||
// Create raider sprite
|
||||
const raider = this.scene.physics.add.sprite(x, y, 'nomad_raider');
|
||||
|
||||
// Stats based on difficulty
|
||||
raider.maxHealth = 50 + (this.raidDifficulty * 20);
|
||||
raider.health = raider.maxHealth;
|
||||
raider.attackDamage = 10 + (this.raidDifficulty * 5);
|
||||
raider.active = true;
|
||||
|
||||
// Add AI
|
||||
raider.ai = new NomadRaiderAI(this.scene, raider);
|
||||
|
||||
// Collisions
|
||||
this.scene.physics.add.collider(raider, this.scene.player);
|
||||
this.scene.physics.add.collider(raider, this.scene.zombieScout);
|
||||
|
||||
// Health bar
|
||||
raider.healthBar = this.scene.add.graphics();
|
||||
|
||||
// Death handler
|
||||
raider.takeDamage = (amount) => this.raiderTakeDamage(raider, amount);
|
||||
|
||||
return raider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Raider takes damage
|
||||
*/
|
||||
raiderTakeDamage(raider, amount) {
|
||||
raider.health -= amount;
|
||||
|
||||
// Update health bar
|
||||
this.updateRaiderHealthBar(raider);
|
||||
|
||||
// VFX
|
||||
raider.setTint(0xff0000);
|
||||
this.scene.time.delayedCall(100, () => raider.clearTint());
|
||||
|
||||
// Death
|
||||
if (raider.health <= 0) {
|
||||
this.raiderDeath(raider);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Raider death
|
||||
*/
|
||||
raiderDeath(raider) {
|
||||
raider.active = false;
|
||||
|
||||
// Drop loot
|
||||
this.dropRaiderLoot(raider.x, raider.y);
|
||||
|
||||
// VFX
|
||||
this.scene.vfxSystem?.playEffect('raider_death', raider.x, raider.y);
|
||||
|
||||
// Remove from active list
|
||||
const index = this.activeRaiders.indexOf(raider);
|
||||
if (index > -1) {
|
||||
this.activeRaiders.splice(index, 1);
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
raider.ai?.destroy();
|
||||
raider.healthBar?.destroy();
|
||||
raider.destroy();
|
||||
|
||||
// Check if wave complete
|
||||
this.checkWaveProgress();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update raider health bar
|
||||
*/
|
||||
updateRaiderHealthBar(raider) {
|
||||
if (!raider.healthBar) return;
|
||||
|
||||
raider.healthBar.clear();
|
||||
|
||||
const barWidth = 40;
|
||||
const barHeight = 4;
|
||||
const healthPercent = raider.health / raider.maxHealth;
|
||||
|
||||
// Background
|
||||
raider.healthBar.fillStyle(0x000000);
|
||||
raider.healthBar.fillRect(raider.x - barWidth / 2, raider.y - 30, barWidth, barHeight);
|
||||
|
||||
// Health
|
||||
raider.healthBar.fillStyle(0xff0000);
|
||||
raider.healthBar.fillRect(raider.x - barWidth / 2, raider.y - 30, barWidth * healthPercent, barHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop loot from raider
|
||||
*/
|
||||
dropRaiderLoot(x, y) {
|
||||
const lootTable = ['wood', 'stone', 'metal', 'raider_weapon', 'medicine'];
|
||||
const drop = Phaser.Utils.Array.GetRandom(lootTable);
|
||||
const amount = Phaser.Math.Between(1, 5);
|
||||
|
||||
// Create loot drop
|
||||
this.scene.lootSystem?.createDrop(x, y, drop, amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if wave is complete
|
||||
*/
|
||||
checkWaveProgress() {
|
||||
if (!this.isRaidActive) return;
|
||||
|
||||
const aliveRaiders = this.activeRaiders.filter(r => r.active);
|
||||
|
||||
if (aliveRaiders.length === 0) {
|
||||
// Wave complete
|
||||
if (this.currentWave < this.totalWaves) {
|
||||
// Next wave
|
||||
this.scene.uiSystem?.showNotification(
|
||||
`Wave ${this.currentWave} cleared! Next wave incoming...`,
|
||||
'success'
|
||||
);
|
||||
|
||||
this.scene.time.delayedCall(5000, () => this.spawnWave());
|
||||
} else {
|
||||
// Raid complete
|
||||
this.endRaid(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* End raid (success or failure)
|
||||
*/
|
||||
endRaid(success) {
|
||||
this.isRaidActive = false;
|
||||
|
||||
if (success) {
|
||||
// Victory!
|
||||
const rewards = this.defenseRewards.get(this.raidDifficulty) || this.defenseRewards.get(1);
|
||||
|
||||
this.scene.uiSystem?.showNotification(
|
||||
`RAID DEFENDED! +${rewards.xp} XP, +${rewards.currency} coins`,
|
||||
'victory',
|
||||
{ priority: 'high' }
|
||||
);
|
||||
|
||||
// Grant rewards
|
||||
this.scene.zombieScoutLeveling?.awardXP(rewards.xp, 'raid_defense');
|
||||
this.scene.economySystem?.addCurrency(rewards.currency);
|
||||
|
||||
rewards.items.forEach(item => {
|
||||
this.scene.inventorySystem?.addItem(item, 1);
|
||||
});
|
||||
|
||||
// VFX
|
||||
this.scene.vfxSystem?.playEffect('victory', 400, 300);
|
||||
this.scene.cameras.main.flash(1000, 0, 255, 0);
|
||||
|
||||
console.log(`✅ RAID DEFENDED! Rewards granted.`);
|
||||
} else {
|
||||
// Defeat
|
||||
this.scene.uiSystem?.showNotification(
|
||||
'RAID DEFEATED YOU! Farm damaged.',
|
||||
'defeat',
|
||||
{ priority: 'high' }
|
||||
);
|
||||
|
||||
// Damage farm buildings
|
||||
this.damageFarm();
|
||||
|
||||
console.log(`❌ RAID FAILED! Farm damaged.`);
|
||||
}
|
||||
|
||||
// Cleanup remaining raiders
|
||||
this.activeRaiders.forEach(raider => {
|
||||
raider.ai?.destroy();
|
||||
raider.healthBar?.destroy();
|
||||
raider.destroy();
|
||||
});
|
||||
this.activeRaiders = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Damage farm on raid failure
|
||||
*/
|
||||
damageFarm() {
|
||||
// Damage random buildings
|
||||
const buildings = this.scene.buildingSystem?.getAllBuildings() || [];
|
||||
const damagedCount = Math.min(3, buildings.length);
|
||||
|
||||
for (let i = 0; i < damagedCount; i++) {
|
||||
const building = Phaser.Utils.Array.GetRandom(buildings);
|
||||
building.takeDamage?.(50);
|
||||
}
|
||||
|
||||
// Destroy random crops
|
||||
const crops = this.scene.crops || [];
|
||||
const destroyedCrops = Math.min(10, crops.length);
|
||||
|
||||
for (let i = 0; i < destroyedCrops; i++) {
|
||||
const crop = Phaser.Utils.Array.GetRandom(crops);
|
||||
crop.destroy?.();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manual raid trigger (for testing/quests)
|
||||
*/
|
||||
triggerRaid(difficulty = 1) {
|
||||
this.startRaid(difficulty);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get raid state
|
||||
*/
|
||||
getRaidState() {
|
||||
return {
|
||||
isActive: this.isRaidActive,
|
||||
currentWave: this.currentWave,
|
||||
totalWaves: this.totalWaves,
|
||||
difficulty: this.raidDifficulty,
|
||||
activeRaiders: this.activeRaiders.filter(r => r.active).length
|
||||
};
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
420
src/systems/ZombieScoutSkills.js
Normal file
420
src/systems/ZombieScoutSkills.js
Normal file
@@ -0,0 +1,420 @@
|
||||
/**
|
||||
* ZOMBIE SCOUT SKILLS SYSTEM
|
||||
* Skill tree with active/passive abilities
|
||||
* Integrates with ZombieScoutLevelingSystem
|
||||
*/
|
||||
|
||||
export class ZombieScoutSkills {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
|
||||
// Skill trees
|
||||
this.skills = new Map();
|
||||
this.unlockedSkills = new Set();
|
||||
this.activeSkills = new Map(); // Currently equipped active skills
|
||||
|
||||
// Skill categories
|
||||
this.categories = ['digging', 'combat', 'utility'];
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.initializeSkillTree();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all available skills
|
||||
*/
|
||||
initializeSkillTree() {
|
||||
// DIGGING SKILLS
|
||||
this.registerSkill({
|
||||
id: 'speed_dig',
|
||||
name: 'Speed Dig',
|
||||
category: 'digging',
|
||||
type: 'active',
|
||||
unlockLevel: 10,
|
||||
cooldown: 30000, // 30 seconds
|
||||
duration: 10000, // 10 seconds
|
||||
description: 'Dig 50% faster for 10 seconds',
|
||||
effect: () => this.activateSpeedDig()
|
||||
});
|
||||
|
||||
this.registerSkill({
|
||||
id: 'deep_scan',
|
||||
name: 'Deep Scan',
|
||||
category: 'digging',
|
||||
type: 'passive',
|
||||
unlockLevel: 5,
|
||||
description: 'Reveal buried items within 5 tiles',
|
||||
effect: () => this.enableDeepScan()
|
||||
});
|
||||
|
||||
this.registerSkill({
|
||||
id: 'treasure_sense',
|
||||
name: 'Treasure Sense',
|
||||
category: 'digging',
|
||||
type: 'active',
|
||||
unlockLevel: 15,
|
||||
cooldown: 60000, // 60 seconds
|
||||
description: 'Reveal all buried treasure on screen',
|
||||
effect: () => this.activateTreasureSense()
|
||||
});
|
||||
|
||||
// COMBAT SKILLS
|
||||
this.registerSkill({
|
||||
id: 'basic_attack',
|
||||
name: 'Claw Swipe',
|
||||
category: 'combat',
|
||||
type: 'active',
|
||||
unlockLevel: 5,
|
||||
cooldown: 2000, // 2 seconds
|
||||
damage: 10,
|
||||
description: 'Basic melee attack dealing 10 damage',
|
||||
effect: () => this.performBasicAttack()
|
||||
});
|
||||
|
||||
this.registerSkill({
|
||||
id: 'leap_attack',
|
||||
name: 'Leap Attack',
|
||||
category: 'combat',
|
||||
type: 'active',
|
||||
unlockLevel: 12,
|
||||
cooldown: 8000, // 8 seconds
|
||||
damage: 25,
|
||||
description: 'Leap at enemy dealing 25 damage',
|
||||
effect: () => this.performLeapAttack()
|
||||
});
|
||||
|
||||
this.registerSkill({
|
||||
id: 'tank_stance',
|
||||
name: 'Undead Resilience',
|
||||
category: 'combat',
|
||||
type: 'passive',
|
||||
unlockLevel: 8,
|
||||
description: '+20% damage resistance',
|
||||
effect: () => this.enableTankStance()
|
||||
});
|
||||
|
||||
// UTILITY SKILLS
|
||||
this.registerSkill({
|
||||
id: 'scout_speed',
|
||||
name: 'Scout Sprint',
|
||||
category: 'utility',
|
||||
type: 'active',
|
||||
unlockLevel: 7,
|
||||
cooldown: 20000, // 20 seconds
|
||||
duration: 5000, // 5 seconds
|
||||
description: 'Move 100% faster for 5 seconds',
|
||||
effect: () => this.activateScoutSpeed()
|
||||
});
|
||||
|
||||
this.registerSkill({
|
||||
id: 'pack_mule',
|
||||
name: 'Pack Mule',
|
||||
category: 'utility',
|
||||
type: 'passive',
|
||||
unlockLevel: 6,
|
||||
description: '+10 inventory slots',
|
||||
effect: () => this.enablePackMule()
|
||||
});
|
||||
|
||||
this.registerSkill({
|
||||
id: 'night_vision',
|
||||
name: 'Night Vision',
|
||||
category: 'utility',
|
||||
type: 'passive',
|
||||
unlockLevel: 10,
|
||||
description: 'See in darkness without penalty',
|
||||
effect: () => this.enableNightVision()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a skill in the system
|
||||
*/
|
||||
registerSkill(skillData) {
|
||||
this.skills.set(skillData.id, {
|
||||
...skillData,
|
||||
unlocked: false,
|
||||
active: false,
|
||||
lastUsed: 0
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlock skill (called when level requirement met)
|
||||
*/
|
||||
unlockSkill(skillId) {
|
||||
const skill = this.skills.get(skillId);
|
||||
if (!skill) return false;
|
||||
|
||||
const scoutLevel = this.scene.zombieScoutLeveling?.currentLevel || 1;
|
||||
|
||||
// Check level requirement
|
||||
if (scoutLevel < skill.unlockLevel) {
|
||||
console.warn(`Scout level ${scoutLevel} too low for ${skill.name} (requires ${skill.unlockLevel})`);
|
||||
return false;
|
||||
}
|
||||
|
||||
skill.unlocked = true;
|
||||
this.unlockedSkills.add(skillId);
|
||||
|
||||
// Auto-activate passive skills
|
||||
if (skill.type === 'passive') {
|
||||
skill.effect();
|
||||
}
|
||||
|
||||
// Notification
|
||||
this.scene.uiSystem?.showNotification(
|
||||
`Skill Unlocked: ${skill.name}!`,
|
||||
'skill_unlock',
|
||||
{ description: skill.description }
|
||||
);
|
||||
|
||||
console.log(`✨ Unlocked skill: ${skill.name}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use active skill
|
||||
*/
|
||||
useSkill(skillId) {
|
||||
const skill = this.skills.get(skillId);
|
||||
if (!skill || !skill.unlocked || skill.type !== 'active') return false;
|
||||
|
||||
// Check cooldown
|
||||
const now = Date.now();
|
||||
const timeSinceLastUse = now - skill.lastUsed;
|
||||
|
||||
if (timeSinceLastUse < skill.cooldown) {
|
||||
const remainingCooldown = Math.ceil((skill.cooldown - timeSinceLastUse) / 1000);
|
||||
console.log(`Skill on cooldown: ${remainingCooldown}s remaining`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Activate skill
|
||||
skill.lastUsed = now;
|
||||
skill.effect();
|
||||
|
||||
// UI feedback
|
||||
this.scene.uiSystem?.showSkillActivation(skill.name, skill.cooldown);
|
||||
|
||||
console.log(`🔥 Activated: ${skill.name}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* SKILL EFFECTS
|
||||
*/
|
||||
|
||||
activateSpeedDig() {
|
||||
const scout = this.scene.zombieScout;
|
||||
if (!scout) return;
|
||||
|
||||
scout.digSpeed *= 1.5; // 50% faster
|
||||
|
||||
// VFX
|
||||
this.scene.vfxSystem?.playEffect('speed_buff', scout.x, scout.y);
|
||||
|
||||
// Restore after duration
|
||||
this.scene.time.delayedCall(10000, () => {
|
||||
scout.digSpeed /= 1.5;
|
||||
console.log('Speed Dig expired');
|
||||
});
|
||||
}
|
||||
|
||||
enableDeepScan() {
|
||||
// Enable passive treasure detection
|
||||
this.scene.gameState.buffs.treasure_detection_range = 5;
|
||||
console.log('Deep Scan enabled: 5 tile range');
|
||||
}
|
||||
|
||||
activateTreasureSense() {
|
||||
// Reveal all buried items on screen
|
||||
this.scene.treasureSystem?.revealAllTreasures();
|
||||
|
||||
// VFX: Screen pulse
|
||||
this.scene.cameras.main.flash(500, 255, 215, 0, true);
|
||||
|
||||
console.log('Treasure Sense activated: All treasures revealed');
|
||||
}
|
||||
|
||||
performBasicAttack() {
|
||||
const scout = this.scene.zombieScout;
|
||||
if (!scout) return;
|
||||
|
||||
// Find nearest enemy
|
||||
const enemy = this.findNearestEnemy(scout, 50);
|
||||
if (!enemy) {
|
||||
console.log('No enemy in range');
|
||||
return;
|
||||
}
|
||||
|
||||
// Deal damage
|
||||
enemy.takeDamage(10);
|
||||
|
||||
// VFX
|
||||
this.scene.vfxSystem?.playEffect('claw_swipe', enemy.x, enemy.y);
|
||||
this.scene.soundSystem?.play('zombie_attack');
|
||||
|
||||
console.log('Basic attack: 10 damage');
|
||||
}
|
||||
|
||||
performLeapAttack() {
|
||||
const scout = this.scene.zombieScout;
|
||||
if (!scout) return;
|
||||
|
||||
// Find nearest enemy
|
||||
const enemy = this.findNearestEnemy(scout, 150);
|
||||
if (!enemy) return;
|
||||
|
||||
// Leap animation
|
||||
this.scene.tweens.add({
|
||||
targets: scout,
|
||||
x: enemy.x,
|
||||
y: enemy.y - 20,
|
||||
duration: 300,
|
||||
ease: 'Quad.easeOut',
|
||||
onComplete: () => {
|
||||
// Deal damage on landing
|
||||
enemy.takeDamage(25);
|
||||
this.scene.vfxSystem?.playEffect('impact', enemy.x, enemy.y);
|
||||
this.scene.cameras.main.shake(200, 0.008);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Leap attack: 25 damage');
|
||||
}
|
||||
|
||||
enableTankStance() {
|
||||
const scout = this.scene.zombieScout;
|
||||
if (scout) {
|
||||
scout.damageResistance = (scout.damageResistance || 0) + 0.2; // +20%
|
||||
console.log('Undead Resilience enabled: +20% damage resistance');
|
||||
}
|
||||
}
|
||||
|
||||
activateScoutSpeed() {
|
||||
const scout = this.scene.zombieScout;
|
||||
if (!scout) return;
|
||||
|
||||
scout.moveSpeed *= 2; // 100% faster
|
||||
|
||||
// VFX: Speed trails
|
||||
this.scene.vfxSystem?.playEffect('speed_trail', scout.x, scout.y, { duration: 5000 });
|
||||
|
||||
// Restore after duration
|
||||
this.scene.time.delayedCall(5000, () => {
|
||||
scout.moveSpeed /= 2;
|
||||
console.log('Scout Sprint expired');
|
||||
});
|
||||
}
|
||||
|
||||
enablePackMule() {
|
||||
this.scene.inventorySystem?.addInventorySlots(10);
|
||||
console.log('Pack Mule enabled: +10 inventory slots');
|
||||
}
|
||||
|
||||
enableNightVision() {
|
||||
this.scene.gameState.buffs.night_vision = true;
|
||||
console.log('Night Vision enabled: Full visibility at night');
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Find nearest enemy
|
||||
*/
|
||||
findNearestEnemy(from, maxDistance) {
|
||||
let nearest = null;
|
||||
let minDist = maxDistance;
|
||||
|
||||
// Search for enemies in scene
|
||||
const enemies = this.scene.enemies || [];
|
||||
|
||||
enemies.forEach(enemy => {
|
||||
const dist = Phaser.Math.Distance.Between(from.x, from.y, enemy.x, enemy.y);
|
||||
if (dist < minDist) {
|
||||
minDist = dist;
|
||||
nearest = enemy;
|
||||
}
|
||||
});
|
||||
|
||||
return nearest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unlocked skills by category
|
||||
*/
|
||||
getSkillsByCategory(category) {
|
||||
return Array.from(this.skills.values())
|
||||
.filter(s => s.category === category && s.unlocked);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all unlocked skills
|
||||
*/
|
||||
getUnlockedSkills() {
|
||||
return Array.from(this.skills.values()).filter(s => s.unlocked);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check skill availability
|
||||
*/
|
||||
isSkillAvailable(skillId) {
|
||||
const skill = this.skills.get(skillId);
|
||||
if (!skill || !skill.unlocked) return false;
|
||||
|
||||
if (skill.type === 'passive') return true;
|
||||
|
||||
const now = Date.now();
|
||||
return (now - skill.lastUsed) >= skill.cooldown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get skill cooldown remaining
|
||||
*/
|
||||
getCooldownRemaining(skillId) {
|
||||
const skill = this.skills.get(skillId);
|
||||
if (!skill) return 0;
|
||||
|
||||
const now = Date.now();
|
||||
const elapsed = now - skill.lastUsed;
|
||||
const remaining = Math.max(0, skill.cooldown - elapsed);
|
||||
|
||||
return Math.ceil(remaining / 1000); // Return in seconds
|
||||
}
|
||||
|
||||
/**
|
||||
* Save/Load
|
||||
*/
|
||||
getSaveData() {
|
||||
return {
|
||||
unlockedSkills: Array.from(this.unlockedSkills),
|
||||
skillCooldowns: Array.from(this.skills.entries()).map(([id, skill]) => ({
|
||||
id,
|
||||
lastUsed: skill.lastUsed
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
loadSaveData(data) {
|
||||
this.unlockedSkills = new Set(data.unlockedSkills || []);
|
||||
|
||||
// Restore cooldowns
|
||||
data.skillCooldowns?.forEach(({ id, lastUsed }) => {
|
||||
const skill = this.skills.get(id);
|
||||
if (skill) {
|
||||
skill.lastUsed = lastUsed;
|
||||
skill.unlocked = this.unlockedSkills.has(id);
|
||||
}
|
||||
});
|
||||
|
||||
// Re-apply passive skills
|
||||
this.unlockedSkills.forEach(skillId => {
|
||||
const skill = this.skills.get(skillId);
|
||||
if (skill && skill.type === 'passive') {
|
||||
skill.effect();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user