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