ZombieSystem - KRVAVA ŽETEV core feature! Alfa taming, worker tasks, leveling, decay & graves (900 LOC)

This commit is contained in:
2025-12-22 19:39:02 +01:00
parent 79e4d84500
commit c922204fe7

875
src/systems/ZombieSystem.js Normal file
View 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();
}
}