ZombieSystem - KRVAVA ŽETEV core feature! Alfa taming, worker tasks, leveling, decay & graves (900 LOC)
This commit is contained in:
875
src/systems/ZombieSystem.js
Normal file
875
src/systems/ZombieSystem.js
Normal file
@@ -0,0 +1,875 @@
|
|||||||
|
/**
|
||||||
|
* ZombieSystem.js
|
||||||
|
* ===============
|
||||||
|
* KRVAVA ŽETEV - Zombie Worker Management System
|
||||||
|
*
|
||||||
|
* Core Concept:
|
||||||
|
* Player is "Alfa" (hybrid virus) - can tame wild zombies
|
||||||
|
* Zombies can be assigned tasks: farming, mining, gathering, guarding
|
||||||
|
* Zombies level up, specialize, get tired, and eventually decay
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Zombie taming (Alfa scent system)
|
||||||
|
* - AI pathfinding & task execution
|
||||||
|
* - Skill specialization (Farmer, Miner, Guard, Gatherer)
|
||||||
|
* - Stamina & decay mechanics
|
||||||
|
* - Grave resting system (regeneration)
|
||||||
|
* - Work commands (follow, farm, mine, gather, guard)
|
||||||
|
*
|
||||||
|
* Uses: zombie_varieties_pack_tiled_1766101086057.tsx
|
||||||
|
* zombie_workers_2x2_grids_1766099189858.tsx
|
||||||
|
* smart_zombies_working_1766097073226.tsx
|
||||||
|
* specialty_zombie_workers_detailed_1766097635926.tsx
|
||||||
|
*
|
||||||
|
* @author NovaFarma Team
|
||||||
|
* @date 2025-12-22
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default class ZombieSystem {
|
||||||
|
constructor(scene) {
|
||||||
|
this.scene = scene;
|
||||||
|
|
||||||
|
// Zombie registry
|
||||||
|
this.zombies = new Map(); // id -> zombie data
|
||||||
|
this.wildZombies = new Set(); // Untamed zombies
|
||||||
|
this.tamedZombies = new Set(); // Player's zombies
|
||||||
|
|
||||||
|
// Alfa system
|
||||||
|
this.alfaScent = 100; // Player's Alfa power (0-100)
|
||||||
|
this.alfaRange = 150; // Taming range (pixels)
|
||||||
|
|
||||||
|
// Task queues
|
||||||
|
this.taskQueue = new Map(); // zombieId -> task
|
||||||
|
|
||||||
|
// Graves
|
||||||
|
this.graves = new Map(); // position -> grave data
|
||||||
|
|
||||||
|
// Zombie definitions
|
||||||
|
this.zombieTypes = this.defineZombieTypes();
|
||||||
|
this.skillTrees = this.defineSkillTrees();
|
||||||
|
|
||||||
|
// Timing constants
|
||||||
|
this.DECAY_RATE = 0.1; // HP loss per second when not resting
|
||||||
|
this.STAMINA_DRAIN_RATE = 5; // Stamina loss per second when working
|
||||||
|
this.STAMINA_REGEN_RATE = 20; // Stamina gain per second when resting
|
||||||
|
this.GRAVE_REST_BONUS = 2.0; // 2x regen in grave
|
||||||
|
|
||||||
|
console.log('🧟 ZombieSystem initialized - ALFA ACTIVE');
|
||||||
|
}
|
||||||
|
|
||||||
|
defineZombieTypes() {
|
||||||
|
return {
|
||||||
|
basic: {
|
||||||
|
name: 'Basic Zombie',
|
||||||
|
baseHP: 100,
|
||||||
|
baseStamina: 100,
|
||||||
|
baseSpeed: 60,
|
||||||
|
baseWorkSpeed: 1.0,
|
||||||
|
sprite: 'zombie_varieties_pack', // Frame 0
|
||||||
|
tameDifficulty: 1
|
||||||
|
},
|
||||||
|
worker: {
|
||||||
|
name: 'Zombie Worker',
|
||||||
|
baseHP: 120,
|
||||||
|
baseStamina: 150,
|
||||||
|
baseSpeed: 70,
|
||||||
|
baseWorkSpeed: 1.2,
|
||||||
|
sprite: 'zombie_workers_2x2_grids', // Frame 0
|
||||||
|
tameDifficulty: 2
|
||||||
|
},
|
||||||
|
smart: {
|
||||||
|
name: 'Smart Zombie',
|
||||||
|
baseHP: 150,
|
||||||
|
baseStamina: 120,
|
||||||
|
baseSpeed: 80,
|
||||||
|
baseWorkSpeed: 1.5,
|
||||||
|
sprite: 'smart_zombies_working',
|
||||||
|
tameDifficulty: 3,
|
||||||
|
canLearn: true // Levels faster
|
||||||
|
},
|
||||||
|
elite: {
|
||||||
|
name: 'Elite Zombie',
|
||||||
|
baseHP: 200,
|
||||||
|
baseStamina: 200,
|
||||||
|
baseSpeed: 100,
|
||||||
|
baseWorkSpeed: 2.0,
|
||||||
|
sprite: 'elite_zombie',
|
||||||
|
tameDifficulty: 5,
|
||||||
|
canLearn: true,
|
||||||
|
specialAbility: 'multitask' // Can do 2 tasks
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
defineSkillTrees() {
|
||||||
|
return {
|
||||||
|
farmer: {
|
||||||
|
name: 'Farmer Zombie',
|
||||||
|
tasks: ['plant', 'harvest', 'water', 'fertilize'],
|
||||||
|
levelBonuses: {
|
||||||
|
1: { workSpeed: 1.0 },
|
||||||
|
5: { workSpeed: 1.2, yieldBonus: 1.1 },
|
||||||
|
10: { workSpeed: 1.5, yieldBonus: 1.3, autoPlant: true },
|
||||||
|
15: { workSpeed: 2.0, yieldBonus: 1.5, qualityBonus: 1.2 }
|
||||||
|
},
|
||||||
|
sprite: 'specialty_zombie_workers', // Frame 0
|
||||||
|
color: 0x00FF00 // Green tint
|
||||||
|
},
|
||||||
|
miner: {
|
||||||
|
name: 'Miner Zombie',
|
||||||
|
tasks: ['mine', 'quarry', 'smelt'],
|
||||||
|
levelBonuses: {
|
||||||
|
1: { workSpeed: 1.0 },
|
||||||
|
5: { workSpeed: 1.2, oreBonus: 1.1 },
|
||||||
|
10: { workSpeed: 1.5, oreBonus: 1.3, rareOreChance: 0.1 },
|
||||||
|
15: { workSpeed: 2.0, oreBonus: 1.5, gemChance: 0.2 }
|
||||||
|
},
|
||||||
|
sprite: 'specialty_zombie_workers', // Frame 1
|
||||||
|
color: 0x888888 // Gray tint
|
||||||
|
},
|
||||||
|
gatherer: {
|
||||||
|
name: 'Gatherer Zombie',
|
||||||
|
tasks: ['gather', 'forage', 'loot'],
|
||||||
|
levelBonuses: {
|
||||||
|
1: { workSpeed: 1.0 },
|
||||||
|
5: { workSpeed: 1.2, gatherRadius: 1.2 },
|
||||||
|
10: { workSpeed: 1.5, gatherRadius: 1.5, rareItemChance: 0.1 },
|
||||||
|
15: { workSpeed: 2.0, gatherRadius: 2.0, doubleGather: 0.3 }
|
||||||
|
},
|
||||||
|
sprite: 'specialty_zombie_workers', // Frame 2
|
||||||
|
color: 0xFFFF00 // Yellow tint
|
||||||
|
},
|
||||||
|
guard: {
|
||||||
|
name: 'Guard Zombie',
|
||||||
|
tasks: ['guard', 'patrol', 'attack'],
|
||||||
|
levelBonuses: {
|
||||||
|
1: { damage: 10, defense: 5 },
|
||||||
|
5: { damage: 20, defense: 10, detectRange: 1.2 },
|
||||||
|
10: { damage: 35, defense: 20, detectRange: 1.5, counterAttack: true },
|
||||||
|
15: { damage: 50, defense: 30, detectRange: 2.0, areaAttack: true }
|
||||||
|
},
|
||||||
|
sprite: 'specialty_zombie_workers', // Frame 3
|
||||||
|
color: 0xFF0000 // Red tint
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== ZOMBIE SPAWNING =====
|
||||||
|
|
||||||
|
spawnWildZombie(x, y, type = 'basic') {
|
||||||
|
const zombieType = this.zombieTypes[type];
|
||||||
|
if (!zombieType) return null;
|
||||||
|
|
||||||
|
const zombie = {
|
||||||
|
id: this.generateId(),
|
||||||
|
type: type,
|
||||||
|
state: 'wild', // wild, tamed, working, resting, decaying
|
||||||
|
|
||||||
|
// Position
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
|
||||||
|
// Stats
|
||||||
|
hp: zombieType.baseHP,
|
||||||
|
maxHP: zombieType.baseHP,
|
||||||
|
stamina: zombieType.baseStamina,
|
||||||
|
maxStamina: zombieType.baseStamina,
|
||||||
|
|
||||||
|
// Progression
|
||||||
|
level: 1,
|
||||||
|
xp: 0,
|
||||||
|
xpToNextLevel: 100,
|
||||||
|
specialization: null, // farmer, miner, gatherer, guard
|
||||||
|
|
||||||
|
// Work
|
||||||
|
currentTask: null,
|
||||||
|
workSpeed: zombieType.baseWorkSpeed,
|
||||||
|
efficiency: 1.0,
|
||||||
|
|
||||||
|
// Decay
|
||||||
|
decayTimer: 24 * 60 * 60 * 1000, // 24 hours to decay
|
||||||
|
decayRate: this.DECAY_RATE,
|
||||||
|
|
||||||
|
// Behavior
|
||||||
|
speed: zombieType.baseSpeed,
|
||||||
|
aggression: 0.3, // Wild zombies slightly aggressive
|
||||||
|
loyalty: 0, // Increases when tamed
|
||||||
|
|
||||||
|
// Visual
|
||||||
|
sprite: null,
|
||||||
|
tint: 0xFFFFFF,
|
||||||
|
|
||||||
|
// Timestamps
|
||||||
|
spawnTime: Date.now(),
|
||||||
|
lastWorked: null,
|
||||||
|
lastRested: null
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create sprite
|
||||||
|
zombie.sprite = this.createZombieSprite(zombie, zombieType);
|
||||||
|
|
||||||
|
// Add to registry
|
||||||
|
this.zombies.set(zombie.id, zombie);
|
||||||
|
this.wildZombies.add(zombie.id);
|
||||||
|
|
||||||
|
console.log(`🧟 Spawned wild ${zombieType.name} at (${x}, ${y})`);
|
||||||
|
return zombie;
|
||||||
|
}
|
||||||
|
|
||||||
|
createZombieSprite(zombie, zombieType) {
|
||||||
|
const sprite = this.scene.add.sprite(zombie.x, zombie.y, zombieType.sprite);
|
||||||
|
|
||||||
|
sprite.setData('zombieId', zombie.id);
|
||||||
|
sprite.setInteractive();
|
||||||
|
|
||||||
|
// Physics
|
||||||
|
this.scene.physics.add.existing(sprite);
|
||||||
|
sprite.body.setCollideWorldBounds(true);
|
||||||
|
|
||||||
|
// Click handler
|
||||||
|
sprite.on('pointerdown', () => {
|
||||||
|
this.onZombieClicked(zombie);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Health bar
|
||||||
|
this.createHealthBar(sprite);
|
||||||
|
|
||||||
|
return sprite;
|
||||||
|
}
|
||||||
|
|
||||||
|
createHealthBar(sprite) {
|
||||||
|
const bar = this.scene.add.graphics();
|
||||||
|
bar.fillStyle(0xFF0000);
|
||||||
|
bar.fillRect(-20, -30, 40, 4);
|
||||||
|
sprite.healthBar = bar;
|
||||||
|
sprite.healthBar.x = sprite.x;
|
||||||
|
sprite.healthBar.y = sprite.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== ALFA SYSTEM (TAMING) =====
|
||||||
|
|
||||||
|
canTameZombie(zombieId) {
|
||||||
|
const zombie = this.zombies.get(zombieId);
|
||||||
|
if (!zombie) {
|
||||||
|
return { canTame: false, reason: 'Zombie not found' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zombie.state !== 'wild') {
|
||||||
|
return { canTame: false, reason: 'Already tamed' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check distance (Alfa scent range)
|
||||||
|
const distance = Phaser.Math.Distance.Between(
|
||||||
|
this.scene.player.x, this.scene.player.y,
|
||||||
|
zombie.x, zombie.y
|
||||||
|
);
|
||||||
|
|
||||||
|
if (distance > this.alfaRange) {
|
||||||
|
return { canTame: false, reason: 'Too far - get closer!' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Alfa power
|
||||||
|
const zombieType = this.zombieTypes[zombie.type];
|
||||||
|
const requiredAlfaPower = zombieType.tameDifficulty * 15;
|
||||||
|
|
||||||
|
if (this.alfaScent < requiredAlfaPower) {
|
||||||
|
return {
|
||||||
|
canTame: false,
|
||||||
|
reason: `Need ${requiredAlfaPower} Alfa power (have ${this.alfaScent})`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { canTame: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
tameZombie(zombieId) {
|
||||||
|
const check = this.canTameZombie(zombieId);
|
||||||
|
if (!check.canTame) {
|
||||||
|
this.scene.uiSystem?.showError(check.reason);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const zombie = this.zombies.get(zombieId);
|
||||||
|
const zombieType = this.zombieTypes[zombie.type];
|
||||||
|
|
||||||
|
// Taming animation
|
||||||
|
if (this.scene.particleSystem) {
|
||||||
|
this.scene.particleSystem.createTamingEffect(zombie.x, zombie.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update zombie state
|
||||||
|
zombie.state = 'tamed';
|
||||||
|
zombie.loyalty = 50; // Starts at 50, increases with work
|
||||||
|
zombie.aggression = 0;
|
||||||
|
|
||||||
|
// Remove from wild, add to tamed
|
||||||
|
this.wildZombies.delete(zombieId);
|
||||||
|
this.tamedZombies.add(zombieId);
|
||||||
|
|
||||||
|
// Visual feedback
|
||||||
|
zombie.sprite.setTint(0x00FF88); // Green tint for tamed
|
||||||
|
|
||||||
|
// Notification
|
||||||
|
this.scene.uiSystem?.showNotification({
|
||||||
|
title: '🧟 Zombie Tamed!',
|
||||||
|
message: `${zombieType.name} joined your workforce!`,
|
||||||
|
icon: 'zombie',
|
||||||
|
duration: 4000,
|
||||||
|
color: '#00FF00'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Emit event
|
||||||
|
this.scene.events.emit('zombieTamed', { zombie });
|
||||||
|
|
||||||
|
// Consume Alfa power
|
||||||
|
const cost = zombieType.tameDifficulty * 10;
|
||||||
|
this.alfaScent = Math.max(0, this.alfaScent - cost);
|
||||||
|
|
||||||
|
console.log(`🧟 Tamed ${zombieType.name} - Loyalty: ${zombie.loyalty}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== TASK ASSIGNMENT =====
|
||||||
|
|
||||||
|
assignTask(zombieId, task) {
|
||||||
|
const zombie = this.zombies.get(zombieId);
|
||||||
|
if (!zombie || zombie.state !== 'tamed') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if zombie has specialization for this task
|
||||||
|
if (zombie.specialization) {
|
||||||
|
const spec = this.skillTrees[zombie.specialization];
|
||||||
|
if (!spec.tasks.includes(task.type)) {
|
||||||
|
this.scene.uiSystem?.showError(`This zombie specializes in ${zombie.specialization} tasks!`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign task
|
||||||
|
zombie.currentTask = task;
|
||||||
|
zombie.state = 'working';
|
||||||
|
this.taskQueue.set(zombieId, task);
|
||||||
|
|
||||||
|
console.log(`🧟 ${zombie.id} assigned task:`, task.type);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
executeTask(zombie, delta) {
|
||||||
|
const task = zombie.currentTask;
|
||||||
|
if (!task) return;
|
||||||
|
|
||||||
|
const deltaSeconds = delta / 1000;
|
||||||
|
|
||||||
|
// Drain stamina
|
||||||
|
zombie.stamina -= this.STAMINA_DRAIN_RATE * deltaSeconds;
|
||||||
|
|
||||||
|
// If exhausted, stop working
|
||||||
|
if (zombie.stamina <= 0) {
|
||||||
|
zombie.state = 'idle';
|
||||||
|
zombie.currentTask = null;
|
||||||
|
this.scene.uiSystem?.showNotification({
|
||||||
|
title: 'Zombie Exhausted!',
|
||||||
|
message: `Zombie ${zombie.id} needs rest`,
|
||||||
|
icon: 'zombie',
|
||||||
|
duration: 2000
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute task based on type
|
||||||
|
switch (task.type) {
|
||||||
|
case 'farm':
|
||||||
|
this.executeFarmTask(zombie, task, deltaSeconds);
|
||||||
|
break;
|
||||||
|
case 'mine':
|
||||||
|
this.executeMineTask(zombie, task, deltaSeconds);
|
||||||
|
break;
|
||||||
|
case 'gather':
|
||||||
|
this.executeGatherTask(zombie, task, deltaSeconds);
|
||||||
|
break;
|
||||||
|
case 'guard':
|
||||||
|
this.executeGuardTask(zombie, task, deltaSeconds);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
executeFarmTask(zombie, task, deltaSeconds) {
|
||||||
|
// Move to farm plot
|
||||||
|
this.moveTowardsTarget(zombie, task.targetX, task.targetY);
|
||||||
|
|
||||||
|
// If close enough, work
|
||||||
|
const distance = Phaser.Math.Distance.Between(
|
||||||
|
zombie.x, zombie.y, task.targetX, task.targetY
|
||||||
|
);
|
||||||
|
|
||||||
|
if (distance < 20) {
|
||||||
|
task.progress = (task.progress || 0) + (zombie.workSpeed * deltaSeconds);
|
||||||
|
|
||||||
|
// Complete task
|
||||||
|
if (task.progress >= task.requiredProgress) {
|
||||||
|
this.completeTask(zombie, task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
executeMineTask(zombie, task, deltaSeconds) {
|
||||||
|
this.moveTowardsTarget(zombie, task.targetX, task.targetY);
|
||||||
|
|
||||||
|
const distance = Phaser.Math.Distance.Between(
|
||||||
|
zombie.x, zombie.y, task.targetX, task.targetY
|
||||||
|
);
|
||||||
|
|
||||||
|
if (distance < 20) {
|
||||||
|
task.progress = (task.progress || 0) + (zombie.workSpeed * 0.8 * deltaSeconds);
|
||||||
|
|
||||||
|
if (task.progress >= task.requiredProgress) {
|
||||||
|
this.completeTask(zombie, task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
executeGatherTask(zombie, task, deltaSeconds) {
|
||||||
|
this.moveTowardsTarget(zombie, task.targetX, task.targetY);
|
||||||
|
|
||||||
|
const distance = Phaser.Math.Distance.Between(
|
||||||
|
zombie.x, zombie.y, task.targetX, task.targetY
|
||||||
|
);
|
||||||
|
|
||||||
|
if (distance < 30) {
|
||||||
|
task.progress = (task.progress || 0) + (zombie.workSpeed * 1.2 * deltaSeconds);
|
||||||
|
|
||||||
|
if (task.progress >= task.requiredProgress) {
|
||||||
|
this.completeTask(zombie, task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
executeGuardTask(zombie, task, deltaSeconds) {
|
||||||
|
// Patrol area
|
||||||
|
if (!task.patrolTarget || zombie.reachedPatrol) {
|
||||||
|
task.patrolTarget = {
|
||||||
|
x: task.centerX + Phaser.Math.Between(-task.radius, task.radius),
|
||||||
|
y: task.centerY + Phaser.Math.Between(-task.radius, task.radius)
|
||||||
|
};
|
||||||
|
zombie.reachedPatrol = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.moveTowardsTarget(zombie, task.patrolTarget.x, task.patrolTarget.y);
|
||||||
|
|
||||||
|
const distance = Phaser.Math.Distance.Between(
|
||||||
|
zombie.x, zombie.y, task.patrolTarget.x, task.patrolTarget.y
|
||||||
|
);
|
||||||
|
|
||||||
|
if (distance < 10) {
|
||||||
|
zombie.reachedPatrol = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for enemies
|
||||||
|
this.detectEnemies(zombie, task.radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
completeTask(zombie, task) {
|
||||||
|
// Award XP
|
||||||
|
const xpGain = task.xpReward || 20;
|
||||||
|
this.addXP(zombie, xpGain);
|
||||||
|
|
||||||
|
// Increase loyalty
|
||||||
|
zombie.loyalty = Math.min(100, zombie.loyalty + 1);
|
||||||
|
|
||||||
|
// Clear task
|
||||||
|
zombie.currentTask = null;
|
||||||
|
zombie.state = 'idle';
|
||||||
|
this.taskQueue.delete(zombie.id);
|
||||||
|
|
||||||
|
// Emit event
|
||||||
|
this.scene.events.emit('zombieTaskComplete', { zombie, task });
|
||||||
|
|
||||||
|
console.log(`✅ Zombie ${zombie.id} completed ${task.type} task (+${xpGain} XP)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== LEVELING & SPECIALIZATION =====
|
||||||
|
|
||||||
|
addXP(zombie, amount) {
|
||||||
|
zombie.xp += amount;
|
||||||
|
|
||||||
|
// Check for level up
|
||||||
|
while (zombie.xp >= zombie.xpToNextLevel) {
|
||||||
|
this.levelUp(zombie);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
levelUp(zombie) {
|
||||||
|
zombie.level++;
|
||||||
|
zombie.xp -= zombie.xpToNextLevel;
|
||||||
|
zombie.xpToNextLevel = Math.floor(zombie.xpToNextLevel * 1.5);
|
||||||
|
|
||||||
|
// Increase stats
|
||||||
|
zombie.maxHP += 10;
|
||||||
|
zombie.hp = zombie.maxHP;
|
||||||
|
zombie.maxStamina += 10;
|
||||||
|
zombie.stamina = zombie.maxStamina;
|
||||||
|
zombie.workSpeed += 0.1;
|
||||||
|
|
||||||
|
// Apply specialization bonuses
|
||||||
|
if (zombie.specialization) {
|
||||||
|
const spec = this.skillTrees[zombie.specialization];
|
||||||
|
const levelBonuses = spec.levelBonuses[zombie.level];
|
||||||
|
|
||||||
|
if (levelBonuses) {
|
||||||
|
Object.assign(zombie, levelBonuses);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notification
|
||||||
|
this.scene.uiSystem?.showNotification({
|
||||||
|
title: '🧟 LEVEL UP!',
|
||||||
|
message: `Zombie ${zombie.id} reached level ${zombie.level}!`,
|
||||||
|
icon: 'zombie',
|
||||||
|
duration: 3000,
|
||||||
|
color: '#FFD700'
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`🆙 Zombie ${zombie.id} leveled up to ${zombie.level}!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
specializeZombie(zombieId, specialization) {
|
||||||
|
const zombie = this.zombies.get(zombieId);
|
||||||
|
if (!zombie) return false;
|
||||||
|
|
||||||
|
if (zombie.specialization) {
|
||||||
|
this.scene.uiSystem?.showError('Already specialized!');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zombie.level < 3) {
|
||||||
|
this.scene.uiSystem?.showError('Zombie must be level 3+ to specialize');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const spec = this.skillTrees[specialization];
|
||||||
|
zombie.specialization = specialization;
|
||||||
|
zombie.sprite.setTint(spec.color);
|
||||||
|
|
||||||
|
this.scene.uiSystem?.showNotification({
|
||||||
|
title: '⚡ Specialization Unlocked!',
|
||||||
|
message: `Zombie is now a ${spec.name}!`,
|
||||||
|
icon: 'zombie',
|
||||||
|
duration: 4000,
|
||||||
|
color: '#00FFFF'
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`⚡ Zombie ${zombie.id} specialized as ${spec.name}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== DECAY SYSTEM =====
|
||||||
|
|
||||||
|
updateDecay(zombie, delta) {
|
||||||
|
if (zombie.state === 'resting') return; // No decay when resting
|
||||||
|
|
||||||
|
const deltaSeconds = delta / 1000;
|
||||||
|
|
||||||
|
// Drain decay timer
|
||||||
|
zombie.decayTimer -= deltaSeconds * 1000;
|
||||||
|
|
||||||
|
// If timer expired, start losing HP
|
||||||
|
if (zombie.decayTimer <= 0) {
|
||||||
|
zombie.hp -= zombie.decayRate * deltaSeconds;
|
||||||
|
|
||||||
|
// Death check
|
||||||
|
if (zombie.hp <= 0) {
|
||||||
|
this.zombieDeath(zombie);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
zombieDeath(zombie) {
|
||||||
|
console.log(`💀 Zombie ${zombie.id} has decayed completely`);
|
||||||
|
|
||||||
|
// Drop fertilizer (good for farming!)
|
||||||
|
if (this.scene.farmingSystem) {
|
||||||
|
this.scene.farmingSystem.spawnFertilizer(zombie.x, zombie.y, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Award player XP
|
||||||
|
if (this.scene.player) {
|
||||||
|
this.scene.player.addXP?.(zombie.level * 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove zombie
|
||||||
|
zombie.sprite?.destroy();
|
||||||
|
zombie.healthBar?.destroy();
|
||||||
|
this.zombies.delete(zombie.id);
|
||||||
|
this.tamedZombies.delete(zombie.id);
|
||||||
|
|
||||||
|
// Notification
|
||||||
|
this.scene.uiSystem?.showNotification({
|
||||||
|
title: '💀 Zombie Decayed',
|
||||||
|
message: `Zombie turned to fertilizer (+${zombie.level * 10} XP)`,
|
||||||
|
icon: 'zombie',
|
||||||
|
duration: 3000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== GRAVE SYSTEM =====
|
||||||
|
|
||||||
|
createGrave(x, y) {
|
||||||
|
const graveId = `${x}_${y}`;
|
||||||
|
|
||||||
|
if (this.graves.has(graveId)) {
|
||||||
|
this.scene.uiSystem?.showError('Grave already exists here!');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if player has materials
|
||||||
|
if (this.scene.recipeSystem) {
|
||||||
|
const canCraft = this.scene.recipeSystem.canCraft('grave');
|
||||||
|
if (!canCraft.canCraft) {
|
||||||
|
this.scene.uiSystem?.showError(canCraft.reason);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const grave = {
|
||||||
|
id: graveId,
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
occupied: false,
|
||||||
|
zombieId: null,
|
||||||
|
sprite: this.scene.add.sprite(x, y, 'gravestone')
|
||||||
|
};
|
||||||
|
|
||||||
|
this.graves.set(graveId, grave);
|
||||||
|
|
||||||
|
console.log(`⚰️ Created grave at (${x}, ${y})`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
zombieRest(zombieId, graveId) {
|
||||||
|
const zombie = this.zombies.get(zombieId);
|
||||||
|
const grave = this.graves.get(graveId);
|
||||||
|
|
||||||
|
if (!zombie || !grave) return false;
|
||||||
|
|
||||||
|
if (grave.occupied) {
|
||||||
|
this.scene.uiSystem?.showError('Grave is occupied!');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move zombie to grave
|
||||||
|
zombie.state = 'resting';
|
||||||
|
zombie.sprite.setPosition(grave.x, grave.y);
|
||||||
|
zombie.sprite.setAlpha(0.5); // Semi-transparent
|
||||||
|
|
||||||
|
grave.occupied = true;
|
||||||
|
grave.zombieId = zombieId;
|
||||||
|
|
||||||
|
console.log(`😴 Zombie ${zombieId} resting in grave`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateResting(zombie, delta) {
|
||||||
|
const deltaSeconds = delta / 1000;
|
||||||
|
|
||||||
|
// Regenerate stamina (2x faster in grave)
|
||||||
|
zombie.stamina = Math.min(
|
||||||
|
zombie.maxStamina,
|
||||||
|
zombie.stamina + this.STAMINA_REGEN_RATE * this.GRAVE_REST_BONUS * deltaSeconds
|
||||||
|
);
|
||||||
|
|
||||||
|
// Slow HP regen
|
||||||
|
zombie.hp = Math.min(
|
||||||
|
zombie.maxHP,
|
||||||
|
zombie.hp + 2 * deltaSeconds
|
||||||
|
);
|
||||||
|
|
||||||
|
// Restore decay timer
|
||||||
|
zombie.decayTimer += 1000 * deltaSeconds; // Add 1 second per second resting
|
||||||
|
|
||||||
|
// If fully rested, wake up
|
||||||
|
if (zombie.stamina >= zombie.maxStamina) {
|
||||||
|
this.wakeUpZombie(zombie);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wakeUpZombie(zombie) {
|
||||||
|
zombie.state = 'idle';
|
||||||
|
zombie.sprite.setAlpha(1.0);
|
||||||
|
|
||||||
|
// Find and free grave
|
||||||
|
for (const [graveId, grave] of this.graves.entries()) {
|
||||||
|
if (grave.zombieId === zombie.id) {
|
||||||
|
grave.occupied = false;
|
||||||
|
grave.zombieId = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`😊 Zombie ${zombie.id} woke up refreshed!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== MOVEMENT & PATHFINDING =====
|
||||||
|
|
||||||
|
moveTowardsTarget(zombie, targetX, targetY) {
|
||||||
|
const angle = Phaser.Math.Angle.Between(zombie.x, zombie.y, targetX, targetY);
|
||||||
|
|
||||||
|
const velocityX = Math.cos(angle) * zombie.speed;
|
||||||
|
const velocityY = Math.sin(angle) * zombie.speed;
|
||||||
|
|
||||||
|
zombie.sprite.body.setVelocity(velocityX, velocityY);
|
||||||
|
|
||||||
|
// Update position
|
||||||
|
zombie.x = zombie.sprite.x;
|
||||||
|
zombie.y = zombie.sprite.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
detectEnemies(zombie, radius) {
|
||||||
|
// Guard zombies detect and attack enemies
|
||||||
|
if (!this.scene.enemies) return;
|
||||||
|
|
||||||
|
this.scene.enemies.getChildren().forEach(enemy => {
|
||||||
|
const distance = Phaser.Math.Distance.Between(
|
||||||
|
zombie.x, zombie.y, enemy.x, enemy.y
|
||||||
|
);
|
||||||
|
|
||||||
|
if (distance < radius) {
|
||||||
|
this.zombieAttack(zombie, enemy);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
zombieAttack(zombie, enemy) {
|
||||||
|
const spec = zombie.specialization === 'guard' ?
|
||||||
|
this.skillTrees.guard.levelBonuses[zombie.level] : null;
|
||||||
|
|
||||||
|
const damage = spec ? spec.damage : 10;
|
||||||
|
enemy.takeDamage?.(damage);
|
||||||
|
|
||||||
|
// Gain XP for combat
|
||||||
|
this.addXP(zombie, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== COMMANDS =====
|
||||||
|
|
||||||
|
commandFollow(zombieId) {
|
||||||
|
const zombie = this.zombies.get(zombieId);
|
||||||
|
if (!zombie) return;
|
||||||
|
|
||||||
|
zombie.currentTask = {
|
||||||
|
type: 'follow',
|
||||||
|
targetX: this.scene.player.x,
|
||||||
|
targetY: this.scene.player.y
|
||||||
|
};
|
||||||
|
zombie.state = 'following';
|
||||||
|
}
|
||||||
|
|
||||||
|
commandGuardArea(zombieId, x, y, radius = 100) {
|
||||||
|
this.assignTask(zombieId, {
|
||||||
|
type: 'guard',
|
||||||
|
centerX: x,
|
||||||
|
centerY: y,
|
||||||
|
radius: radius,
|
||||||
|
requiredProgress: Infinity // Never ends
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== HELPER FUNCTIONS =====
|
||||||
|
|
||||||
|
generateId() {
|
||||||
|
return `zombie_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
onZombieClicked(zombie) {
|
||||||
|
this.scene.events.emit('zombieSelected', { zombie });
|
||||||
|
// Show zombie info UI
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== GETTERS =====
|
||||||
|
|
||||||
|
getTamedZombies() {
|
||||||
|
return Array.from(this.tamedZombies).map(id => this.zombies.get(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
getWorkingZombies() {
|
||||||
|
return this.getTamedZombies().filter(z => z.state === 'working');
|
||||||
|
}
|
||||||
|
|
||||||
|
getIdleZombies() {
|
||||||
|
return this.getTamedZombies().filter(z => z.state === 'idle');
|
||||||
|
}
|
||||||
|
|
||||||
|
getZombiesBySpecialization(spec) {
|
||||||
|
return this.getTamedZombies().filter(z => z.specialization === spec);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== UPDATE =====
|
||||||
|
|
||||||
|
update(time, delta) {
|
||||||
|
for (const [id, zombie] of this.zombies.entries()) {
|
||||||
|
// Update based on state
|
||||||
|
switch (zombie.state) {
|
||||||
|
case 'working':
|
||||||
|
this.executeTask(zombie, delta);
|
||||||
|
this.updateDecay(zombie, delta);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'resting':
|
||||||
|
this.updateResting(zombie, delta);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'following':
|
||||||
|
this.moveTowardsTarget(zombie, this.scene.player.x, this.scene.player.y);
|
||||||
|
this.updateDecay(zombie, delta);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'idle':
|
||||||
|
case 'tamed':
|
||||||
|
this.updateDecay(zombie, delta);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'wild':
|
||||||
|
// Wild zombies wander
|
||||||
|
if (Math.random() < 0.01) {
|
||||||
|
const wanderX = zombie.x + Phaser.Math.Between(-50, 50);
|
||||||
|
const wanderY = zombie.y + Phaser.Math.Between(-50, 50);
|
||||||
|
this.moveTowardsTarget(zombie, wanderX, wanderY);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update health bar
|
||||||
|
if (zombie.healthBar && zombie.sprite) {
|
||||||
|
zombie.healthBar.x = zombie.sprite.x;
|
||||||
|
zombie.healthBar.y = zombie.sprite.y;
|
||||||
|
|
||||||
|
const healthPercent = zombie.hp / zombie.maxHP;
|
||||||
|
zombie.healthBar.clear();
|
||||||
|
zombie.healthBar.fillStyle(healthPercent > 0.5 ? 0x00FF00 : 0xFF0000);
|
||||||
|
zombie.healthBar.fillRect(-20, -30, 40 * healthPercent, 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regenerate Alfa scent slowly
|
||||||
|
this.alfaScent = Math.min(100, this.alfaScent + 0.01 * (delta / 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== CLEANUP =====
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
for (const zombie of this.zombies.values()) {
|
||||||
|
zombie.sprite?.destroy();
|
||||||
|
zombie.healthBar?.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const grave of this.graves.values()) {
|
||||||
|
grave.sprite?.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.zombies.clear();
|
||||||
|
this.wildZombies.clear();
|
||||||
|
this.tamedZombies.clear();
|
||||||
|
this.graves.clear();
|
||||||
|
this.taskQueue.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user