This commit is contained in:
2026-01-16 02:43:46 +01:00
parent bc2225ad64
commit 3ae8d39f9c
218 changed files with 87850 additions and 353 deletions

View File

@@ -0,0 +1,634 @@
/**
* BUILDING VISUALS & GENETICS SYSTEM
* Advanced animations for farm automation buildings and genetics lab
*/
class BuildingVisualsSystem {
constructor(scene) {
this.scene = scene;
this.enabled = true;
// Building animations
this.buildings = new Map();
this.conveyorBelts = [];
this.windmills = [];
this.silos = [];
// Genetics lab
this.geneticsLab = null;
this.dnaHelixAnimation = null;
this.mutationVats = [];
// Settings
this.settings = {
buildingAnimations: true,
particleEffects: true,
geneticsUI: true,
animationSpeed: 1.0
};
this.loadSettings();
this.init();
console.log('✅ Building Visuals System initialized');
}
init() {
console.log('🏭 Building animations ready');
}
// ========== AUTO-PLANTER ==========
createAutoPlanter(x, y) {
if (!this.settings.buildingAnimations) return null;
const planter = {
x, y,
arm: null,
seed: null,
isPlanting: false
};
// Create mechanical arm
const arm = this.scene.add.graphics();
arm.lineStyle(4, 0x888888, 1);
arm.lineBetween(0, 0, 0, 30); // Vertical arm
arm.lineBetween(0, 30, 20, 30); // Horizontal extension
arm.setPosition(x, y);
arm.setDepth(100);
planter.arm = arm;
this.buildings.set(`planter_${x}_${y}`, planter);
return planter;
}
animatePlanting(planter, targetX, targetY) {
if (!planter || planter.isPlanting) return;
planter.isPlanting = true;
// Arm extends down
this.scene.tweens.add({
targets: planter.arm,
y: planter.y + 20,
duration: 500,
ease: 'Power2',
onComplete: () => {
// Drop seed
this.createSeedParticle(targetX, targetY);
// Arm retracts
this.scene.tweens.add({
targets: planter.arm,
y: planter.y,
duration: 500,
ease: 'Power2',
onComplete: () => {
planter.isPlanting = false;
}
});
}
});
}
createSeedParticle(x, y) {
const seed = this.scene.add.circle(x, y - 20, 3, 0x8B4513);
seed.setDepth(99);
this.scene.tweens.add({
targets: seed,
y: y,
duration: 300,
ease: 'Bounce.easeOut',
onComplete: () => {
// Puff of dirt
if (this.scene.visualEnhancements) {
this.scene.visualEnhancements.createSparkleEffect(x, y);
}
seed.destroy();
}
});
}
// ========== AUTO-HARVESTER ==========
createAutoHarvester(x, y) {
if (!this.settings.buildingAnimations) return null;
const harvester = {
x, y,
blades: [],
isHarvesting: false,
rotation: 0
};
// Create spinning blades
for (let i = 0; i < 4; i++) {
const blade = this.scene.add.graphics();
blade.lineStyle(3, 0xC0C0C0, 1);
blade.lineBetween(0, 0, 15, 0);
blade.setPosition(x, y);
blade.setRotation((Math.PI / 2) * i);
blade.setDepth(100);
harvester.blades.push(blade);
}
this.buildings.set(`harvester_${x}_${y}`, harvester);
return harvester;
}
animateHarvesting(harvester, cropX, cropY) {
if (!harvester || harvester.isHarvesting) return;
harvester.isHarvesting = true;
// Spin blades
for (const blade of harvester.blades) {
this.scene.tweens.add({
targets: blade,
rotation: blade.rotation + Math.PI * 4,
duration: 1000,
ease: 'Linear'
});
}
// Move to crop
this.scene.tweens.add({
targets: harvester.blades,
x: cropX,
y: cropY,
duration: 500,
ease: 'Power2',
onComplete: () => {
// Harvest effect
this.createHarvestEffect(cropX, cropY);
// Return to base
this.scene.tweens.add({
targets: harvester.blades,
x: harvester.x,
y: harvester.y,
duration: 500,
ease: 'Power2',
onComplete: () => {
harvester.isHarvesting = false;
}
});
}
});
}
createHarvestEffect(x, y) {
// Crop particles
const emitter = this.scene.add.particles(x, y, 'particle_white', {
speed: { min: 30, max: 60 },
scale: { start: 0.5, end: 0 },
alpha: { start: 1, end: 0 },
lifespan: 800,
quantity: 10,
tint: 0xFFD700
});
this.scene.time.delayedCall(800, () => emitter.destroy());
}
// ========== CONVEYOR BELT ==========
createConveyorBelt(x, y, width, direction = 'right') {
if (!this.settings.buildingAnimations) return null;
const belt = {
x, y, width, direction,
items: [],
speed: 50 // pixels per second
};
// Create belt graphics
const graphics = this.scene.add.graphics();
graphics.fillStyle(0x444444, 1);
graphics.fillRect(x, y, width, 20);
// Moving lines to show direction
for (let i = 0; i < width; i += 20) {
const line = this.scene.add.rectangle(
x + i,
y + 10,
10,
2,
0x888888
);
line.setDepth(99);
// Animate line movement
const targetX = direction === 'right' ? x + width : x;
this.scene.tweens.add({
targets: line,
x: targetX,
duration: (width / belt.speed) * 1000,
repeat: -1,
ease: 'Linear'
});
}
belt.graphics = graphics;
this.conveyorBelts.push(belt);
return belt;
}
addItemToBelt(belt, itemSprite) {
if (!belt) return;
belt.items.push(itemSprite);
itemSprite.setPosition(belt.x, belt.y + 10);
itemSprite.setDepth(100);
// Move item along belt
const targetX = belt.direction === 'right'
? belt.x + belt.width
: belt.x;
this.scene.tweens.add({
targets: itemSprite,
x: targetX,
duration: (belt.width / belt.speed) * 1000,
ease: 'Linear',
onComplete: () => {
// Remove from belt
const index = belt.items.indexOf(itemSprite);
if (index > -1) {
belt.items.splice(index, 1);
}
}
});
}
// ========== WINDMILL ==========
createWindmill(x, y) {
if (!this.settings.buildingAnimations) return null;
const windmill = {
x, y,
blades: null,
powerGlow: null,
isPowered: false,
rotation: 0
};
// Create blades
const blades = this.scene.add.graphics();
blades.lineStyle(4, 0x8B4513, 1);
// Draw 4 blades
for (let i = 0; i < 4; i++) {
const angle = (Math.PI / 2) * i;
const endX = Math.cos(angle) * 30;
const endY = Math.sin(angle) * 30;
blades.lineBetween(0, 0, endX, endY);
}
blades.setPosition(x, y);
blades.setDepth(100);
windmill.blades = blades;
// Power glow
const glow = this.scene.add.circle(x, y, 40, 0x00ffff, 0);
glow.setBlendMode(Phaser.BlendModes.ADD);
glow.setDepth(99);
windmill.powerGlow = glow;
// Rotate blades
this.scene.tweens.add({
targets: blades,
rotation: Math.PI * 2,
duration: 3000 / this.settings.animationSpeed,
repeat: -1,
ease: 'Linear'
});
this.windmills.push(windmill);
return windmill;
}
setPowerState(windmill, powered) {
if (!windmill) return;
windmill.isPowered = powered;
if (powered) {
// Glow on
this.scene.tweens.add({
targets: windmill.powerGlow,
alpha: 0.4,
duration: 500
});
// Particle trail
if (this.settings.particleEffects) {
const emitter = this.scene.add.particles(
windmill.x,
windmill.y,
'particle_white',
{
speed: 20,
scale: { start: 0.3, end: 0 },
alpha: { start: 0.6, end: 0 },
lifespan: 1000,
frequency: 200,
quantity: 1,
tint: 0x00ffff
}
);
windmill.particleEmitter = emitter;
}
} else {
// Glow off
this.scene.tweens.add({
targets: windmill.powerGlow,
alpha: 0,
duration: 500
});
// Stop particles
if (windmill.particleEmitter) {
windmill.particleEmitter.destroy();
windmill.particleEmitter = null;
}
}
}
// ========== STORAGE SILO ==========
createStorageSilo(x, y, capacity = 1000) {
if (!this.settings.buildingAnimations) return null;
const silo = {
x, y,
capacity,
currentAmount: 0,
fillIndicator: null,
fillBar: null
};
// Silo structure
const structure = this.scene.add.graphics();
structure.fillStyle(0x666666, 1);
structure.fillRect(x - 20, y - 60, 40, 60);
structure.fillCircle(x, y - 60, 20);
structure.setDepth(100);
silo.structure = structure;
// Fill indicator (inside silo)
const fillBar = this.scene.add.rectangle(
x,
y,
36,
0,
0xFFD700,
0.8
);
fillBar.setOrigin(0.5, 1);
fillBar.setDepth(101);
silo.fillBar = fillBar;
// Percentage text
const text = this.scene.add.text(x, y - 70, '0%', {
fontSize: '12px',
color: '#ffffff',
fontStyle: 'bold'
});
text.setOrigin(0.5);
text.setDepth(102);
silo.text = text;
this.silos.push(silo);
return silo;
}
updateSiloFill(silo, amount) {
if (!silo) return;
silo.currentAmount = Phaser.Math.Clamp(amount, 0, silo.capacity);
const percentage = (silo.currentAmount / silo.capacity) * 100;
const fillHeight = (percentage / 100) * 56; // Max height
// Animate fill
this.scene.tweens.add({
targets: silo.fillBar,
height: fillHeight,
duration: 500,
ease: 'Power2'
});
// Update text
silo.text.setText(`${Math.round(percentage)}%`);
}
// ========== GENETICS LAB ==========
createGeneticsLab(x, y) {
if (!this.settings.geneticsUI) return null;
const lab = {
x, y,
dnaHelix: null,
vats: [],
isActive: false
};
// Lab structure
const structure = this.scene.add.rectangle(x, y, 80, 60, 0x333333);
structure.setStrokeStyle(2, 0x00ff00);
structure.setDepth(100);
lab.structure = structure;
this.geneticsLab = lab;
return lab;
}
showDNAHelixAnimation(x, y) {
if (!this.settings.geneticsUI) return null;
// Create DNA helix
const helix = this.scene.add.container(x, y);
helix.setDepth(200);
// Two strands
const strand1 = [];
const strand2 = [];
for (let i = 0; i < 20; i++) {
const yPos = i * 10 - 100;
const angle = (i / 20) * Math.PI * 4;
const x1 = Math.cos(angle) * 15;
const x2 = Math.cos(angle + Math.PI) * 15;
const dot1 = this.scene.add.circle(x1, yPos, 3, 0x00ff00);
const dot2 = this.scene.add.circle(x2, yPos, 3, 0x00ffff);
helix.add(dot1);
helix.add(dot2);
strand1.push(dot1);
strand2.push(dot2);
// Connecting lines
if (i % 3 === 0) {
const line = this.scene.add.graphics();
line.lineStyle(1, 0xffffff, 0.5);
line.lineBetween(x1, yPos, x2, yPos);
helix.add(line);
}
}
// Rotate helix
this.scene.tweens.add({
targets: helix,
rotation: Math.PI * 2,
duration: 4000,
repeat: -1,
ease: 'Linear'
});
// Pulse effect
this.scene.tweens.add({
targets: helix,
scale: { from: 1, to: 1.1 },
duration: 2000,
yoyo: true,
repeat: -1
});
this.dnaHelixAnimation = helix;
return helix;
}
hideDNAHelixAnimation() {
if (this.dnaHelixAnimation) {
this.dnaHelixAnimation.destroy();
this.dnaHelixAnimation = null;
}
}
createMutationVat(x, y) {
if (!this.settings.geneticsUI) return null;
const vat = {
x, y,
container: null,
liquid: null,
bubbles: null,
isActive: false
};
// Vat container
const container = this.scene.add.graphics();
container.fillStyle(0x444444, 1);
container.fillRect(x - 20, y - 40, 40, 40);
container.lineStyle(2, 0x666666);
container.strokeRect(x - 20, y - 40, 40, 40);
container.setDepth(100);
vat.container = container;
// Liquid
const liquid = this.scene.add.rectangle(
x,
y - 20,
36,
36,
0x00ff00,
0.6
);
liquid.setDepth(101);
vat.liquid = liquid;
this.mutationVats.push(vat);
return vat;
}
activateMutationVat(vat) {
if (!vat || vat.isActive) return;
vat.isActive = true;
// Bubbling effect
const emitter = this.scene.add.particles(
vat.x,
vat.y - 40,
'particle_white',
{
speedY: { min: -30, max: -50 },
speedX: { min: -5, max: 5 },
scale: { start: 0.2, end: 0 },
alpha: { start: 0.8, end: 0 },
lifespan: 1000,
frequency: 200,
quantity: 2,
tint: 0x00ff00
}
);
vat.bubbles = emitter;
// Lightning effect
if (this.settings.particleEffects) {
this.createLightningEffect(vat.x, vat.y - 60);
}
// Liquid glow pulse
this.scene.tweens.add({
targets: vat.liquid,
alpha: { from: 0.6, to: 0.9 },
duration: 800,
yoyo: true,
repeat: -1
});
}
createLightningEffect(x, y) {
const lightning = this.scene.add.graphics();
lightning.lineStyle(2, 0xffff00, 1);
// Zigzag lightning
let currentX = x;
let currentY = y;
for (let i = 0; i < 5; i++) {
const nextX = currentX + (Math.random() - 0.5) * 20;
const nextY = currentY + 10;
lightning.lineBetween(currentX, currentY, nextX, nextY);
currentX = nextX;
currentY = nextY;
}
lightning.setDepth(200);
// Flash and fade
this.scene.tweens.add({
targets: lightning,
alpha: 0,
duration: 200,
onComplete: () => lightning.destroy()
});
}
// ========== SETTINGS ==========
saveSettings() {
localStorage.setItem('novafarma_building_visuals', JSON.stringify(this.settings));
}
loadSettings() {
const saved = localStorage.getItem('novafarma_building_visuals');
if (saved) {
this.settings = { ...this.settings, ...JSON.parse(saved) };
}
}
destroy() {
if (this.dnaHelixAnimation) this.dnaHelixAnimation.destroy();
for (const vat of this.mutationVats) {
if (vat.bubbles) vat.bubbles.destroy();
}
console.log('🏭 Building Visuals System destroyed');
}
}

View File

@@ -0,0 +1,313 @@
// 🎬 INTRO SEQUENCE - 60-SECOND EPIC CINEMATIC
// "From Colors to Darkness" - Complete Story
// Created: January 10, 2026
// Style: Style 32 Dark-Chibi Noir + Polaroid + VHS
// Voices: Kai (Rok) + Ana (Petra) + Gronk (Rok deep)
class IntroScene extends Phaser.Scene {
constructor() {
super({ key: 'IntroScene' });
this.skipEnabled = false;
this.currentPhase = 0;
this.skipPrompt = null;
this.currentPolaroid = null;
this.currentText = null;
this.vhsNoise = null;
this.scanlines = null;
this.ambientAudio = null;
this.currentVoice = null;
}
preload() {
console.log('🎬 IntroScene: Loading EPIC 60s assets...');
// Base path for intro shots
const introPath = 'assets/references/intro_shots/';
// ALL 20 INTRO SHOTS
// DREAMY INTRO ASSETS (Generated)
const dreamyPath = 'assets/images/intro_sequence/';
this.load.image('intro_family_portrait', dreamyPath + 'family_portrait_complete_dreamy.png');
this.load.image('intro_otac_longboard', dreamyPath + 'otac_longboard_pier_dreamy.png');
this.load.image('intro_kai_dreads', dreamyPath + 'kai_first_dreads_family_dreamy.png');
this.load.image('intro_ana_barbershop', dreamyPath + 'ana_barbershop_dreads_dreamy.png');
this.load.image('intro_birthday_cake', dreamyPath + 'birthday_cake_rd_dreamy.png');
this.load.image('intro_virus', introPath + 'virus_xnoir_microscope.png');
this.load.image('intro_chaos', dreamyPath + 'chaos_streets_apocalypse_dreamy.png');
this.load.image('intro_zombies', dreamyPath + 'zombie_silhouettes_panic_dreamy.png');
this.load.image('intro_parents_ghosts', dreamyPath + 'parents_transparent_ghosts_dreamy.png');
this.load.image('intro_ana_taken', introPath + 'ana_taken_military.png');
this.load.image('intro_kai_alone', introPath + 'kai_alone_basement.png');
this.load.image('intro_kai_young', introPath + 'kai_young_timelapse.png');
this.load.image('intro_kai_adult', introPath + 'kai_adult_35_timelapse.png');
this.load.image('intro_kai_elder', introPath + 'kai_elder_50_timelapse.png');
this.load.image('intro_ana_memory', introPath + 'ana_memory_flash_purple.png');
this.load.image('intro_bedroom', introPath + 'kai_bedroom_wakeup.png');
this.load.image('intro_gronk', introPath + 'gronk_doorway_silhouette.png');
this.load.image('intro_twins_childhood', introPath + 'kai_ana_twins_childhood.png');
// 🎵 AMBIENT MUSIC
this.loadAudioSafe('noir_ambience', 'assets/audio/ambient/noir_ambience.mp3');
// 🎤 KAI VOICES (12 total - ENGLISH)
this.loadAudioSafe('kai_01', 'assets/audio/voiceover/kai_en_01.mp3');
this.loadAudioSafe('kai_02', 'assets/audio/voiceover/kai_en_02.mp3');
this.loadAudioSafe('kai_03', 'assets/audio/voiceover/kai_en_03.mp3');
this.loadAudioSafe('kai_04', 'assets/audio/voiceover/kai_en_04.mp3');
this.loadAudioSafe('kai_05', 'assets/audio/voiceover/kai_en_05.mp3');
this.loadAudioSafe('kai_06', 'assets/audio/voiceover/kai_en_06.mp3');
this.loadAudioSafe('kai_07', 'assets/audio/voiceover/kai_en_07.mp3');
this.loadAudioSafe('kai_08', 'assets/audio/voiceover/kai_en_08.mp3');
this.loadAudioSafe('kai_09', 'assets/audio/voiceover/kai_en_09.mp3');
this.loadAudioSafe('kai_10', 'assets/audio/voiceover/kai_en_10.mp3');
this.loadAudioSafe('kai_11', 'assets/audio/voiceover/kai_en_11.mp3');
this.loadAudioSafe('kai_12', 'assets/audio/voiceover/kai_en_12.mp3');
// 🎤 ANA VOICES (8 total - ENGLISH)
this.loadAudioSafe('ana_01', 'assets/audio/voiceover/ana_en_01.mp3');
this.loadAudioSafe('ana_02', 'assets/audio/voiceover/ana_en_02.mp3');
this.loadAudioSafe('ana_03', 'assets/audio/voiceover/ana_en_03.mp3');
this.loadAudioSafe('ana_04', 'assets/audio/voiceover/ana_en_04.mp3');
this.loadAudioSafe('ana_05', 'assets/audio/voiceover/ana_en_05.mp3');
this.loadAudioSafe('ana_06', 'assets/audio/voiceover/ana_en_06.mp3');
this.loadAudioSafe('ana_07', 'assets/audio/voiceover/ana_en_07.mp3');
this.loadAudioSafe('ana_08', 'assets/audio/voiceover/ana_en_08.mp3');
// 🎤 GRONK VOICE (ENGLISH - Deep UK)
this.loadAudioSafe('gronk_01', 'assets/audio/voiceover/gronk_en_01.mp3');
}
loadAudioSafe(key, path) {
try {
this.load.audio(key, path);
} catch (error) {
console.warn(`⚠️ Audio skipped: ${key}`);
}
}
create() {
console.log('🎬 IntroScene: Amnesia Start Sequence (Fixed Timing)');
this.cameras.main.setBackgroundColor('#000000');
// 1. SHADER INTEGRATION: Gaussian Blur
this.blurFX = null;
if (this.cameras.main.postFX) {
try {
this.blurFX = this.cameras.main.postFX.addBlur(0, 0, 0);
} catch (e) {
console.warn('⚠️ PostFX not supported');
}
}
// Initialize Blur at 20
if (this.blurFX) {
this.blurFX.strength = 20; // High blur
// Tween blur to 0 over 6 seconds (Standard Time)
this.tweens.add({
targets: this.blurFX,
strength: 0,
duration: 6000,
ease: 'Power2.easeOut',
onComplete: () => {
this.triggerWakeUp();
}
});
} else {
// Fallback if shaders not supported
this.cameras.main.alpha = 0;
this.tweens.add({
targets: this.cameras.main,
alpha: 1,
duration: 6000,
onComplete: () => this.triggerWakeUp()
});
}
// 2. FLASHBACK ENGINE (Safe Mode)
// Images: Birthday (0s), Longboard (1.5s), Dreads (3.0s)
this.flashbackSequence([
'intro_birthday_cake',
'intro_otac_longboard',
'intro_ana_barbershop'
]);
// 3. TYPEWRITER LOGIC (Fixed Timing & No Overlap)
// Line 1: 0.5s -> 3.0s
this.time.delayedCall(500, () => {
this.showDialogue('Vse je zamegljeno... Zakaj me vse boli?', 2500);
});
// Line 2: 4.0s -> End (starts only after Line 1 is cleared)
this.time.delayedCall(4000, () => {
this.showDialogue('Kdo so ti ljudje v moji glavi?', 2000);
});
// Audio atmosphere
this.startAmbientAudio();
}
flashbackSequence(images) {
images.forEach((key, index) => {
// Check if asset exists before scheduling
if (this.textures.exists(key)) {
this.time.delayedCall(index * 1500, () => {
this.triggerFlashback(key);
});
} else {
console.warn(`⚠️ Missing flash asset: ${key} - Skipping visual, keeping timing.`);
}
});
}
triggerFlashback(key) {
// Double check existence
if (!this.textures.exists(key)) return;
const width = this.cameras.main.width;
const height = this.cameras.main.height;
const image = this.add.image(width / 2, height / 2, key);
// Scale to cover most of screen (maintain aspect ratio)
const scale = Math.max(width / image.width, height / image.height) * 0.8;
image.setScale(scale);
image.setAlpha(0);
image.setDepth(10);
image.setBlendMode(Phaser.BlendModes.ADD);
// Flash in and out
this.tweens.add({
targets: image,
alpha: { from: 0, to: 0.15 },
duration: 500,
yoyo: true,
hold: 500,
onComplete: () => {
if (image && image.active) image.destroy();
}
});
// Zoom
this.tweens.add({
targets: image,
scale: scale * 1.1,
duration: 1500
});
}
showDialogue(text, duration) {
// OVERLAP FIX: Destroy previous text immediately
if (this.currentText) {
this.currentText.destroy();
this.currentText = null;
}
const width = this.cameras.main.width;
const height = this.cameras.main.height;
const textObj = this.add.text(width / 2, height - 100, '', {
fontFamily: 'Courier New',
fontSize: '24px',
fill: '#ffffff',
align: 'center',
stroke: '#000000',
strokeThickness: 4
});
textObj.setOrigin(0.5);
textObj.setDepth(100);
this.currentText = textObj; // Track current text
// Typewriter
let charIndex = 0;
const speed = 50;
if (text.length * speed > duration) {
// Speed up if text is too long for duration
speed = Math.floor(duration / text.length);
}
const timer = this.time.addEvent({
delay: speed,
callback: () => {
if (!textObj.active) return;
textObj.text += text[charIndex];
charIndex++;
if (charIndex >= text.length) {
timer.remove();
// Fade out logic
this.time.delayedCall(Math.max(500, duration - (text.length * speed)), () => {
if (textObj.active) {
this.tweens.add({
targets: textObj,
alpha: 0,
duration: 500,
onComplete: () => {
if (textObj.active) textObj.destroy();
if (this.currentText === textObj) this.currentText = null;
}
});
}
});
}
},
repeat: text.length - 1
});
}
startAmbientAudio() {
try {
if (this.cache.audio.exists('noir_ambience')) {
this.ambientAudio = this.sound.add('noir_ambience', {
volume: 0.1, // Start very quiet
loop: true
});
this.ambientAudio.play();
// Fade in audio
this.tweens.add({
targets: this.ambientAudio,
volume: 0.4,
duration: 6000
});
}
} catch (e) {
console.warn('⚠️ Ambient audio not available');
}
}
triggerWakeUp() {
console.log('⚡ TRANSITION: Wake Up!');
// 4. TRANSITION TO GAMEPLAY
// White Flash
this.cameras.main.flash(1000, 255, 255, 255);
// Stop audio
if (this.ambientAudio) {
this.tweens.add({
targets: this.ambientAudio,
volume: 0,
duration: 500,
onComplete: () => {
if (this.ambientAudio && this.ambientAudio.stop) {
this.ambientAudio.stop();
}
}
});
}
// Switch Scene
this.time.delayedCall(1000, () => {
// Set flag
if (window.gameState && window.gameState.story) {
window.gameState.story.isAmnesiaActive = true;
}
this.scene.start('GameScene');
});
}
}

View File

@@ -0,0 +1,358 @@
// 🌱 MICRO FARM SYSTEM - Phase 37
// Začetna 8x8 parcela z postopno širitvijo
class MicroFarmSystem {
constructor(scene) {
this.scene = scene;
// MICRO FARM CONFIG
this.farmCenterX = 50; // Center of 100x100 map
this.farmCenterY = 50;
this.farmSize = 8; // 8x8 tiles (initial)
// EXPANSION SYSTEM
this.unlockedTiles = new Set(); // Tracks unlocked tiles
this.expansionCost = 50; // Gold per 2x2 expansion
// LAND TYPES
this.landTypes = {
GRASS: 'grass', // Free to use
FOREST: 'forest', // Needs clearing (trees)
ROCKY: 'rocky', // Needs mining (rocks)
SWAMP: 'swamp' // Needs drainage (water)
};
this.init();
}
init() {
console.log('🌱 MicroFarmSystem initialized');
// Unlock initial 8x8 farm
this.unlockInitialFarm();
// Create visual boundaries
this.createFarmBoundaries();
// Render locked tile overlay
this.renderLockedTileOverlay();
// Create expansion UI buttons
this.createExpansionUI();
}
createExpansionUI() {
// Create UI buttons for farm expansion
const uiScene = this.scene.scene.get('UIScene');
if (!uiScene) {
console.warn('⚠️ UIScene not found - cannot create expansion UI');
return;
}
// Store reference
this.uiScene = uiScene;
this.expansionButtons = [];
const buttonSize = 40;
const buttonColor = 0x8B4513; // Brown
const buttonHoverColor = 0xD2691E;
const expandCost = this.expansionCost;
// Button positions relative to farm center
const tileSize = (this.scene.terrainSystem && this.scene.terrainSystem.tileSize) || 48;
const farmWorldX = this.farmCenterX * tileSize;
const farmWorldY = this.farmCenterY * tileSize;
const farmPixelSize = this.farmSize * tileSize;
const halfSize = farmPixelSize / 2;
const buttons = [
{ dir: 'north', x: farmWorldX, y: farmWorldY - halfSize - 60, icon: '⬆️' },
{ dir: 'south', x: farmWorldX, y: farmWorldY + halfSize + 60, icon: '⬇️' },
{ dir: 'east', x: farmWorldX + halfSize + 60, y: farmWorldY, icon: '➡️' },
{ dir: 'west', x: farmWorldX - halfSize - 60, y: farmWorldY, icon: '⬅️' }
];
buttons.forEach(btn => {
// Button background
const bg = this.scene.add.rectangle(btn.x, btn.y, buttonSize, buttonSize, buttonColor);
bg.setStrokeStyle(2, 0xFFFFFF);
bg.setDepth(10);
bg.setInteractive({ useHandCursor: true });
// Button text
const text = this.scene.add.text(btn.x, btn.y, btn.icon, {
fontSize: '20px',
color: '#ffffff'
}).setOrigin(0.5);
text.setDepth(11);
// Cost label
const costLabel = this.scene.add.text(btn.x, btn.y + 30, `${expandCost}g`, {
fontSize: '12px',
color: '#FFD700',
fontStyle: 'bold'
}).setOrigin(0.5);
costLabel.setDepth(11);
// Hover effects
bg.on('pointerover', () => {
bg.setFillStyle(buttonHoverColor);
bg.setScale(1.1);
});
bg.on('pointerout', () => {
bg.setFillStyle(buttonColor);
bg.setScale(1.0);
});
// Click handler
bg.on('pointerdown', () => {
this.tryExpandFarm(btn.dir);
});
this.expansionButtons.push({ bg, text, costLabel, dir: btn.dir });
});
console.log('✅ Expansion UI created!');
}
tryExpandFarm(direction) {
// Check if player has enough gold
const inv = this.scene.inventorySystem;
if (!inv || inv.gold < this.expansionCost) {
console.log(`❌ Not enough gold! Need ${this.expansionCost}g`);
if (this.scene.events) {
this.scene.events.emit('show-floating-text', {
x: this.scene.cameras.main.width / 2,
y: 100,
text: `Need ${this.expansionCost} gold!`,
color: '#ff0000'
});
}
return;
}
// Deduct gold
inv.gold -= this.expansionCost;
// Expand farm
this.expandFarm(direction);
// Success feedback
if (this.scene.events) {
this.scene.events.emit('show-floating-text', {
x: this.scene.cameras.main.width / 2,
y: 100,
text: `✅ Farm expanded ${direction.toUpperCase()}!`,
color: '#00ff00'
});
}
console.log(`✅ Farm expanded ${direction}! (-${this.expansionCost}g)`);
}
unlockInitialFarm() {
// Unlock central 8x8 tiles
const halfSize = Math.floor(this.farmSize / 2);
for (let y = this.farmCenterY - halfSize; y < this.farmCenterY + halfSize; y++) {
for (let x = this.farmCenterX - halfSize; x < this.farmCenterX + halfSize; x++) {
const tileKey = `${x},${y}`;
this.unlockedTiles.add(tileKey);
}
}
console.log(`✅ Unlocked ${this.unlockedTiles.size} tiles (8x8 micro farm)`);
}
createFarmBoundaries() {
// Clear previous if exists
if (this.boundaryGraphics) {
this.boundaryGraphics.destroy();
}
const graphics = this.scene.add.graphics();
const tileSize = (this.scene.terrainSystem && this.scene.terrainSystem.tileSize) || 48;
// 🏗️ 1. LARGE MASTER PLATFORM (32x32)
const largeSize = 32;
const largeHalf = largeSize / 2;
const lx1 = (this.farmCenterX - largeHalf) * tileSize;
const ly1 = (this.farmCenterY - largeHalf) * tileSize;
const lWidth = largeSize * tileSize;
const lHeight = largeSize * tileSize;
// Draw Master Platform
graphics.fillStyle(0x0000FF, 0.05); // Very subtle blue for "Large Platform"
graphics.fillRect(lx1, ly1, lWidth, lHeight);
graphics.lineStyle(2, 0x0000FF, 0.3);
graphics.strokeRect(lx1, ly1, lWidth, lHeight);
// 🏗️ 2. SMALL MICRO FARM PLATFORM (8x8)
const halfSize = Math.floor(this.farmSize / 2);
const startX = (this.farmCenterX - halfSize) * tileSize;
const startY = (this.farmCenterY - halfSize) * tileSize;
const width = this.farmSize * tileSize;
const height = this.farmSize * tileSize;
// Draw Highlight
graphics.fillStyle(0xFFFFFF, 0.15); // 15% white for "Starter Platform"
graphics.fillRect(startX, startY, width, height);
// Draw Bold Boundary
graphics.lineStyle(4, 0x00FF00, 0.8); // High vis green
graphics.strokeRect(startX, startY, width, height);
// Corner markers
graphics.fillStyle(0xFFFF00, 1);
const markerSize = 12;
graphics.fillCircle(startX, startY, markerSize);
graphics.fillCircle(startX + width, startY, markerSize);
graphics.fillCircle(startX, startY + height, markerSize);
graphics.fillCircle(startX + width, startY + height, markerSize);
graphics.setDepth(50);
this.boundaryGraphics = graphics;
console.log(`🏰 Dual platforms rendered: Master (${largeSize}x${largeSize}) & Micro (${this.farmSize}x${this.farmSize})`);
}
renderLockedTileOverlay() {
// Render dark overlay on locked tiles
if (this.lockedOverlayGraphics) {
this.lockedOverlayGraphics.destroy();
}
this.lockedOverlayGraphics = this.scene.add.graphics();
this.lockedOverlayGraphics.setDepth(4); // Above ground, below boundaries
this.lockedOverlayGraphics.clear();
// Darken all tiles that are NOT unlocked
const halfSize = Math.floor(this.farmSize / 2);
const farmStartX = this.farmCenterX - halfSize;
const farmEndX = this.farmCenterX + halfSize;
const farmStartY = this.farmCenterY - halfSize;
const farmEndY = this.farmCenterY + halfSize;
// Render grid of locked tiles
const viewRange = 15; // Show some area around farm
for (let y = this.farmCenterY - viewRange; y < this.farmCenterY + viewRange; y++) {
for (let x = this.farmCenterX - viewRange; x < this.farmCenterX + viewRange; x++) {
// Skip if within farm boundaries
if (x >= farmStartX && x < farmEndX && y >= farmStartY && y < farmEndY) {
continue;
}
// Draw dark overlay (lighter)
const tileSize = (this.scene.terrainSystem && this.scene.terrainSystem.tileSize) || 48;
const worldX = x * tileSize;
const worldY = y * tileSize;
this.lockedOverlayGraphics.fillStyle(0x000000, 0.3); // 0.5 -> 0.3
this.lockedOverlayGraphics.fillRect(worldX, worldY, tileSize, tileSize);
}
}
console.log('🔒 Locked tile overlay rendered!');
}
isTileUnlocked(tileX, tileY) {
const tileKey = `${tileX},${tileY}`;
return this.unlockedTiles.has(tileKey);
}
canExpand(direction) {
// Check if expansion in direction is possible
// direction: 'north', 'south', 'east', 'west'
// TODO: Implement expansion logic
return true;
}
expandFarm(direction) {
// Unlock 2x2 tiles in specified direction
const halfSize = Math.floor(this.farmSize / 2);
const expansionSize = 2; // Unlock 2x2 tiles at a time
let newTiles = [];
switch (direction) {
case 'north':
for (let y = this.farmCenterY - halfSize - expansionSize; y < this.farmCenterY - halfSize; y++) {
for (let x = this.farmCenterX - halfSize; x < this.farmCenterX + halfSize; x++) {
newTiles.push({ x, y });
}
}
break;
case 'south':
for (let y = this.farmCenterY + halfSize; y < this.farmCenterY + halfSize + expansionSize; y++) {
for (let x = this.farmCenterX - halfSize; x < this.farmCenterX + halfSize; x++) {
newTiles.push({ x, y });
}
}
break;
case 'east':
for (let y = this.farmCenterY - halfSize; y < this.farmCenterY + halfSize; y++) {
for (let x = this.farmCenterX + halfSize; x < this.farmCenterX + halfSize + expansionSize; x++) {
newTiles.push({ x, y });
}
}
break;
case 'west':
for (let y = this.farmCenterY - halfSize; y < this.farmCenterY + halfSize; y++) {
for (let x = this.farmCenterX - halfSize - expansionSize; x < this.farmCenterX - halfSize; x++) {
newTiles.push({ x, y });
}
}
break;
}
// Unlock the tiles
newTiles.forEach(tile => {
const tileKey = `${tile.x},${tile.y}`;
this.unlockedTiles.add(tileKey);
});
// Update farm size
if (direction === 'north' || direction === 'south') {
this.farmSize += expansionSize;
} else {
this.farmSize += expansionSize;
}
// Update visuals
this.createFarmBoundaries(); // Redraw boundaries
this.renderLockedTileOverlay(); // Update overlay
console.log(`🔓 Expanded farm ${direction}! +${newTiles.length} tiles. Total: ${this.unlockedTiles.size}`);
}
getLandType(tileX, tileY) {
// Determine land type based on tile position
// TODO: Use terrain system data
// For now, return grass for unlocked tiles
if (this.isTileUnlocked(tileX, tileY)) {
return this.landTypes.GRASS;
}
// Surrounding areas have different types
const distFromCenter = Math.sqrt(
Math.pow(tileX - this.farmCenterX, 2) +
Math.pow(tileY - this.farmCenterY, 2)
);
if (distFromCenter < 10) return this.landTypes.GRASS;
if (distFromCenter < 15) return this.landTypes.FOREST;
if (distFromCenter < 20) return this.landTypes.ROCKY;
return this.landTypes.SWAMP;
}
destroy() {
// Cleanup
this.unlockedTiles.clear();
}
}

View File

@@ -0,0 +1,337 @@
/**
* 👥 NPC POPULATION SYSTEM
* Spawns and manages NPCs in structures across the world
* - Biome-specific NPCs
* - Dialog system
* - Trading functionality
* - Quest giving
*/
class NPCPopulationSystem {
constructor(scene) {
this.scene = scene;
// All NPCs in the world
this.npcs = [];
// NPC types per biome
this.npcTypes = {
'Grassland': ['farmer', 'blacksmith', 'merchant', 'guard'],
'Forest': ['hunter', 'herbalist', 'ranger', 'druid'],
'Desert': ['nomad', 'treasure_hunter', 'merchant', 'archaeologist'],
'Mountain': ['miner', 'dwarf', 'geologist', 'mountaineer'],
'Swamp': ['witch', 'alchemist', 'hermit', 'shaman']
};
// Dialog templates
this.dialogs = {
'farmer': [
"Welcome to my farm! Need any seeds?",
"The harvest this year is bountiful!",
"I sell the best wheat in the land!"
],
'merchant': [
"Looking to buy or sell? I've got the best deals!",
"Welcome, traveler! Check out my wares!",
"Gold for goods, goods for gold!"
],
'hunter': [
"The forest is full of game. Happy hunting!",
"Watch out for the wolves at night.",
"I can sell you some arrows if you need them."
],
'nomad': [
"The desert holds many secrets...",
"Water is more valuable than gold here.",
"I've traveled far and wide, seen many things."
],
'miner': [
"These mountains are rich with ore!",
"I can sell you some iron if you need it.",
"Watch your step in those caves!"
],
'witch': [
"Potions and hexes, what do you need?",
"The swamp holds ancient magic...",
"I can brew you something special."
]
};
// Trade goods per NPC type
this.tradeGoods = {
'farmer': [
{ item: 'wheat_seeds', price: 10, stock: 50 },
{ item: 'wheat', price: 5, stock: 100 },
{ item: 'bread', price: 15, stock: 20 }
],
'merchant': [
{ item: 'wood', price: 8, stock: 200 },
{ item: 'stone', price: 6, stock: 150 },
{ item: 'iron_ore', price: 20, stock: 50 }
],
'blacksmith': [
{ item: 'iron_sword', price: 150, stock: 5 },
{ item: 'iron_pickaxe', price: 100, stock: 10 },
{ item: 'iron_axe', price: 120, stock: 8 }
],
'hunter': [
{ item: 'arrow', price: 5, stock: 100 },
{ item: 'bow', price: 80, stock: 3 },
{ item: 'meat', price: 20, stock: 30 }
]
};
console.log('👥 NPCPopulationSystem initialized');
}
// Populate structures with NPCs
populateStructures(structureSystem) {
if (!structureSystem) return;
let npcsSpawned = 0;
// Spawn NPCs in structures (30% chance)
for (const structure of structureSystem.structures) {
if (Math.random() < 0.3) {
const npcType = this.selectNPCType(structure.biome);
this.spawnNPC(structure.x, structure.y, npcType, structure.biome);
npcsSpawned++;
}
}
// Always spawn special NPCs at landmarks
for (const landmark of structureSystem.landmarks) {
this.spawnNPC(landmark.x, landmark.y, 'quest_giver', landmark.type, true);
npcsSpawned++;
}
console.log(`✅ Spawned ${npcsSpawned} NPCs in structures`);
}
// Select NPC type for biome
selectNPCType(biome) {
const types = this.npcTypes[biome] || this.npcTypes['Grassland'];
return types[Math.floor(Math.random() * types.length)];
}
// Spawn single NPC
spawnNPC(x, y, type, biome, isQuestGiver = false) {
const npc = {
x,
y,
type,
biome,
isQuestGiver,
dialogIndex: 0,
hasQuest: isQuestGiver,
questCompleted: false,
sprite: null
};
this.npcs.push(npc);
return npc;
}
// Create NPC sprite (called when chunk loads)
createNPCSprite(npc, chunk) {
if (npc.sprite) return; // Already has sprite
const worldX = npc.x * 48 + 24;
const worldY = npc.y * 48 + 24;
// Create simple circle sprite for NPC
const color = this.getNPCColor(npc.type);
const sprite = this.scene.add.circle(worldX, worldY, 12, color);
sprite.setDepth(10 + worldY);
// Add name label
const label = this.scene.add.text(worldX, worldY - 20, npc.type, {
fontSize: '12px',
color: '#ffffff',
backgroundColor: '#000000',
padding: { x: 4, y: 2 }
});
label.setOrigin(0.5);
label.setDepth(10 + worldY);
// Quest marker for quest givers
if (npc.isQuestGiver && !npc.questCompleted) {
const questMarker = this.scene.add.text(worldX, worldY - 35, '!', {
fontSize: '24px',
color: '#FFD700',
fontStyle: 'bold'
});
questMarker.setOrigin(0.5);
questMarker.setDepth(10 + worldY);
npc.questMarker = questMarker;
if (chunk) chunk.sprites.push(questMarker);
}
npc.sprite = sprite;
npc.label = label;
if (chunk) {
chunk.sprites.push(sprite);
chunk.sprites.push(label);
}
}
// Get NPC color
getNPCColor(type) {
const colors = {
'farmer': 0x8B4513,
'merchant': 0xDAA520,
'blacksmith': 0x696969,
'hunter': 0x228B22,
'nomad': 0xD2691E,
'miner': 0x708090,
'witch': 0x9370DB,
'quest_giver': 0xFFD700
};
return colors[type] || 0x808080;
}
// Check for nearby NPCs
update(playerX, playerY) {
let nearestNPC = null;
let minDist = 3;
for (const npc of this.npcs) {
const dist = Math.sqrt((npc.x - playerX) ** 2 + (npc.y - playerY) ** 2);
if (dist < minDist) {
minDist = dist;
nearestNPC = npc;
}
}
if (nearestNPC && !this.currentNPC) {
this.showTalkPrompt(nearestNPC);
this.currentNPC = nearestNPC;
} else if (!nearestNPC && this.currentNPC) {
this.hideTalkPrompt();
this.currentNPC = null;
}
}
// Show prompt to talk
showTalkPrompt(npc) {
if (this.talkPrompt) return;
const promptText = npc.isQuestGiver
? '💬 Press T to talk (QUEST AVAILABLE)'
: `💬 Press T to talk to ${npc.type}`;
this.talkPrompt = this.scene.add.text(
this.scene.cameras.main.centerX,
this.scene.cameras.main.height - 100,
promptText,
{
fontSize: '24px',
color: npc.isQuestGiver ? '#FFD700' : '#ffffff',
backgroundColor: '#000000',
padding: { x: 20, y: 10 }
}
);
this.talkPrompt.setOrigin(0.5);
this.talkPrompt.setScrollFactor(0);
this.talkPrompt.setDepth(10000);
}
// Hide talk prompt
hideTalkPrompt() {
if (this.talkPrompt) {
this.talkPrompt.destroy();
this.talkPrompt = null;
}
}
// Talk to NPC (T key)
talkToNPC() {
if (!this.currentNPC) return;
const npc = this.currentNPC;
// Get dialog
const dialogs = this.dialogs[npc.type] || ["Hello, traveler!"];
const dialog = dialogs[npc.dialogIndex % dialogs.length];
npc.dialogIndex++;
// Show dialog box
this.showDialog(npc, dialog);
}
// Show dialog UI
showDialog(npc, text) {
// Close existing dialog
if (this.dialogBox) {
this.dialogBox.destroy();
this.dialogText.destroy();
this.dialogBox = null;
}
// Create dialog box
const centerX = this.scene.cameras.main.centerX;
const centerY = this.scene.cameras.main.height - 150;
this.dialogBox = this.scene.add.rectangle(
centerX, centerY,
600, 120,
0x000000, 0.8
);
this.dialogBox.setStrokeStyle(3, 0xFFFFFF);
this.dialogBox.setScrollFactor(0);
this.dialogBox.setDepth(10001);
this.dialogText = this.scene.add.text(
centerX, centerY - 30,
`${npc.type}:\n${text}`,
{
fontSize: '20px',
color: '#ffffff',
align: 'center',
wordWrap: { width: 550 }
}
);
this.dialogText.setOrigin(0.5, 0);
this.dialogText.setScrollFactor(0);
this.dialogText.setDepth(10002);
// Auto-close after 4 seconds
this.scene.time.delayedCall(4000, () => {
if (this.dialogBox) {
this.dialogBox.destroy();
this.dialogText.destroy();
this.dialogBox = null;
}
});
}
// Get statistics
getStats() {
const byBiome = {};
for (const npc of this.npcs) {
byBiome[npc.biome] = (byBiome[npc.biome] || 0) + 1;
}
return {
totalNPCs: this.npcs.length,
questGivers: this.npcs.filter(n => n.isQuestGiver).length,
byBiome
};
}
destroy() {
this.hideTalkPrompt();
if (this.dialogBox) {
this.dialogBox.destroy();
this.dialogText.destroy();
}
this.npcs.forEach(npc => {
if (npc.sprite) npc.sprite.destroy();
if (npc.label) npc.label.destroy();
if (npc.questMarker) npc.questMarker.destroy();
});
this.npcs = [];
}
}

View File

@@ -0,0 +1,454 @@
/**
* NPC PRIVACY & HOME DECORATION SYSTEM
* Part of: Game Systems Expansion
* Created: January 4, 2026
*
* Features:
* - Hobby-based automatic interior generation
* - Door lock system (heart-based access)
* - NPC home visit mechanics
* - Relationship effects from visits
* - Privacy violations & consequences
*/
class NPCPrivacySystem {
constructor(game) {
this.game = game;
this.player = game.player;
// Privacy settings
this.privacyLevels = {
PUBLIC: 0, // Anyone can enter (living room, kitchen)
FRIENDLY: 3, // 3+ hearts required
PRIVATE: 5, // 5+ hearts required (bedroom)
INTIMATE: 8, // 8+ hearts required (special rooms)
LOCKED: 10 // 10 hearts or marriage required
};
// Visit tracking
this.visitHistory = {};
this.lastVisitTimes = {};
// NPC home decorations (generated based on hobby)
this.npcHomes = {};
}
/**
* Generate NPC home interior based on hobby
*/
generateNPCHome(npcId) {
const npc = this.game.npcs.get(npcId);
if (!npc) return null;
const hobby = npc.hobby;
// Base home structure
const home = {
npcId: npcId,
rooms: {
living_room: {
name: 'Living Room',
privacyLevel: this.privacyLevels.PUBLIC,
objects: this.generateLivingRoomObjects(hobby)
},
kitchen: {
name: 'Kitchen',
privacyLevel: this.privacyLevels.PUBLIC,
objects: this.generateKitchenObjects(hobby)
},
bedroom: {
name: 'Bedroom',
privacyLevel: this.privacyLevels.PRIVATE,
objects: this.generateBedroomObjects(hobby)
},
hobby_room: {
name: this.getHobbyRoomName(hobby),
privacyLevel: this.privacyLevels.FRIENDLY,
objects: this.generateHobbyRoomObjects(hobby)
}
},
visitCount: 0,
lastVisit: null
};
// Store home
this.npcHomes[npcId] = home;
return home;
}
/**
* Generate living room objects
*/
generateLivingRoomObjects(hobby) {
const base = [
'interior_table_small',
'interior_bookshelf',
'interior_gothic_lantern'
];
// Hobby-specific additions
const hobbyAdditions = {
fishing: ['mounted_fish', 'fishing_net'],
farming: ['grain_sack', 'tool_rack'],
cooking: ['recipe_shelf'],
reading: ['bookshelf', 'reading_chair'],
music: ['guitar_stand', 'music_sheets'],
crafting: ['crafting_workshop']
};
return [...base, ...(hobbyAdditions[hobby] || [])];
}
/**
* Generate kitchen objects
*/
generateKitchenObjects(hobby) {
const base = [
'interior_kitchen_stove',
'interior_kitchen_counter'
];
if (hobby === 'cooking') {
base.push(
'interior_kitchen_fridge',
'interior_kitchen_sink',
'interior_recipe_shelf'
);
}
return base;
}
/**
* Generate bedroom objects
*/
generateBedroomObjects(hobby) {
const base = [
'interior_bed_wooden',
'interior_wardrobe',
'interior_chest_locked'
];
// Personal items based on hobby
const hobbyItems = {
zombie_worker: ['brain_jar', 'work_uniform'],
baker: ['flour_workspace', 'dough_tools'],
barber: ['dreadlock_kit', 'styling_tools'],
fisherman: ['tackle_box', 'fish_collection']
};
return [...base, ...(hobbyItems[hobby] || [])];
}
/**
* Generate hobby room objects
*/
generateHobbyRoomObjects(hobby) {
const hobbyRooms = {
fishing: [
'fishing_rod_rack',
'bait_storage',
'fish_tank',
'mounted_trophy_fish'
],
zombie_worker: [
'brain_jar',
'work_bench',
'tool_storage',
'miner_equipment'
],
baker: [
'flour_workspace',
'mixing_bowls',
'bread_storage',
'recipe_collection'
],
alchemy: [
'interior_alchemy_bottle',
'interior_brewing_cauldron',
'interior_chemistry_set',
'interior_potion_shelf'
],
crafting: [
'interior_crafting_workshop',
'interior_tool_rack',
'material_storage',
'blueprint_table'
],
reading: [
'interior_bookshelf',
'reading_chair',
'ancient_manuscripts',
'writing_desk'
]
};
return hobbyRooms[hobby] || ['generic_workspace'];
}
/**
* Get hobby room name
*/
getHobbyRoomName(hobby) {
const names = {
fishing: 'Fishing Den',
zombie_worker: 'Worker\'s Quarters',
baker: 'Baking Workshop',
alchemy: 'Alchemy Lab',
crafting: 'Craft Room',
reading: 'Library',
music: 'Music Studio',
farming: 'Storage Shed'
};
return names[hobby] || 'Hobby Room';
}
/**
* Attempt to enter NPC room
*/
attemptEntry(npcId, roomId) {
const npc = this.game.npcs.get(npcId);
if (!npc) {
return { success: false, message: 'NPC not found!' };
}
// Generate home if doesn't exist
if (!this.npcHomes[npcId]) {
this.generateNPCHome(npcId);
}
const home = this.npcHomes[npcId];
const room = home.rooms[roomId];
if (!room) {
return { success: false, message: 'Room not found!' };
}
// Check privacy level
const playerHearts = this.player.getRelationshipHearts(npcId);
const requiredHearts = room.privacyLevel;
if (playerHearts < requiredHearts) {
// Privacy violation!
return this.handlePrivacyViolation(npcId, roomId, requiredHearts);
}
// Allowed entry
return this.handleSuccessfulEntry(npcId, roomId);
}
/**
* Handle privacy violation
*/
handlePrivacyViolation(npcId, roomId, requiredHearts) {
const npc = this.game.npcs.get(npcId);
// Relationship penalty
const penalty = (requiredHearts - this.player.getRelationshipHearts(npcId)) * 20;
npc.addRelationshipPoints(-penalty);
// NPC reaction
const reactions = [
"Hey! That's private!",
"What are you doing in my room?!",
"Get out! This is MY space!",
"I can't believe you just walked in here...",
"Privacy, please!"
];
const reaction = Phaser.Utils.Array.GetRandom(reactions);
this.game.showDialogue(npc.name, reaction, {
mood: 'angry',
animation: 'shocked'
});
this.game.showMessage(
`${npc.name} is upset! -${penalty} relationship points`
);
// Player gets kicked out
this.game.player.teleportToLocation('outside_' + npcId + '_home');
return {
success: false,
privacyViolation: true,
penalty: penalty,
requiredHearts: requiredHearts,
currentHearts: this.player.getRelationshipHearts(npcId)
};
}
/**
* Handle successful entry
*/
handleSuccessfulEntry(npcId, roomId) {
const npc = this.game.npcs.get(npcId);
const home = this.npcHomes[npcId];
// Track visit
if (!this.visitHistory[npcId]) {
this.visitHistory[npcId] = [];
}
const visit = {
roomId: roomId,
timestamp: this.game.time.currentTime,
timeOfDay: this.game.time.getTimeOfDay()
};
this.visitHistory[npcId].push(visit);
this.lastVisitTimes[npcId] = this.game.time.currentTime;
home.visitCount++;
home.lastVisit = this.game.time.currentTime;
// Relationship effects based on visit
this.applyVisitEffects(npcId, roomId, visit);
// Enter room scene
this.game.scene.start('NPCRoomScene', {
npcId: npcId,
roomId: roomId,
room: home.rooms[roomId]
});
return {
success: true,
room: home.rooms[roomId],
npc: npc
};
}
/**
* Apply relationship effects from visit
*/
applyVisitEffects(npcId, roomId, visit) {
const npc = this.game.npcs.get(npcId);
const timeOfDay = visit.timeOfDay;
// Visit frequency check
const recentVisits = this.visitHistory[npcId].filter(v => {
const hoursSince = (this.game.time.currentTime - v.timestamp) / 3600;
return hoursSince < 24; // Last 24 hours
});
if (recentVisits.length > 3) {
// Visiting TOO much = annoying
npc.addRelationshipPoints(-10);
this.game.showMessage(
`${npc.name} seems a bit annoyed by frequent visits...`
);
return;
}
// Time of day effects
if (timeOfDay === 'night' && roomId === 'bedroom') {
// Visiting bedroom at night = awkward
npc.addRelationshipPoints(-5);
this.game.showDialogue(
npc.name,
"It's quite late... Is everything okay?",
{ mood: 'concerned' }
);
} else if (timeOfDay === 'morning' && roomId === 'kitchen') {
// Morning kitchen visit = breakfast together!
npc.addRelationshipPoints(5);
this.game.showDialogue(
npc.name,
"Good morning! Care to join me for breakfast?",
{ mood: 'happy' }
);
} else if (roomId === 'hobby_room') {
// Showing interest in hobby = bonus points!
npc.addRelationshipPoints(10);
this.game.showDialogue(
npc.name,
"I'm glad you're interested in my hobby!",
{ mood: 'excited' }
);
}
}
/**
* Get visit statistics for NPC
*/
getVisitStats(npcId) {
const visits = this.visitHistory[npcId] || [];
// Count visits per room
const roomCounts = {};
visits.forEach(visit => {
roomCounts[visit.roomId] = (roomCounts[visit.roomId] || 0) + 1;
});
// Average visits per day
const daysSinceFirstVisit = visits.length > 0
? (this.game.time.currentTime - visits[0].timestamp) / 86400
: 0;
const avgVisitsPerDay = daysSinceFirstVisit > 0
? visits.length / daysSinceFirstVisit
: 0;
return {
totalVisits: visits.length,
roomCounts: roomCounts,
avgVisitsPerDay: avgVisitsPerDay,
lastVisit: this.lastVisitTimes[npcId],
favoriteRoom: Object.keys(roomCounts).reduce((a, b) =>
roomCounts[a] > roomCounts[b] ? a : b, null)
};
}
/**
* Check if player can access special room
*/
canAccessSpecialRoom(npcId, roomId) {
const npc = this.game.npcs.get(npcId);
if (!npc) return false;
const home = this.npcHomes[npcId];
if (!home) return false;
const room = home.rooms[roomId];
if (!room) return false;
const playerHearts = this.player.getRelationshipHearts(npcId);
// Special case: marriage allows LOCKED access
if (room.privacyLevel === this.privacyLevels.LOCKED) {
return this.player.spouse === npcId;
}
return playerHearts >= room.privacyLevel;
}
/**
* Get NPC home UI data
*/
getNPCHomeUIData(npcId) {
if (!this.npcHomes[npcId]) {
this.generateNPCHome(npcId);
}
const home = this.npcHomes[npcId];
const npc = this.game.npcs.get(npcId);
return {
npc: npc,
home: home,
accessibleRooms: Object.keys(home.rooms).filter(roomId =>
this.canAccessSpecialRoom(npcId, roomId)
),
lockedRooms: Object.keys(home.rooms).filter(roomId =>
!this.canAccessSpecialRoom(npcId, roomId)
),
visitStats: this.getVisitStats(npcId)
};
}
}

View File

@@ -0,0 +1,341 @@
/**
* NPC SETTLEMENT SYSTEM (Magic Helpers)
* NPCs auto-assist with tasks
* Worker efficiency, happiness, housing
*/
export class NPCSettlementSystem {
constructor(scene) {
this.scene = scene;
// Settled NPCs
this.settledNPCs = new Map();
// Worker assignments
this.assignments = new Map(); // npcId → task
// NPC stats
this.npcHappiness = new Map(); // npcId → happiness (0-100)
this.npcEfficiency = new Map(); // npcId → efficiency (0.5-2.0)
// Housing
this.npcHomes = new Map(); // npcId → buildingId
this.housingCapacity = 0;
// Task types
this.taskTypes = ['farming', 'building', 'crafting', 'defense', 'gathering'];
this.init();
}
init() {
// Update worker tasks periodically
this.scene.time.addEvent({
delay: 5000, // Every 5s
callback: () => this.updateWorkers(),
loop: true
});
// Update happiness hourly
this.scene.time.addEvent({
delay: 3600000, // 1 hour
callback: () => this.updateHappiness(),
loop: true
});
}
/**
* Settle an NPC (they join the town)
*/
settleNPC(npcId, npcData) {
if (this.settledNPCs.has(npcId)) return false;
//Check housing availability
if (this.settledNPCs.size >= this.housingCapacity) {
console.warn('No housing available');
return false;
}
this.settledNPCs.set(npcId, {
id: npcId,
name: npcData.name,
skills: npcData.skills || [],
assignedTask: null,
...npcData
});
// Initial stats
this.npcHappiness.set(npcId, 75); // Start at 75% happiness
this.npcEfficiency.set(npcId, 1.0); // Base efficiency
// Notification
this.scene.uiSystem?.showNotification(
`${npcData.name} has joined the settlement!`,
'success',
{ icon: 'npc_join' }
);
console.log(`🏘️ ${npcData.name} settled in town`);
return true;
}
/**
* Assign NPC to task
*/
assignTask(npcId, taskType, taskData) {
const npc = this.settledNPCs.get(npcId);
if (!npc) return false;
// Check if NPC has skill for task
if (!npc.skills.includes(taskType)) {
console.warn(`${npc.name} lacks skill: ${taskType}`);
return false;
}
// Assign
npc.assignedTask = taskType;
this.assignments.set(npcId, {
type: taskType,
data: taskData,
startTime: Date.now()
});
console.log(`👷 ${npc.name} assigned to ${taskType}`);
return true;
}
/**
* Unassign NPC from task
*/
unassignTask(npcId) {
const npc = this.settledNPCs.get(npcId);
if (!npc) return false;
npc.assignedTask = null;
this.assignments.delete(npcId);
console.log(`${npc.name} unassigned`);
return true;
}
/**
* Update all worker tasks
*/
updateWorkers() {
this.assignments.forEach((assignment, npcId) => {
const npc = this.settledNPCs.get(npcId);
const efficiency = this.npcEfficiency.get(npcId) || 1.0;
switch (assignment.type) {
case 'farming':
this.performFarming(npc, efficiency, assignment.data);
break;
case 'building':
this.performBuilding(npc, efficiency, assignment.data);
break;
case 'crafting':
this.performCrafting(npc, efficiency, assignment.data);
break;
case 'defense':
this.performDefense(npc, efficiency);
break;
case 'gathering':
this.performGathering(npc, efficiency);
break;
}
});
}
/**
* WORKER TASKS
*/
performFarming(npc, efficiency, data) {
// Auto-tend crops
const crops = this.scene.crops || [];
const tendsPerCycle = Math.floor(2 * efficiency);
for (let i = 0; i < tendsPerCycle && i < crops.length; i++) {
const crop = crops[i];
crop.water?.();
crop.fertilize?.();
}
// Chance to auto-harvest
if (Math.random() < 0.1 * efficiency) {
const harvestable = crops.find(c => c.isHarvestable);
if (harvestable) {
this.scene.farmingSystem?.harvestCrop(harvestable);
console.log(`${npc.name} harvested crop`);
}
}
}
performBuilding(npc, efficiency, data) {
// Speed up construction
const building = data.buildingId ? this.scene.buildingSystem?.getBuilding(data.buildingId) : null;
if (building && building.isUnderConstruction) {
const progressBonus = 5 * efficiency;
building.constructionProgress += progressBonus;
if (building.constructionProgress >= 100) {
this.scene.buildingSystem?.completeConstruction(building.id);
console.log(`${npc.name} completed building: ${building.name}`);
}
}
}
performCrafting(npc, efficiency, data) {
// Auto-craft items
if (data.recipe) {
const canCraft = this.scene.craftingSystem?.canCraft(data.recipe);
if (canCraft && Math.random() < 0.2 * efficiency) {
this.scene.craftingSystem?.craft(data.recipe);
console.log(`${npc.name} crafted ${data.recipe}`);
}
}
}
performDefense(npc, efficiency) {
// Patrol and defend
// Increased detection range for raids
this.scene.gameState.buffs.raid_detection_range = (this.scene.gameState.buffs.raid_detection_range || 1.0) + (0.1 * efficiency);
// Auto-repair walls/defenses
const defenses = this.scene.defenseSystem?.getAllDefenses() || [];
defenses.forEach(defense => {
if (defense.health < defense.maxHealth && Math.random() < 0.05 * efficiency) {
defense.health = Math.min(defense.maxHealth, defense.health + 10);
console.log(`${npc.name} repaired ${defense.name}`);
}
});
}
performGathering(npc, efficiency) {
// Auto-gather resources
const gatherChance = 0.15 * efficiency;
if (Math.random() < gatherChance) {
const resources = ['wood', 'stone', 'berries', 'herbs'];
const resource = Phaser.Utils.Array.GetRandom(resources);
const amount = Math.floor(Phaser.Math.Between(1, 3) * efficiency);
this.scene.inventorySystem?.addItem(resource, amount);
console.log(`${npc.name} gathered ${amount} ${resource}`);
}
}
/**
* Update NPC happiness
*/
updateHappiness() {
this.settledNPCs.forEach((npc, npcId) => {
let happiness = this.npcHappiness.get(npcId) || 50;
// Factors affecting happiness
const hasHome = this.npcHomes.has(npcId);
const hasTask = npc.assignedTask !== null;
const isOverworked = hasTask && this.assignments.get(npcId)?.overworked;
// Adjustments
if (hasHome) happiness += 5;
if (hasTask) happiness += 2;
if (isOverworked) happiness -= 10;
if (!hasHome && this.settledNPCs.size > this.housingCapacity) happiness -= 15;
// Natural decay
happiness -= 1;
// Clamp
happiness = Math.max(0, Math.min(100, happiness));
this.npcHappiness.set(npcId, happiness);
// Update efficiency based on happiness
const efficiency = 0.5 + (happiness / 100) * 1.5; // 0.5-2.0
this.npcEfficiency.set(npcId, efficiency);
// Low happiness warning
if (happiness < 30) {
this.scene.uiSystem?.showNotification(
`${npc.name} is unhappy!`,
'warning'
);
console.warn(`${npc.name} happiness: ${happiness}`);
}
});
}
/**
* Assign NPC to home
*/
assignHome(npcId, buildingId) {
this.npcHomes.set(npcId, buildingId);
// Happiness boost
const happiness = this.npcHappiness.get(npcId) || 50;
this.npcHappiness.set(npcId, Math.min(100, happiness + 20));
console.log(`${this.settledNPCs.get(npcId).name} moved into home`);
}
/**
* Increase housing capacity
*/
addHousing(capacity) {
this.housingCapacity += capacity;
console.log(`Housing capacity: ${this.housingCapacity}`);
}
/**
* Get settlement statistics
*/
getSettlementStats() {
const npcs = Array.from(this.settledNPCs.values());
const avgHappiness = npcs.reduce((sum, npc) => sum + (this.npcHappiness.get(npc.id) || 0), 0) / npcs.length || 0;
const avgEfficiency = npcs.reduce((sum, npc) => sum + (this.npcEfficiency.get(npc.id) || 1), 0) / npcs.length || 1;
return {
population: npcs.length,
housingCapacity: this.housingCapacity,
averageHappiness: Math.round(avgHappiness),
averageEfficiency: avgEfficiency.toFixed(2),
assignedWorkers: this.assignments.size,
unemployed: npcs.length - this.assignments.size
};
}
/**
* Get NPCs by skill
*/
getNPCsBySkill(skill) {
return Array.from(this.settledNPCs.values()).filter(npc => npc.skills.includes(skill));
}
/**
* Save/Load
*/
getSaveData() {
return {
settledNPCs: Array.from(this.settledNPCs.entries()),
assignments: Array.from(this.assignments.entries()),
npcHappiness: Array.from(this.npcHappiness.entries()),
npcHomes: Array.from(this.npcHomes.entries()),
housingCapacity: this.housingCapacity
};
}
loadSaveData(data) {
this.settledNPCs = new Map(data.settledNPCs || []);
this.assignments = new Map(data.assignments || []);
this.npcHappiness = new Map(data.npcHappiness || []);
this.npcHomes = new Map(data.npcHomes || []);
this.housingCapacity = data.housingCapacity || 0;
// Recalculate efficiency
this.npcHappiness.forEach((happiness, npcId) => {
const efficiency = 0.5 + (happiness / 100) * 1.5;
this.npcEfficiency.set(npcId, efficiency);
});
}
}

View File

@@ -0,0 +1,503 @@
/**
* NPCShopSystem.js
* ================
* KRVAVA ŽETEV - NPC Trading & Shop System (Phase 38)
*
* Features:
* - 4 NPC shop types (Blacksmith, Baker, Trader, Healer)
* - Shop UI with buy/sell
* - Dynamic pricing
* - Stock management
* - Relationship discounts
*
* @author NovaFarma Team
* @date 2025-12-23
*/
class NPCShopSystem {
constructor(scene) {
this.scene = scene;
// Shop registry
this.shops = new Map();
this.currentShop = null;
// Shop UI
this.shopContainer = null;
this.isShopOpen = false;
// Player inventory reference
this.playerInventory = null;
this.playerZlatniki = 0;
console.log('🛒 NPCShopSystem initialized');
// Register all shops
this.registerShops();
// Create shop UI
this.createShopUI();
}
/**
* Register all NPC shops
*/
registerShops() {
const shops = [
{
id: 'blacksmith',
name: 'Kovač (Blacksmith)',
npc: 'Ivan the Blacksmith',
icon: '⚒️',
location: { x: 200, y: 200 },
inventory: [
// Tools
{ id: 'iron_axe', name: 'Iron Axe', price: 200, stock: 5, category: 'tools', locked: true },
{ id: 'iron_pickaxe', name: 'Iron Pickaxe', price: 200, stock: 5, category: 'tools', locked: true },
{ id: 'iron_hoe', name: 'Iron Hoe', price: 150, stock: 5, category: 'tools', locked: true },
{ id: 'watering_can', name: 'Watering Can', price: 100, stock: 10, category: 'tools' },
// Weapons
{ id: 'iron_sword', name: 'Iron Sword', price: 500, stock: 3, category: 'weapons', locked: true },
{ id: 'steel_sword', name: 'Steel Sword', price: 1000, stock: 2, category: 'weapons', locked: true },
{ id: 'crossbow', name: 'Crossbow', price: 800, stock: 2, category: 'weapons', locked: true },
// Armor
{ id: 'leather_armor', name: 'Leather Armor', price: 300, stock: 5, category: 'armor' },
{ id: 'iron_armor', name: 'Iron Armor', price: 800, stock: 3, category: 'armor' }
],
buyback: ['iron_ore', 'steel_bar', 'scrap_metal']
},
{
id: 'baker',
name: 'Pekarica (Baker)',
npc: 'Maria the Baker',
icon: '🍞',
location: { x: 250, y: 200 },
inventory: [
// Food
{ id: 'bread', name: 'Bread', price: 10, stock: 50, category: 'food' },
{ id: 'cheese', name: 'Cheese', price: 20, stock: 30, category: 'food' },
{ id: 'apple_pie', name: 'Apple Pie', price: 50, stock: 20, category: 'food' },
{ id: 'cake', name: 'Cake', price: 100, stock: 10, category: 'food' },
// Recipes
{ id: 'recipe_cookies', name: 'Cookie Recipe', price: 200, stock: 1, category: 'recipes' },
{ id: 'recipe_pizza', name: 'Pizza Recipe', price: 300, stock: 1, category: 'recipes' },
// Ingredients
{ id: 'flour', name: 'Flour', price: 15, stock: 100, category: 'ingredients' },
{ id: 'sugar', name: 'Sugar', price: 20, stock: 80, category: 'ingredients' },
{ id: 'yeast', name: 'Yeast', price: 10, stock: 50, category: 'ingredients' }
],
buyback: ['wheat', 'milk', 'eggs', 'berries']
},
{
id: 'trader',
name: 'Trgovec (General Trader)',
npc: 'Gregor the Trader',
icon: '💰',
location: { x: 300, y: 200 },
inventory: [
// Seeds
{ id: 'wheat_seeds', name: 'Wheat Seeds', price: 5, stock: 200, category: 'seeds' },
{ id: 'corn_seeds', name: 'Corn Seeds', price: 8, stock: 150, category: 'seeds' },
{ id: 'tomato_seeds', name: 'Tomato Seeds', price: 10, stock: 100, category: 'seeds' },
{ id: 'strawberry_seeds', name: 'Strawberry Seeds', price: 15, stock: 80, category: 'seeds' },
{ id: 'cannabis_seeds', name: 'Cannabis Seeds', price: 20, stock: 50, category: 'seeds' },
// Materials
{ id: 'wood', name: 'Wood', price: 10, stock: 500, category: 'materials' },
{ id: 'stone', name: 'Stone', price: 15, stock: 300, category: 'materials' },
{ id: 'clay', name: 'Clay', price: 20, stock: 200, category: 'materials' },
// Special
{ id: 'saddle', name: 'Saddle', price: 500, stock: 2, category: 'special' },
{ id: 'bouquet', name: 'Bouquet', price: 100, stock: 10, category: 'special' },
{ id: 'mermaid_pendant', name: 'Mermaid Pendant', price: 5000, stock: 1, category: 'special' }
],
buyback: ['crops', 'foraged_items', 'fish']
},
{
id: 'healer',
name: 'Zdravnik (Healer)',
npc: 'Dr. Ana Kovač',
icon: '⚕️',
location: { x: 350, y: 200 },
inventory: [
// Potions
{ id: 'health_potion', name: 'Health Potion', price: 50, stock: 50, category: 'potions' },
{ id: 'stamina_potion', name: 'Stamina Potion', price: 40, stock: 50, category: 'potions' },
{ id: 'antidote', name: 'Antidote', price: 30, stock: 30, category: 'potions' },
{ id: 'cure_infection', name: 'Cure Infection', price: 200, stock: 10, category: 'potions' },
// Research
{ id: 'cure_research_1', name: 'Cure Research Notes I', price: 1000, stock: 1, category: 'research' },
{ id: 'cure_research_2', name: 'Cure Research Notes II', price: 2000, stock: 1, category: 'research' },
// Medical supplies
{ id: 'bandage', name: 'Bandage', price: 15, stock: 100, category: 'medical' },
{ id: 'medicine', name: 'Medicine', price: 80, stock: 30, category: 'medical' }
],
buyback: ['herbs', 'mushrooms', 'zombie_samples', 'cannabis', 'cannabis_buds']
}
];
shops.forEach(shop => this.shops.set(shop.id, shop));
console.log(`✅ Registered ${this.shops.size} NPC shops`);
}
/**
* Create shop UI
*/
createShopUI() {
const width = this.scene.cameras.main.width;
const height = this.scene.cameras.main.height;
// Main container
this.shopContainer = this.scene.add.container(width / 2, height / 2);
this.shopContainer.setScrollFactor(0);
this.shopContainer.setDepth(10000);
this.shopContainer.setVisible(false);
// Background
const bg = this.scene.add.rectangle(0, 0, 900, 600, 0x1a1a1a, 0.95);
bg.setStrokeStyle(3, 0xFFD700);
this.shopContainer.add(bg);
// Title (will be updated)
this.shopTitle = this.scene.add.text(0, -280, '🛒 SHOP', {
fontSize: '32px',
fontFamily: 'Arial',
color: '#FFD700',
fontStyle: 'bold'
});
this.shopTitle.setOrigin(0.5);
this.shopContainer.add(this.shopTitle);
// Close button
const closeBtn = this.scene.add.text(430, -280, '❌', {
fontSize: '24px',
cursor: 'pointer'
});
closeBtn.setInteractive();
closeBtn.on('pointerdown', () => this.closeShop());
this.shopContainer.add(closeBtn);
// Player money display
this.moneyText = this.scene.add.text(-430, -250, '💰 0 Zlatniki', {
fontSize: '18px',
fontFamily: 'Arial',
color: '#FFD700'
});
this.shopContainer.add(this.moneyText);
// Category tabs
this.createCategoryTabs();
// Item list container
this.itemListContainer = this.scene.add.container(0, 0);
this.shopContainer.add(this.itemListContainer);
console.log('✅ Shop UI created');
}
/**
* Create category tabs
*/
createCategoryTabs() {
const categories = ['all', 'tools', 'weapons', 'food', 'seeds', 'potions'];
const tabWidth = 120;
const startX = -400;
const y = -200;
categories.forEach((category, index) => {
const tab = this.scene.add.rectangle(
startX + (index * tabWidth),
y,
110, 40,
0x2d2d2d
);
tab.setStrokeStyle(2, 0x666666);
tab.setInteractive();
tab.on('pointerdown', () => this.filterByCategory(category));
const label = this.scene.add.text(
startX + (index * tabWidth),
y,
category.toUpperCase(),
{
fontSize: '14px',
fontFamily: 'Arial',
color: '#ffffff'
}
);
label.setOrigin(0.5);
this.shopContainer.add(tab);
this.shopContainer.add(label);
});
}
/**
* Open shop
*/
openShop(shopId) {
const shop = this.shops.get(shopId);
if (!shop) {
console.error(`Shop ${shopId} not found!`);
return false;
}
this.currentShop = shop;
this.isShopOpen = true;
// Update title
this.shopTitle.setText(`${shop.icon} ${shop.name}`);
// Update money
this.updateMoneyDisplay();
// Display items
this.displayShopItems(shop.inventory);
// Show container
this.shopContainer.setVisible(true);
console.log(`🛒 Opened ${shop.name}`);
return true;
}
/**
* Close shop
*/
closeShop() {
this.isShopOpen = false;
this.currentShop = null;
this.shopContainer.setVisible(false);
console.log('🛒 Shop closed');
}
/**
* Display shop items
*/
displayShopItems(items, filter = 'all') {
// Clear previous items
this.itemListContainer.removeAll(true);
// Filter items
let filteredItems = items;
if (filter !== 'all') {
filteredItems = items.filter(item => item.category === filter);
}
// Display items (max 10 visible, scrollable)
const itemHeight = 50;
const startY = -150;
filteredItems.slice(0, 10).forEach((item, index) => {
const y = startY + (index * itemHeight);
// Item background
const itemBg = this.scene.add.rectangle(-400, y, 850, 45, 0x2d2d2d, 0.8);
itemBg.setStrokeStyle(1, 0x444444);
this.itemListContainer.add(itemBg);
// Item name
const nameText = this.scene.add.text(-380, y - 10, item.name, {
fontSize: '16px',
fontFamily: 'Arial',
color: '#ffffff'
});
this.itemListContainer.add(nameText);
// Stock
const stockText = this.scene.add.text(-380, y + 10, `Stock: ${item.stock}`, {
fontSize: '12px',
fontFamily: 'Arial',
color: '#888888'
});
this.itemListContainer.add(stockText);
// Price
const priceText = this.scene.add.text(200, y, `${item.price} Ž`, {
fontSize: '18px',
fontFamily: 'Arial',
color: '#FFD700',
fontStyle: 'bold'
});
priceText.setOrigin(0.5);
this.itemListContainer.add(priceText);
// Buy button
const isLocked = item.locked && !this.isShopRestored(this.currentShop.id);
const buyColor = isLocked ? 0x666666 : 0x228B22;
const buyBtn = this.scene.add.rectangle(350, y, 100, 35, buyColor);
buyBtn.setStrokeStyle(2, isLocked ? 0x999999 : 0x32CD32);
buyBtn.setInteractive();
buyBtn.on('pointerdown', () => {
if (isLocked) {
this.showNotification({
title: 'Shop Ruined',
text: `Restore the shop to unlock this item!`,
icon: '🏚️'
});
} else {
this.buyItem(item);
}
});
this.itemListContainer.add(buyBtn);
const buyText = this.scene.add.text(350, y, isLocked ? 'LOCKED' : 'BUY', {
fontSize: '14px',
fontFamily: 'Arial',
color: '#ffffff',
fontStyle: 'bold'
});
buyText.setOrigin(0.5);
this.itemListContainer.add(buyText);
});
}
isShopRestored(shopId) {
if (!this.scene.townRestorationSystem) return true; // Fallback if system missing
// Map shopId to buildingId
const mapping = {
'blacksmith': 'jakob_shop', // or whichever ID correctly matches TownRestorationSystem
'baker': 'lena_bakery',
'healer': 'dr_chen_clinic'
};
const buildingId = mapping[shopId];
if (!buildingId) return true; // General trader is always open
const building = this.scene.townRestorationSystem.buildings.get(buildingId);
return building ? building.isRestored : false;
}
/**
* Filter by category
*/
filterByCategory(category) {
if (!this.currentShop) return;
this.displayShopItems(this.currentShop.inventory, category);
}
/**
* Buy item
*/
buyItem(item) {
// Check stock
if (item.stock <= 0) {
this.showNotification({
title: 'Out of Stock',
text: `${item.name} is out of stock!`,
icon: '📦'
});
return false;
}
// Calculate price with relationship discount
const finalPrice = this.calculatePrice(item.price);
// Check if player can afford
if (this.playerZlatniki < finalPrice) {
this.showNotification({
title: 'Not Enough Money',
text: `Need ${finalPrice}Ž to buy ${item.name}!`,
icon: '💰'
});
return false;
}
// Purchase!
this.playerZlatniki -= finalPrice;
item.stock--;
// TODO: Add item to player inventory
console.log(`✅ Purchased: ${item.name} for ${finalPrice}Ž`);
// Update UI
this.updateMoneyDisplay();
this.displayShopItems(this.currentShop.inventory);
this.showNotification({
title: 'Purchase Complete!',
text: `Bought ${item.name} for ${finalPrice}Ž!`,
icon: '✅'
});
return true;
}
/**
* Calculate price with discounts
*/
calculatePrice(basePrice) {
// TODO: Apply relationship discounts
// For now, return base price
return basePrice;
}
/**
* Update money display
*/
updateMoneyDisplay() {
this.moneyText.setText(`💰 ${this.playerZlatniki} Zlatniki`);
}
/**
* Set player money
*/
setPlayerMoney(amount) {
this.playerZlatniki = amount;
this.updateMoneyDisplay();
}
/**
* Get shop info
*/
getShopInfo(shopId) {
return this.shops.get(shopId);
}
/**
* Get all shops
*/
getAllShops() {
return Array.from(this.shops.values());
}
/**
* Restock shop
*/
restockShop(shopId) {
const shop = this.shops.get(shopId);
if (!shop) return false;
shop.inventory.forEach(item => {
item.stock = Math.min(item.stock + 5, 100); // Restock +5, max 100
});
console.log(`📦 ${shop.name} restocked!`);
return true;
}
/**
* Helper: Show notification
*/
showNotification(notification) {
console.log(`📢 ${notification.icon} ${notification.title}: ${notification.text}`);
const ui = this.scene.scene.get('UIScene');
if (ui && ui.showNotification) {
ui.showNotification(notification);
}
}
}

View File

@@ -0,0 +1,76 @@
// NPCSpawner.js - Sistem za spawnjanje NPCjev
class NPCSpawner {
constructor(scene) {
this.scene = scene;
this.spawnInterval = 30000; // 30 sekund
this.maxNPCs = 3; // 3 NPCji na 100x100 mapo
this.spawnTimer = 0;
console.log('🧑 NPCSpawner: Initialized');
}
spawnInitialNPCs() {
// Spawn 3 NPCs at random locations
for (let i = 0; i < this.maxNPCs; i++) {
this.spawnRandomNPC();
}
console.log(`✅ Spawned ${this.maxNPCs} initial NPCs`);
}
spawnRandomNPC() {
if (!this.scene.terrainSystem || !this.scene.npcs) return;
// Random position (avoid farm area 20,20)
let x, y, attempts = 0;
do {
x = Phaser.Math.Between(5, 95);
y = Phaser.Math.Between(5, 95);
attempts++;
} while (this.isTooCloseToFarm(x, y) && attempts < 50);
// Check if tile is valid
const tile = this.scene.terrainSystem.getTile(x, y);
if (!tile || tile.type === 'water') return;
// Check if decoration exists
const key = `${x},${y}`;
if (this.scene.terrainSystem.decorationsMap.has(key)) return;
// Spawn NPC
const npc = new NPC(
this.scene,
x,
y,
this.scene.terrainOffsetX || 0,
this.scene.terrainOffsetY || 0,
'zombie' // Type
);
// Set to PASSIVE mode (random walk)
npc.state = 'PASSIVE';
npc.isTamed = false;
this.scene.npcs.push(npc);
console.log(`🧟 Spawned NPC at (${x}, ${y})`);
}
isTooCloseToFarm(x, y) {
const farmX = 50; // Farm center (updated from 20,20 to 50,50)
const farmY = 50;
const farmRadius = 15;
const dist = Math.sqrt((x - farmX) ** 2 + (y - farmY) ** 2);
return dist < farmRadius;
}
update(delta) {
// Check if we need to spawn more NPCs
if (this.scene.npcs.length < this.maxNPCs) {
this.spawnTimer += delta;
if (this.spawnTimer >= this.spawnInterval) {
this.spawnTimer = 0;
this.spawnRandomNPC();
}
}
}
}

View File

@@ -0,0 +1,600 @@
/**
* SMART ZOMBIE SYSTEM
* Manages zombie intelligence levels (Lv1-10), independent AI, follower commands, and team construction.
*
* Features:
* - Lv1-4: Helpers (resource finding, carry materials, warn danger)
* - Lv5-7: Assistants (repair assistance +25% speed, material delivery AI)
* - Lv8-10: INDEPENDENT AI (build/repair alone, auto-detect damage, teach others)
* - Follower System: 10 zombie followers with commands (Stop, Help, Attack, Home)
* - Team Construction: Lv10 leader + multi-zombie mega-projects
*/
class SmartZombieSystem {
constructor(scene) {
this.scene = scene;
// Zombie intelligence data
this.zombies = new Map(); // zombieId -> zombie data
this.followers = []; // Active followers (max 10)
this.independentWorkers = []; // Lv8+ zombies working autonomously
this.teams = []; // Construction teams (Lv10 leader + team)
// Commands
this.followerCommands = ['STOP', 'HELP', 'ATTACK', 'HOME'];
this.currentCommand = 'HELP'; // Default command
// AI Task Queue
this.taskQueue = []; // {type, position, priority, assignedZombie}
// Intelligence Levels
this.intelligenceLevels = {
1: { name: 'Curious', abilities: ['ping_resources', 'carry_light'] },
2: { name: 'Aware', abilities: ['carry_medium', 'warn_danger'] },
3: { name: 'Helper', abilities: ['gather_resources', 'follow_player'] },
4: { name: 'Assistant', abilities: ['detect_ores', 'organize_storage'] },
5: { name: 'Skilled', abilities: ['repair_assist', 'deliver_materials'] },
6: { name: 'Expert', abilities: ['craft_assist', 'defend_farm'] },
7: { name: 'Advanced', abilities: ['build_assist', 'teach_lv1_3'] },
8: { name: 'INDEPENDENT', abilities: ['build_alone', 'repair_alone', 'auto_detect_damage'] },
9: { name: 'MASTER', abilities: ['teach_all', 'lead_team', 'optimize_tasks'] },
10: { name: 'GENIUS', abilities: ['mega_projects', 'invent_blueprints', 'immortal_knowledge'] }
};
console.log('🧠 Smart Zombie System initialized!');
}
/**
* Create or upgrade a zombie
*/
createZombie(name, level = 1, position = { x: 0, y: 0 }) {
const zombieId = `zombie_${Date.now()}_${Math.random()}`;
const zombie = {
id: zombieId,
name: name,
level: Math.min(level, 10),
position: position,
status: 'idle', // idle, working, following, patrolling
currentTask: null,
experience: 0,
experienceToNext: this.getExpRequired(level),
abilities: this.getAbilities(level),
inventory: [],
loyalty: 50, // 0-100
energy: 100, // 0-100
sprite: null // Will be set when rendered
};
this.zombies.set(zombieId, zombie);
console.log(`🧟 Created ${name} (Lv${level}) - ${this.intelligenceLevels[level].name}`);
// Auto-assign if Lv8+
if (level >= 8) {
this.makeIndependent(zombieId);
}
return zombieId;
}
/**
* Get required XP for next level
*/
getExpRequired(level) {
return 100 * level * level; // 100, 400, 900, 1600, etc.
}
/**
* Get abilities for a level
*/
getAbilities(level) {
const abilities = [];
for (let i = 1; i <= level; i++) {
abilities.push(...this.intelligenceLevels[i].abilities);
}
return [...new Set(abilities)]; // Remove duplicates
}
/**
* Add experience to a zombie
*/
addExperience(zombieId, amount) {
const zombie = this.zombies.get(zombieId);
if (!zombie) return;
zombie.experience += amount;
// Level up?
while (zombie.experience >= zombie.experienceToNext && zombie.level < 10) {
zombie.level++;
zombie.experience -= zombie.experienceToNext;
zombie.experienceToNext = this.getExpRequired(zombie.level + 1);
zombie.abilities = this.getAbilities(zombie.level);
console.log(`⬆️ ${zombie.name} leveled up to Lv${zombie.level}!`);
this.scene.events.emit('show-floating-text', {
x: zombie.position.x,
y: zombie.position.y - 50,
text: `LEVEL UP! Lv${zombie.level}`,
color: '#FFD700'
});
// Auto-assign if reached Lv8
if (zombie.level === 8) {
this.makeIndependent(zombieId);
}
}
}
/**
* Make a zombie independent (Lv8+)
*/
makeIndependent(zombieId) {
const zombie = this.zombies.get(zombieId);
if (!zombie || zombie.level < 8) return false;
if (!this.independentWorkers.includes(zombieId)) {
this.independentWorkers.push(zombieId);
zombie.status = 'independent';
console.log(`🤖 ${zombie.name} is now INDEPENDENT!`);
this.scene.events.emit('notification', {
title: 'Independent Worker!',
message: `${zombie.name} can now work autonomously!`,
icon: '🤖'
});
}
return true;
}
/**
* Add zombie to follower group (max 10)
*/
addFollower(zombieId) {
if (this.followers.length >= 10) {
console.log('❌ Maximum 10 followers!');
return false;
}
const zombie = this.zombies.get(zombieId);
if (!zombie) return false;
if (!this.followers.includes(zombieId)) {
this.followers.push(zombieId);
zombie.status = 'following';
console.log(` ${zombie.name} joined your followers!`);
return true;
}
return false;
}
/**
* Remove zombie from followers
*/
removeFollower(zombieId) {
const index = this.followers.indexOf(zombieId);
if (index > -1) {
this.followers.splice(index, 1);
const zombie = this.zombies.get(zombieId);
if (zombie) {
zombie.status = 'idle';
console.log(` ${zombie.name} left your followers.`);
}
return true;
}
return false;
}
/**
* Send command to all followers
*/
commandFollowers(command) {
if (!this.followerCommands.includes(command)) {
console.log(`❌ Invalid command: ${command}`);
return;
}
this.currentCommand = command;
this.followers.forEach(zombieId => {
const zombie = this.zombies.get(zombieId);
if (!zombie) return;
switch (command) {
case 'STOP':
zombie.status = 'idle';
zombie.currentTask = null;
break;
case 'HELP':
zombie.status = 'helping';
this.assignHelpTask(zombieId);
break;
case 'ATTACK':
zombie.status = 'combat';
this.assignCombatTask(zombieId);
break;
case 'HOME':
zombie.status = 'returning';
this.sendHome(zombieId);
break;
}
});
console.log(`📢 Commanded ${this.followers.length} followers: ${command}`);
this.scene.events.emit('notification', {
title: 'Follower Command',
message: `${this.followers.length} zombies: ${command}`,
icon: '📢'
});
}
/**
* Assign help task (gather, repair, build)
*/
assignHelpTask(zombieId) {
const zombie = this.zombies.get(zombieId);
if (!zombie) return;
// Find nearest task from queue
const task = this.findNearestTask(zombie.position);
if (task) {
zombie.currentTask = task;
task.assignedZombie = zombieId;
console.log(`🛠️ ${zombie.name} assigned to ${task.type}`);
} else {
// Follow player and gather resources
zombie.currentTask = { type: 'gather', target: 'player' };
}
}
/**
* Assign combat task
*/
assignCombatTask(zombieId) {
const zombie = this.zombies.get(zombieId);
if (!zombie) return;
zombie.currentTask = { type: 'defend', target: 'player', radius: 200 };
console.log(`⚔️ ${zombie.name} defending!`);
}
/**
* Send zombie home
*/
sendHome(zombieId) {
const zombie = this.zombies.get(zombieId);
if (!zombie) return;
zombie.currentTask = { type: 'return_home', target: { x: 0, y: 0 } };
console.log(`🏠 ${zombie.name} returning home.`);
}
/**
* Find nearest task for zombie
*/
findNearestTask(position) {
if (this.taskQueue.length === 0) return null;
let nearest = null;
let minDist = Infinity;
this.taskQueue.forEach(task => {
if (task.assignedZombie) return; // Already assigned
const dist = Phaser.Math.Distance.Between(
position.x, position.y,
task.position.x, task.position.y
);
if (dist < minDist) {
minDist = dist;
nearest = task;
}
});
return nearest;
}
/**
* Add task to queue
*/
addTask(type, position, priority = 1) {
const task = {
id: `task_${Date.now()}`,
type: type, // 'build', 'repair', 'gather', 'mine'
position: position,
priority: priority,
assignedZombie: null,
progress: 0,
required: 100
};
this.taskQueue.push(task);
// Auto-assign to independent workers
this.autoAssignIndependentWorkers();
return task.id;
}
/**
* Auto-assign tasks to independent workers (Lv8+)
*/
autoAssignIndependentWorkers() {
this.independentWorkers.forEach(zombieId => {
const zombie = this.zombies.get(zombieId);
if (!zombie || zombie.currentTask) return;
const task = this.findNearestTask(zombie.position);
if (task && zombie.abilities.includes('build_alone') || zombie.abilities.includes('repair_alone')) {
zombie.currentTask = task;
task.assignedZombie = zombieId;
console.log(`🤖 Independent: ${zombie.name} auto-assigned to ${task.type}`);
}
});
}
/**
* Create construction team (Lv10 leader required)
*/
createTeam(leaderZombieId, memberZombieIds) {
const leader = this.zombies.get(leaderZombieId);
if (!leader || leader.level < 10) {
console.log('❌ Team leader must be Lv10!');
return null;
}
const members = memberZombieIds
.map(id => this.zombies.get(id))
.filter(z => z && z.level >= 5);
if (members.length === 0) {
console.log('❌ Need at least 1 Lv5+ member!');
return null;
}
const teamId = `team_${Date.now()}`;
const team = {
id: teamId,
leader: leaderZombieId,
members: memberZombieIds,
status: 'ready',
currentProject: null,
efficiency: 1.0 + (members.length * 0.15) // +15% per member
};
this.teams.push(team);
console.log(`👥 Team created! Leader: ${leader.name}, Members: ${members.length}, Efficiency: ${team.efficiency}x`);
this.scene.events.emit('notification', {
title: 'Team Created!',
message: `${leader.name}'s team (${members.length} members) is ready!`,
icon: '👥'
});
return teamId;
}
/**
* Assign mega-project to team
*/
assignMegaProject(teamId, projectType, position) {
const team = this.teams.find(t => t.id === teamId);
if (!team) return false;
const project = {
type: projectType, // 'mega_barn', 'fortress', 'factory'
position: position,
progress: 0,
required: 1000, // Mega projects take time
efficiency: team.efficiency
};
team.currentProject = project;
team.status = 'working';
console.log(`🏗️ Team assigned to ${projectType}! Efficiency: ${team.efficiency}x`);
return true;
}
/**
* Update all zombies (called every frame)
*/
update(delta) {
// Update followers
this.followers.forEach(zombieId => {
const zombie = this.zombies.get(zombieId);
if (!zombie) return;
this.updateZombie(zombie, delta);
});
// Update independent workers
this.independentWorkers.forEach(zombieId => {
const zombie = this.zombies.get(zombieId);
if (!zombie) return;
this.updateZombie(zombie, delta);
});
// Update teams
this.teams.forEach(team => {
if (team.status === 'working' && team.currentProject) {
team.currentProject.progress += delta * 0.01 * team.efficiency;
if (team.currentProject.progress >= team.currentProject.required) {
this.completeMegaProject(team);
}
}
});
}
/**
* Update individual zombie
*/
updateZombie(zombie, delta) {
if (!zombie.currentTask) return;
const task = zombie.currentTask;
switch (task.type) {
case 'build':
case 'repair':
this.progressTask(zombie, task, delta);
break;
case 'gather':
this.gatherResources(zombie, delta);
break;
case 'defend':
this.defendArea(zombie);
break;
case 'return_home':
this.moveToHome(zombie);
break;
}
// Drain energy
zombie.energy = Math.max(0, zombie.energy - delta * 0.001);
}
/**
* Progress on a task
*/
progressTask(zombie, task, delta) {
const speedBonus = zombie.level >= 5 ? 1.25 : 1.0;
task.progress += delta * 0.02 * speedBonus;
if (task.progress >= task.required) {
this.completeTask(zombie, task);
}
}
/**
* Complete a task
*/
completeTask(zombie, task) {
console.log(`${zombie.name} completed ${task.type}!`);
// Award XP
this.addExperience(zombie.id, 50 + (task.priority * 10));
// Remove from queue
const index = this.taskQueue.indexOf(task);
if (index > -1) {
this.taskQueue.splice(index, 1);
}
zombie.currentTask = null;
// Find next task if independent
if (zombie.status === 'independent') {
this.autoAssignIndependentWorkers();
}
}
/**
* Gather resources
*/
gatherResources(zombie, delta) {
// Simple resource gathering simulation
if (Math.random() < 0.01) {
const resources = ['wood', 'stone', 'iron_ore'];
const resource = resources[Math.floor(Math.random() * resources.length)];
zombie.inventory.push(resource);
console.log(`📦 ${zombie.name} gathered ${resource}`);
}
}
/**
* Defend area around player/target
*/
defendArea(zombie) {
// Detect nearby enemies (placeholder)
// In real game, would check for hostile entities
}
/**
* Move zombie to home position
*/
moveToHome(zombie) {
const target = zombie.currentTask.target;
const dist = Phaser.Math.Distance.Between(
zombie.position.x, zombie.position.y,
target.x, target.y
);
if (dist < 10) {
zombie.status = 'idle';
zombie.currentTask = null;
console.log(`🏠 ${zombie.name} arrived home.`);
} else {
// Move toward home (simplified)
const angle = Phaser.Math.Angle.Between(
zombie.position.x, zombie.position.y,
target.x, target.y
);
zombie.position.x += Math.cos(angle) * 2;
zombie.position.y += Math.sin(angle) * 2;
}
}
/**
* Complete mega-project
*/
completeMegaProject(team) {
console.log(`🏆 Mega-project ${team.currentProject.type} COMPLETE!`);
const leader = this.zombies.get(team.leader);
if (leader) {
this.addExperience(leader.id, 500);
}
team.members.forEach(memberId => {
this.addExperience(memberId, 200);
});
this.scene.events.emit('notification', {
title: 'Mega-Project Complete!',
message: `${team.currentProject.type} is finished!`,
icon: '🏆'
});
team.currentProject = null;
team.status = 'ready';
}
/**
* Get zombie stats
*/
getZombieStats(zombieId) {
const zombie = this.zombies.get(zombieId);
if (!zombie) return null;
return {
name: zombie.name,
level: zombie.level,
intelligence: this.intelligenceLevels[zombie.level].name,
abilities: zombie.abilities,
status: zombie.status,
energy: Math.round(zombie.energy),
loyalty: zombie.loyalty,
experience: `${zombie.experience}/${zombie.experienceToNext}`,
inventory: zombie.inventory.length
};
}
}

View File

@@ -0,0 +1,460 @@
/**
* TOWN GROWTH SYSTEM - Population & Expansion
* Part of: Game Systems Expansion
* Created: January 4, 2026
*
* Features:
* - Dynamic town population (4 → 20 NPCs)
* - Town sign with live stats
* - Small villages (5-10 scattered across map)
* - Population unlock requirements
* - Town Services unlock based on population
*/
class TownGrowthSystem {
constructor(game) {
this.game = game;
this.player = game.player;
// Town stats
this.townName = 'Dolina Smrti';
this.population = 4; // Starting NPCs: Kai, Ana, Gronk, Baker
this.maxPopulation = 20;
this.populationSlots = [
{ index: 1, unlocked: true, npc: 'kai' },
{ index: 2, unlocked: true, npc: 'ana' },
{ index: 3, unlocked: true, npc: 'gronk' },
{ index: 4, unlocked: true, npc: 'baker' },
{ index: 5, unlocked: false, npc: null, requirement: { farmLevel: 2 } },
{ index: 6, unlocked: false, npc: null, requirement: { money: 10000 } },
{ index: 7, unlocked: false, npc: null, requirement: { quest: 'expand_town_1' } },
{ index: 8, unlocked: false, npc: null, requirement: { population: 6 } },
{ index: 9, unlocked: false, npc: null, requirement: { building: 'bakery' } },
{ index: 10, unlocked: false, npc: null, requirement: { building: 'barbershop' } },
{ index: 11, unlocked: false, npc: null, requirement: { hearts: 5, npcId: 'any' } },
{ index: 12, unlocked: false, npc: null, requirement: { quest: 'expand_town_2' } },
{ index: 13, unlocked: false, npc: null, requirement: { zombieWorkers: 5 } },
{ index: 14, unlocked: false, npc: null, requirement: { building: 'mine' } },
{ index: 15, unlocked: false, npc: null, requirement: { money: 50000 } },
{ index: 16, unlocked: false, npc: null, requirement: { quest: 'expand_town_3' } },
{ index: 17, unlocked: false, npc: null, requirement: { marriage: true } },
{ index: 18, unlocked: false, npc: null, requirement: { population: 15 } },
{ index: 19, unlocked: false, npc: null, requirement: { allBuildings: true } },
{ index: 20, unlocked: false, npc: null, requirement: { quest: 'town_master' } }
];
// Town sign
this.townSign = {
location: { x: 400, y: 300 },
visible: true,
displayMode: 'population' // 'population', 'status', 'full'
};
// Small villages
this.villages = this.initializeVillages();
// Town services (unlock based on population)
this.services = {
market: { unlocked: false, requiredPopulation: 6 },
hospital: { unlocked: false, requiredPopulation: 8 },
school: { unlocked: false, requiredPopulation: 10 },
bank: { unlocked: false, requiredPopulation: 12 },
museum: { unlocked: false, requiredPopulation: 15 },
theater: { unlocked: false, requiredPopulation: 18 }
};
}
/**
* Initialize small villages
*/
initializeVillages() {
return [
{
id: 'village_north',
name: 'Severna Vas',
position: { x: 3000, y: 500 },
population: 7,
discovered: false,
npcs: [
{ id: 'fisherman', name: 'Old Fisher', hobby: 'fishing' },
{ id: 'hunter', name: 'Hunter Dane', hobby: 'hunting' },
{ id: 'hermit', name: 'Wise Hermit', hobby: 'meditation' }
],
specialItems: ['ancient_fishing_rod', 'hunter_bow', 'meditation_mat']
},
{
id: 'village_east',
name: 'Vzhodna Stran',
position: { x: 5000, y: 2000 },
population: 5,
discovered: false,
npcs: [
{ id: 'blacksmith', name: 'Master Smith', hobby: 'forging' },
{ id: 'alchemist', name: 'Mysterious Alchemist', hobby: 'alchemy' }
],
specialItems: ['master_anvil', 'legendary_hammer', 'philosopher_stone']
},
{
id: 'village_south',
name: 'Južno Naselje',
position: { x: 2500, y: 4500 },
population: 6,
discovered: false,
npcs: [
{ id: 'trader', name: 'Traveling Trader', hobby: 'collecting' },
{ id: 'musician', name: 'Bard Luka', hobby: 'music' }
],
specialItems: ['exotic_seeds', 'rare_instruments', 'ancient_map']
},
{
id: 'village_west',
name: 'Zahodna Dolina',
position: { x: 500, y: 3000 },
population: 8,
discovered: false,
npcs: [
{ id: 'chef', name: 'Chef Antonio', hobby: 'cooking' },
{ id: 'librarian', name: 'Keeper of Books', hobby: 'reading' },
{ id: 'artist', name: 'Painter Ana', hobby: 'painting' }
],
specialItems: ['master_cookbook', 'ancient_tome', 'rare_pigments']
},
{
id: 'village_mysterious',
name: '??? Mystery Village',
position: { x: 4000, y: 4000 },
population: 10,
discovered: false,
hidden: true, // Only visible after completing special quest
npcs: [
{ id: 'time_keeper', name: 'Keeper of Time', hobby: 'timekeeping' },
{ id: 'oracle', name: 'Oracle of Dolina', hobby: 'prophecy' }
],
specialItems: ['time_crystal', 'prophecy_scroll', 'reality_gem']
}
];
}
/**
* Check and unlock new population slot
*/
checkPopulationUnlocks() {
let newUnlocks = 0;
this.populationSlots.forEach(slot => {
if (!slot.unlocked && slot.requirement) {
if (this.meetsRequirement(slot.requirement)) {
slot.unlocked = true;
newUnlocks++;
this.game.showMessage(
`New population slot unlocked! (${this.population}/${this.maxPopulation})`
);
}
}
});
if (newUnlocks > 0) {
this.updateTownServices();
}
return newUnlocks;
}
/**
* Check if requirement is met
*/
meetsRequirement(requirement) {
// Farm level
if (requirement.farmLevel) {
if (this.player.farmLevel < requirement.farmLevel) {
return false;
}
}
// Money
if (requirement.money) {
if (this.player.money < requirement.money) {
return false;
}
}
// Quest
if (requirement.quest) {
if (!this.player.hasCompletedQuest(requirement.quest)) {
return false;
}
}
// Building
if (requirement.building) {
if (!this.player.hasBuilding(requirement.building)) {
return false;
}
}
// Current population
if (requirement.population) {
if (this.population < requirement.population) {
return false;
}
}
// Zombie workers
if (requirement.zombieWorkers) {
const zombieCount = this.game.zombieWorkers?.getWorkerCount() || 0;
if (zombieCount < requirement.zombieWorkers) {
return false;
}
}
// Marriage
if (requirement.marriage) {
if (!this.player.isMarried) {
return false;
}
}
// Hearts with any NPC
if (requirement.hearts && requirement.npcId === 'any') {
const hasHighRelationship = this.game.npcs.getAllNPCs()
.some(npc => npc.relationshipHearts >= requirement.hearts);
if (!hasHighRelationship) {
return false;
}
}
// All buildings
if (requirement.allBuildings) {
const requiredBuildings = ['bakery', 'barbershop', 'lawyer', 'mine', 'hospital'];
if (!requiredBuildings.every(b => this.player.hasBuilding(b))) {
return false;
}
}
return true;
}
/**
* Invite new NPC to town
*/
inviteNPC(npcId) {
// Find available slot
const availableSlot = this.populationSlots.find(
slot => slot.unlocked && slot.npc === null
);
if (!availableSlot) {
return {
success: false,
message: 'No available population slots!'
};
}
// Check if NPC exists
const npcData = this.game.npcs.getNPCData(npcId);
if (!npcData) {
return { success: false, message: 'NPC not found!' };
}
// Assign NPC to slot
availableSlot.npc = npcId;
// Spawn NPC in town
this.game.npcs.spawn(npcId, {
homeLocation: 'town',
moveInDate: this.game.time.currentDate
});
// Increase population
this.population++;
// Update town sign
this.updateTownSign();
// Check for service unlocks
this.updateTownServices();
this.game.showMessage(
`${npcData.name} moved to town! Population: ${this.population}/${this.maxPopulation}`
);
return { success: true, npc: npcData };
}
/**
* Update town services based on population
*/
updateTownServices() {
let newServices = [];
Object.entries(this.services).forEach(([serviceId, service]) => {
if (!service.unlocked && this.population >= service.requiredPopulation) {
service.unlocked = true;
newServices.push(serviceId);
this.game.showMessage(
`🏛️ New service unlocked: ${serviceId}! (Pop: ${this.population})`
);
// Trigger service built event
this.game.emit('serviceUnlocked', {
serviceId: serviceId,
population: this.population
});
}
});
return newServices;
}
/**
* Update town sign display
*/
updateTownSign() {
const signData = {
townName: this.townName,
population: this.population,
maxPopulation: this.maxPopulation,
status: this.getTownStatus(),
services: Object.keys(this.services).filter(s => this.services[s].unlocked).length
};
// Emit event to update sign sprite
this.game.emit('townSignUpdate', signData);
}
/**
* Get town status description
*/
getTownStatus() {
if (this.population >= 18) {
return 'Thriving City';
}
if (this.population >= 15) {
return 'Prosperous Town';
}
if (this.population >= 10) {
return 'Growing Town';
}
if (this.population >= 6) {
return 'Small Town';
}
return 'Village';
}
/**
* Discover village
*/
discoverVillage(villageId) {
const village = this.villages.find(v => v.id === villageId);
if (!village) {
return { success: false };
}
if (village.discovered) {
return {
success: false,
message: 'Village already discovered!'
};
}
// Check if hidden village requires quest
if (village.hidden && !this.player.hasCompletedQuest('find_mystery_village')) {
return {
success: false,
message: 'This village remains hidden...'
};
}
// Discover village
village.discovered = true;
// Spawn village NPCs
village.npcs.forEach(npcData => {
this.game.npcs.spawn(npcData.id, {
homeLocation: villageId,
position: village.position,
hobby: npcData.hobby
});
});
// Mark special items as available
village.specialItems.forEach(itemId => {
this.game.items.markAsDiscovered(itemId, villageId);
});
this.game.showMessage(
`Discovered ${village.name}! Population: ${village.population} NPCs`
);
// Achievement
const discoveredCount = this.villages.filter(v => v.discovered).length;
if (discoveredCount === this.villages.length) {
this.game.achievements.unlock('village_explorer');
}
return { success: true, village: village };
}
/**
* Get travel distance to village
*/
getTravelDistance(villageId) {
const village = this.villages.find(v => v.id === villageId);
if (!village) return null;
const playerPos = this.player.getPosition();
const dx = village.position.x - playerPos.x;
const dy = village.position.y - playerPos.y;
return Math.sqrt(dx * dx + dy * dy);
}
/**
* Get village trade options
*/
getVillageTradeOptions(villageId) {
const village = this.villages.find(v => v.id === villageId);
if (!village || !village.discovered) return null;
return {
villageName: village.name,
npcs: village.npcs,
specialItems: village.specialItems,
population: village.population
};
}
/**
* Get town growth UI data
*/
getTownGrowthUIData() {
return {
townName: this.townName,
population: this.population,
maxPopulation: this.maxPopulation,
status: this.getTownStatus(),
populationSlots: this.populationSlots,
availableSlots: this.populationSlots.filter(s => s.unlocked && !s.npc).length,
services: this.services,
villages: this.villages.filter(v => !v.hidden || v.discovered),
discoveredVillages: this.villages.filter(v => v.discovered).length,
totalVillages: this.villages.filter(v => !v.hidden).length
};
}
/**
* Update (check for new unlocks)
*/
update() {
// Check for new population slot unlocks
this.checkPopulationUnlocks();
// Update town sign
this.updateTownSign();
}
}

View File

@@ -0,0 +1,474 @@
/**
* TOWN RESTORATION LOGIC SYSTEM
* Complete logic for restoring buildings in Mrtva Dolina
* Handles material requirements, construction progress, worker assignments, NPC unlocks
*/
export class TownRestorationLogic {
constructor(scene) {
this.scene = scene;
// Building states
this.buildings = new Map(); //buildingId → buildingData
// Construction queues
this.activeConstructions = [];
// Worker management
this.assignedWorkers = new Map(); // buildingId → [workerIds]
// NPC unlock tracking
this.unlockedNPCs = new Set();
this.init();
}
init() {
this.initializeBuildingDatabase();
}
/**
* Initialize all 14 buildings with restoration data
*/
initializeBuildingDatabase() {
const buildingSpecs = [
{
id: 'hospital',
name: 'Hospital',
npcUnlock: 'Ana', // Doctor Ana unlocked
materials: {
wood: 150,
stone: 100,
metal: 50,
tools: 20
},
constructionTime: 3600, // 1 hour
stages: 3,
benefits: ['healing', 'medical_supplies']
},
{
id: 'police',
name: 'Police Station',
npcUnlock: null,
materials: {
wood: 100,
stone: 150,
metal: 80,
weapons: 10
},
constructionTime: 3600,
stages: 3,
benefits: ['security', 'patrol_unlock']
},
{
id: 'mayor_office',
name: "Mayor's Office",
npcUnlock: 'Župan', // Mayor unlocked
materials: {
wood: 120,
stone: 80,
metal: 40,
papers: 50
},
constructionTime: 2400,
stages: 3,
benefits: ['election_unlock', 'city_management']
},
{
id: 'tech_workshop',
name: 'Tech Workshop',
npcUnlock: 'Tehnik', // Technician unlocked
materials: {
wood: 80,
stone: 60,
metal: 150,
electronics: 30
},
constructionTime: 3000,
stages: 3,
benefits: ['tech_upgrades', 'electronics_crafting']
},
{
id: 'tailor',
name: 'Tailor Shop',
npcUnlock: 'Šivilja', // Seamstress unlocked
materials: {
wood: 60,
fabric: 100,
thread: 50,
tools: 15
},
constructionTime: 1800,
stages: 3,
benefits: ['armor_crafting', 'clothing_upgrades']
},
{
id: 'museum',
name: 'Museum',
npcUnlock: 'Kustos', // Curator unlocked
materials: {
wood: 100,
stone: 120,
glass: 40,
artifacts: 10
},
constructionTime: 4800, // 80 minutes - complex restoration
stages: 3,
benefits: ['lore_unlock', 'artifact_collection']
},
{
id: 'school',
name: 'School',
npcUnlock: 'Teacher', // Teacher unlocked
materials: {
wood: 90,
stone: 70,
books: 50,
tools: 20
},
constructionTime: 3600,
stages: 3,
benefits: ['buff_unlock', 'education_system']
},
{
id: 'church',
name: 'Church',
npcUnlock: 'Župnik', // Priest unlocked
materials: {
wood: 80,
stone: 200,
metal: 30,
religious_items: 5
},
constructionTime: 5400, // 90 minutes - sacred restoration
stages: 3,
benefits: ['blessing_system', 'graveyard_access']
},
{
id: 'blacksmith',
name: 'Blacksmith',
npcUnlock: 'Ivan Kovač', // Already available
materials: {
wood: 50,
stone: 100,
metal: 120,
coal: 80
},
constructionTime: 3000,
stages: 3,
benefits: ['weapon_crafting', 'tool_upgrades']
},
{
id: 'bakery',
name: 'Bakery',
npcUnlock: 'Pek', // Baker unlocked
materials: {
wood: 70,
stone: 50,
flour: 100,
tools: 10
},
constructionTime: 2400,
stages: 3,
benefits: ['food_production', 'energy_bonus']
}
];
buildingSpecs.forEach(spec => {
this.buildings.set(spec.id, {
...spec,
currentState: 'ruined', // ruined, under_construction, restored
currentStage: 0, // 0 = ruined, 1-3 = construction stages
progress: 0, // 0-100% construction progress
workersAssigned: 0,
materialsPaid: false,
completionTime: null
});
});
}
/**
* Check if player can start building restoration
*/
canStartRestoration(buildingId) {
const building = this.buildings.get(buildingId);
if (!building) return { canStart: false, reason: 'Building not found' };
if (building.currentState !== 'ruined') {
return { canStart: false, reason: 'Building already restored or under construction' };
}
// Check materials
const inventory = this.scene.inventorySystem;
for (const [material, amount] of Object.entries(building.materials)) {
if (!inventory.hasItem(material, amount)) {
return {
canStart: false,
reason: `Not enough ${material}. Need ${amount}, have ${inventory.getItemCount(material)}`
};
}
}
return { canStart: true };
}
/**
* Start building restoration
*/
startRestoration(buildingId) {
const check = this.canStartRestoration(buildingId);
if (!check.canStart) {
console.warn(`Cannot start restoration: ${check.reason}`);
return false;
}
const building = this.buildings.get(buildingId);
const inventory = this.scene.inventorySystem;
// Deduct materials
for (const [material, amount] of Object.entries(building.materials)) {
inventory.removeItem(material, amount);
}
// Start construction
building.currentState = 'under_construction';
building.currentStage = 1;
building.progress = 0;
building.materialsPaid = true;
building.completionTime = Date.now() + (building.constructionTime * 1000);
this.activeConstructions.push(buildingId);
// Spawn scaffolding visual
this.spawnConstructionVisuals(buildingId);
console.log(`Started restoration of ${building.name}`);
return true;
}
/**
* Assign workers to speed up construction
*/
assignWorker(buildingId, workerType = 'zombie') {
const building = this.buildings.get(buildingId);
if (!building || building.currentState !== 'under_construction') return false;
const speedBonus = {
'zombie': 0.1, // 10% faster per zombie
'human': 0.25, // 25% faster per human NPC
'kai': 0.5 // Kai works fastest
};
building.workersAssigned++;
if (!this.assignedWorkers.has(buildingId)) {
this.assignedWorkers.set(buildingId, []);
}
this.assignedWorkers.get(buildingId).push({ type: workerType, bonus: speedBonus[workerType] });
console.log(`Assigned ${workerType} to ${building.name}. Speed bonus: +${speedBonus[workerType] * 100}%`);
return true;
}
/**
* Update construction progress
*/
update(deltaTime) {
this.activeConstructions.forEach(buildingId => {
const building = this.buildings.get(buildingId);
// Calculate progress speed (base: 100% over constructionTime)
let baseSpeed = (100 / building.constructionTime) * (deltaTime / 1000);
// Apply worker bonuses
if (this.assignedWorkers.has(buildingId)) {
const workers = this.assignedWorkers.get(buildingId);
const totalBonus = workers.reduce((sum, w) => sum + w.bonus, 0);
baseSpeed *= (1 + totalBonus);
}
building.progress += baseSpeed;
// Check stage transitions
if (building.progress >= 33 && building.currentStage === 1) {
building.currentStage = 2;
this.updateBuildingVisual(buildingId, 2);
} else if (building.progress >= 66 && building.currentStage === 2) {
building.currentStage = 3;
this.updateBuildingVisual(buildingId, 3);
}
// Check completion
if (building.progress >= 100) {
this.completeRestoration(buildingId);
}
});
}
/**
* Complete building restoration
*/
completeRestoration(buildingId) {
const building = this.buildings.get(buildingId);
building.currentState = 'restored';
building.currentStage = 3;
building.progress = 100;
// Remove from active constructions
const index = this.activeConstructions.indexOf(buildingId);
if (index > -1) this.activeConstructions.splice(index, 1);
// Remove scaffolding
this.removeConstructionVisuals(buildingId);
// Update building sprite to restored
this.updateBuildingVisual(buildingId, 'restored');
// VFX: Building restoration sparkles
this.scene.vfxSystem?.playEffect('building_restoration', building.x, building.y);
// Unlock NPC if applicable
if (building.npcUnlock) {
this.unlockNPC(building.npcUnlock);
}
// Grant benefits
this.grantBuildingBenefits(buildingId);
console.log(`${building.name} restored! NPC unlocked: ${building.npcUnlock || 'None'}`);
// Quest completion check
this.scene.questSystem?.checkBuildingRestoration(buildingId);
}
/**
* Unlock NPC after building restoration
*/
unlockNPC(npcName) {
if (this.unlockedNPCs.has(npcName)) return;
this.unlockedNPCs.add(npcName);
// Spawn NPC in town
const npcData = this.getNPCSpawnData(npcName);
this.scene.npcSystem?.spawnNPC(npcName, npcData.x, npcData.y);
// Notification
this.scene.uiSystem?.showNotification(`${npcName} has arrived in Mrtva Dolina!`, 'success');
console.log(`🎉 NPC unlocked: ${npcName}`);
}
/**
* Get NPC spawn location based on their building
*/
getNPCSpawnData(npcName) {
const spawnLocations = {
'Ana': { x: 1200, y: 800 }, // Hospital
'Župan': { x: 1400, y: 700 }, // Mayor's Office
'Tehnik': { x: 1100, y: 900 }, // Tech Workshop
'Šivilja': { x: 1300, y: 1000 }, // Tailor
'Kustos': { x: 1500, y: 600 }, // Museum
'Teacher': { x: 1000, y: 700 }, // School
'Župnik': { x: 1600, y: 500 }, // Church
'Pek': { x: 900, y: 800 } // Bakery
};
return spawnLocations[npcName] || { x: 1200, y: 800 };
}
/**
* Grant building benefits to player
*/
grantBuildingBenefits(buildingId) {
const building = this.buildings.get(buildingId);
building.benefits.forEach(benefit => {
switch (benefit) {
case 'healing':
this.scene.gameState.unlocks.healing = true;
break;
case 'security':
this.scene.defenseSystem?.unlockPatrols();
break;
case 'election_unlock':
this.scene.electionSystem?.unlockElections();
break;
case 'tech_upgrades':
this.scene.craftingSystem?.unlockCategory('electronics');
break;
case 'armor_crafting':
this.scene.craftingSystem?.unlockCategory('armor');
break;
case 'lore_unlock':
this.scene.gameState.unlocks.museum = true;
break;
case 'buff_unlock':
this.scene.schoolSystem?.enable();
break;
case 'blessing_system':
this.scene.churchSystem?.enable();
break;
case 'weapon_crafting':
this.scene.craftingSystem?.unlockCategory('weapons');
break;
case 'food_production':
this.scene.gameState.unlocks.bakery = true;
break;
}
});
}
/**
* Visual updates
*/
spawnConstructionVisuals(buildingId) {
// Spawn scaffolding sprite
// Spawn worker NPCs/zombies
// Add construction sounds
}
removeConstructionVisuals(buildingId) {
// Remove scaffolding
// Remove workers
// Stop construction sounds
}
updateBuildingVisual(buildingId, stageOrState) {
// Update building sprite to show construction progress or restored state
// stages: 1 (10-30% built), 2 (30-70% built), 3 (70-100% built)
// 'restored' = final completed building
}
/**
* Get building restoration status
*/
getBuildingStatus(buildingId) {
return this.buildings.get(buildingId);
}
/**
* Get all restorable buildings
*/
getAllBuildings() {
return Array.from(this.buildings.values());
}
/**
* Get restoration progress summary
*/
getRestorationProgress() {
const total = this.buildings.size;
const restored = Array.from(this.buildings.values()).filter(b => b.currentState === 'restored').length;
const underConstruction = this.activeConstructions.length;
return {
total,
restored,
underConstruction,
ruined: total - restored - underConstruction,
percentComplete: Math.round((restored / total) * 100)
};
}
}

View File

@@ -0,0 +1,403 @@
/**
* TownRestorationSystem.js
* =========================
* KRVAVA ŽETEV - Complete Town Restoration System (Phase 19)
*
* Features:
* - 27 towns across 18 biomes
* - 150+ ruined buildings
* - 180 total NPCs
* - Building restoration mechanics
* - NPC move-in system
* - Global milestones & rewards
* - Major city endgame project
*
* @author NovaFarma Team
* @date 2025-12-23
*/
class TownRestorationSystem {
constructor(scene) {
this.scene = scene;
// Town registry
this.towns = new Map();
this.buildings = new Map();
this.npcs = new Map();
// Progress tracking
this.buildingsRestored = 0;
this.totalBuildings = 150;
this.npcsRescued = 0;
this.totalNPCs = 180;
this.townsCompleted = 0;
this.totalTowns = 27;
// Milestones
this.milestones = [10, 25, 50, 100, 180];
this.unlockedMilestones = [];
console.log('🏗️ TownRestorationSystem initialized');
// Register all towns
this.registerTowns();
// Register Hope Valley buildings
this.registerHopeValleyBuildings();
}
/**
* Register all towns
*/
registerTowns() {
// Hope Valley (Starting town - 15 buildings)
this.towns.set('hope_valley', {
id: 'hope_valley',
name: 'Hope Valley',
biome: 'grassland',
icon: '🏘️',
totalBuildings: 15,
restoredBuildings: 0,
npcs: 0,
isUnlocked: true,
isCompleted: false
});
// Other towns (6 more buildings each = 156 total)
const otherTowns = [
{ id: 'forest_grove', name: 'Forest Grove', biome: 'forest', buildings: 6 },
{ id: 'desert_oasis', name: 'Desert Oasis', biome: 'desert', buildings: 6 },
{ id: 'frozen_harbor', name: 'Frozen Harbor', biome: 'frozen', buildings: 6 },
{ id: 'volcanic_refuge', name: 'Volcanic Refuge', biome: 'volcanic', buildings: 6 },
{ id: 'coastal_bay', name: 'Coastal Bay', biome: 'beach', buildings: 6 },
{ id: 'mountain_peak', name: 'Mountain Peak', biome: 'mountain', buildings: 6 },
{ id: 'swamp_village', name: 'Swamp Village', biome: 'swamp', buildings: 6 },
{ id: 'crystal_city', name: 'Crystal City', biome: 'crystal', buildings: 8 },
{ id: 'atlantis', name: 'Atlantis', biome: 'underwater', buildings: 10 },
// ... (27 total towns)
];
otherTowns.forEach(town => {
this.towns.set(town.id, {
id: town.id,
name: town.name,
biome: town.biome,
icon: '🏘️',
totalBuildings: town.buildings,
restoredBuildings: 0,
npcs: 0,
isUnlocked: false,
isCompleted: false
});
});
console.log(`✅ Registered ${this.towns.size} towns`);
}
/**
* Register Hope Valley buildings (15)
*/
registerHopeValleyBuildings() {
const buildings = [
// Essential (NPCs' homes)
{ id: 'ivan_house', name: "Ivan's House", npc: 'Ivan', materials: { wood: 100, stone: 50 }, time: 3 },
{ id: 'marija_house', name: "Marija's House", npc: 'Marija', materials: { wood: 100, stone: 50 }, time: 3 },
{ id: 'jakob_shop', name: "Jakob's Shop", npc: 'Jakob', materials: { wood: 150, iron: 30 }, time: 4 },
{ id: 'dr_chen_clinic', name: "Dr. Chen's Clinic", npc: 'Dr. Chen', materials: { wood: 200, stone: 100 }, time: 5 },
{ id: 'lena_bakery', name: "Lena's Bakery", npc: 'Lena', materials: { wood: 120, stone: 60 }, time: 4 },
// Community buildings
{ id: 'town_hall', name: 'Town Hall', materials: { wood: 500, stone: 300, iron: 100 }, time: 10 },
{ id: 'community_center', name: 'Community Center', materials: { wood: 400, stone: 250 }, time: 8 },
{ id: 'library', name: 'Library', materials: { wood: 300, stone: 150 }, time: 6 },
{ id: 'school', name: 'School', materials: { wood: 350, stone: 200 }, time: 7 },
{ id: 'guard_tower', name: 'Guard Tower', materials: { stone: 400, iron: 150 }, time: 8 },
// Utility buildings
{ id: 'warehouse', name: 'Warehouse', materials: { wood: 250, stone: 150 }, time: 5 },
{ id: 'water_tower', name: 'Water Tower', materials: { stone: 300, iron: 100 }, time: 6 },
{ id: 'power_station', name: 'Power Station', materials: { stone: 400, iron: 200, wire: 50 }, time: 9 },
{ id: 'market', name: 'Market Square', materials: { wood: 200, stone: 100 }, time: 5 },
{ id: 'fountain', name: 'Town Fountain', materials: { stone: 150, marble: 50 }, time: 4 }
];
buildings.forEach(building => {
this.buildings.set(building.id, {
...building,
town: 'hope_valley',
isRestored: false,
progress: 0,
workers: [],
startTime: null
});
});
console.log(`✅ Registered ${buildings.length} Hope Valley buildings`);
}
/**
* Start building restoration
*/
startRestoration(buildingId, zombieWorkers = []) {
const building = this.buildings.get(buildingId);
if (!building) {
console.error(`Building ${buildingId} not found!`);
return false;
}
if (building.isRestored) {
console.log(`${building.name} is already restored!`);
return false;
}
// Check materials
if (!this.hasMaterials(building.materials)) {
console.log(`Not enough materials for ${building.name}!`);
return false;
}
// Consume materials
this.consumeMaterials(building.materials);
// Assign workers
building.workers = zombieWorkers;
building.startTime = Date.now();
building.progress = 0;
// Calculate construction time (base time reduced by workers)
const workerBonus = zombieWorkers.length * 0.2; // 20% per worker
const constructionTime = building.time * (1 - workerBonus);
console.log(`🏗️ Started restoring ${building.name} (${constructionTime} days with ${zombieWorkers.length} workers)`);
// Auto-complete after time
setTimeout(() => {
this.completeRestoration(buildingId);
}, constructionTime * 1000 * 60); // Convert to ms (for demo, 1 day = 1 min)
this.showNotification({
title: 'Restoration Started!',
text: `🏗️ ${building.name} - ${constructionTime} days`,
icon: '🔨'
});
return true;
}
/**
* Complete building restoration
*/
completeRestoration(buildingId) {
const building = this.buildings.get(buildingId);
if (!building) return;
building.isRestored = true;
building.progress = 100;
this.buildingsRestored++;
console.log(`${building.name} restored! (${this.buildingsRestored}/${this.totalBuildings})`);
// Move in NPC if building has one
if (building.npc) {
this.moveInNPC(building.npc, building.town);
}
// Update town progress
this.updateTownProgress(building.town);
// Check milestones
this.checkMilestones();
// Update visuals in the current scene
if (this.scene.updateBuildingVisuals) {
this.scene.updateBuildingVisuals(buildingId);
}
this.showNotification({
title: 'Building Complete!',
text: `${building.name} restored!`,
icon: '🏗️'
});
}
/**
* Move in NPC
*/
moveInNPC(npcName, townId) {
const npc = {
name: npcName,
town: townId,
movedInDate: Date.now(),
hearts: 0,
giftsGiven: 0,
questsCompleted: 0
};
this.npcs.set(npcName, npc);
this.npcsRescued++;
console.log(`👤 ${npcName} moved into ${townId}! (${this.npcsRescued}/${this.totalNPCs})`);
// Update town NPC count
const town = this.towns.get(townId);
if (town) {
town.npcs++;
}
this.showNotification({
title: 'NPC Moved In!',
text: `👤 ${npcName} is now living in town!`,
icon: '🏠'
});
}
/**
* Update town progress
*/
updateTownProgress(townId) {
const town = this.towns.get(townId);
if (!town) return;
// Count restored buildings in this town
const townBuildings = Array.from(this.buildings.values())
.filter(b => b.town === townId);
const restored = townBuildings.filter(b => b.isRestored).length;
town.restoredBuildings = restored;
// Check if town is complete
if (restored === town.totalBuildings) {
town.isCompleted = true;
this.townsCompleted++;
console.log(`🏆 ${town.name} 100% COMPLETE! (${this.townsCompleted}/${this.totalTowns})`);
this.showNotification({
title: 'TOWN COMPLETE!',
text: `🏆 ${town.name} fully restored!`,
icon: '🎉'
});
}
}
/**
* Check milestones
*/
checkMilestones() {
this.milestones.forEach(milestone => {
if (this.npcsRescued >= milestone && !this.unlockedMilestones.includes(milestone)) {
this.unlockMilestone(milestone);
}
});
}
/**
* Unlock milestone
*/
unlockMilestone(milestone) {
this.unlockedMilestones.push(milestone);
console.log(`🏆 MILESTONE: ${milestone} NPCs rescued!`);
// Grant rewards
const rewards = this.getMilestoneReward(milestone);
this.showNotification({
title: `MILESTONE: ${milestone} NPCs!`,
text: `🏆 Unlocked: ${rewards.text}`,
icon: '🎉'
});
}
/**
* Get milestone reward
*/
getMilestoneReward(milestone) {
const rewards = {
10: { text: 'Community Center unlocked!', feature: 'community_center' },
25: { text: 'Town Festivals enabled!', feature: 'festivals' },
50: { text: 'Town Guard system!', feature: 'town_guard' },
100: { text: 'Major City project unlocked!', feature: 'major_city' },
180: { text: 'UTOPIA ENDING unlocked!', feature: 'utopia_ending' }
};
return rewards[milestone] || { text: 'Bonus unlocked!' };
}
hasMaterials(materials) {
if (!this.scene.inventorySystem) return true; // Safety fallback
for (const [item, count] of Object.entries(materials)) {
if (!this.scene.inventorySystem.hasItem(item, count)) {
return false;
}
}
return true;
}
consumeMaterials(materials) {
if (!this.scene.inventorySystem) return;
for (const [item, count] of Object.entries(materials)) {
this.scene.inventorySystem.removeItem(item, count);
}
console.log('📦 Materials consumed:', materials);
}
/**
* Get restoration progress
*/
getProgress() {
return {
buildings: {
restored: this.buildingsRestored,
total: this.totalBuildings,
percentage: ((this.buildingsRestored / this.totalBuildings) * 100).toFixed(1)
},
npcs: {
rescued: this.npcsRescued,
total: this.totalNPCs,
percentage: ((this.npcsRescued / this.totalNPCs) * 100).toFixed(1)
},
towns: {
completed: this.townsCompleted,
total: this.totalTowns,
percentage: ((this.townsCompleted / this.totalTowns) * 100).toFixed(1)
},
milestones: this.unlockedMilestones
};
}
/**
* Get town info
*/
getTownInfo(townId) {
return this.towns.get(townId);
}
/**
* Get all towns
*/
getAllTowns() {
return Array.from(this.towns.values());
}
/**
* Get building info
*/
getBuildingInfo(buildingId) {
return this.buildings.get(buildingId);
}
/**
* Helper: Show notification
*/
showNotification(notification) {
console.log(`📢 ${notification.icon} ${notification.title}: ${notification.text}`);
const ui = this.scene.scene.get('UIScene');
if (ui && ui.showNotification) {
ui.showNotification(notification);
}
}
}

View File

@@ -0,0 +1,432 @@
/**
* TwinBondSystem.js
* =================
* KRVAVA ŽETEV - Twin Bond Mechanic (Kai ↔ Ana Connection)
*
* Core Concept:
* Kai and Ana share a psychic bond through the Alfa virus
* As twins, they can:
* - Feel each other's emotions
* - Sense each other's location (vaguely)
* - Communicate telepathically (limited)
* - Share HP/stamina in emergencies
*
* Features:
* - Bond Strength meter (0-100)
* - Telepathic messages from Ana
* - Direction to Ana indicator
* - Twin abilities (heal twin, boost twin, swap positions)
* - Bond events (visions, flashbacks)
* - Ana's status tracking (health, danger level)
*
* @author NovaFarma Team
* @date 2025-12-23
*/
class TwinBondSystem {
constructor(scene) {
this.scene = scene;
// Bond state
this.bondStrength = 75; // Starts strong (0-100)
this.maxBondStrength = 100;
// Ana's state (unknown location)
this.anaState = {
alive: true,
health: 100,
dangerLevel: 0, // 0 = safe, 100 = critical
distance: 5000, // pixels from Kai (initially far)
direction: { x: 1000, y: 1000 }, // General direction
lastMessage: null,
messageTime: null
};
// Bond abilities
this.abilities = {
telepathy: { unlocked: true, cooldown: 0, maxCooldown: 30000 }, // 30s
sensePulse: { unlocked: true, cooldown: 0, maxCooldown: 60000 }, // 1min
emergencyLink: { unlocked: false, cooldown: 0, maxCooldown: 300000 }, // 5min
twinRecall: { unlocked: false, cooldown: 0, maxCooldown: 600000 } // 10min
};
// Messages from Ana (telepathic)
this.messageQueue = [];
this.lastMessageTime = 0;
// UI elements
this.bondUI = null;
// Events
this.bondEvents = this.defineBondEvents();
this.nextEventTime = Date.now() + 60000; // First event in 1 minute
console.log('💞 TwinBondSystem initialized - Bond Strength:', this.bondStrength);
}
/**
* Update bond strength based on actions
*/
update(delta) {
const deltaSeconds = delta / 1000;
// Passive bond decay (small)
this.bondStrength = Math.max(0, this.bondStrength - 0.01 * deltaSeconds);
// Update ability cooldowns
for (const ability in this.abilities) {
if (this.abilities[ability].cooldown > 0) {
this.abilities[ability].cooldown -= delta;
}
}
// Check for random bond events
if (Date.now() > this.nextEventTime) {
this.triggerRandomBondEvent();
this.nextEventTime = Date.now() + Phaser.Math.Between(60000, 180000); // 1-3 min
}
// Update Ana's danger level based on story progress
this.updateAnaDanger(deltaSeconds);
}
/**
* Define bond events (telepathic visions)
*/
defineBondEvents() {
return [
{
id: 'first_contact',
trigger: 'auto',
condition: null,
message: 'Kai... can you hear me? I\'m... somewhere dark...',
emotion: 'worried',
bondChange: +5
},
{
id: 'danger_warning',
trigger: 'danger_high',
condition: () => this.anaState.dangerLevel > 70,
message: 'Brother! They\'re coming for me! Please hurry!',
emotion: 'fear',
bondChange: -10
},
{
id: 'memory_flash',
trigger: 'random',
condition: null,
message: 'Remember when we first discovered the Alfa strain? We were so hopeful...',
emotion: 'sad',
bondChange: +3
},
{
id: 'location_hint',
trigger: 'sense_pulse',
condition: null,
message: 'I can feel concrete walls... cold metal... some kind of facility?',
emotion: 'neutral',
bondChange: +5
},
{
id: 'encouragement',
trigger: 'random',
condition: () => this.scene.player?.hp < 50,
message: 'Stay strong, Kai! I believe in you!',
emotion: 'determined',
bondChange: +10
}
];
}
/**
* Trigger a random bond event
*/
triggerRandomBondEvent() {
const randomEvents = this.bondEvents.filter(e => e.trigger === 'random');
if (randomEvents.length === 0) return;
const event = Phaser.Utils.Array.GetRandom(randomEvents);
// Check condition
if (event.condition && !event.condition()) {
return;
}
this.showTelepathicMessage(event.message, event.emotion);
this.changeBondStrength(event.bondChange);
}
/**
* Show telepathic message from Ana
*/
showTelepathicMessage(message, emotion = 'neutral') {
console.log(`💭 Twin Bond Message: ${message}`);
// Update Ana's last message
this.anaState.lastMessage = message;
this.anaState.messageTime = Date.now();
// Show in dialogue system
if (this.scene.dialogueSystem) {
const anaData = {
name: 'Ana (Twin Bond)',
id: 'ana_telepathy'
};
// Create temporary dialogue
const telepathyDialogue = {
root: 'message',
nodes: {
'message': {
speaker: 'Ana (Twin Bond)',
emotion: emotion,
text: message,
next: null // Auto-close
}
}
};
this.scene.dialogueSystem.registerDialogue('telepathy_' + Date.now(), telepathyDialogue);
this.scene.dialogueSystem.startDialogue('telepathy_' + Date.now(), anaData);
}
// Visual effect (bond pulse)
this.scene.cameras.main.flash(500, 150, 100, 255, false);
// Bond strength change
this.showBondPulse();
}
/**
* Change bond strength
*/
changeBondStrength(amount) {
this.bondStrength = Phaser.Math.Clamp(
this.bondStrength + amount,
0,
this.maxBondStrength
);
console.log(`💞 Bond Strength: ${this.bondStrength.toFixed(1)}% (${amount > 0 ? '+' : ''}${amount})`);
// Notify player
if (amount > 0) {
this.scene.events.emit('bondStrengthened', { strength: this.bondStrength });
} else {
this.scene.events.emit('bondWeakened', { strength: this.bondStrength });
}
}
/**
* Visual bond pulse effect
*/
showBondPulse() {
// TODO: Create particle effect at player position
console.log('💫 Bond pulse visualization');
}
/**
* Ability: Telepathy (send message to Ana)
*/
useTelepathy(message) {
if (!this.abilities.telepathy.unlocked) {
console.log('❌ Telepathy not unlocked');
return false;
}
if (this.abilities.telepathy.cooldown > 0) {
console.log('⏸️ Telepathy on cooldown');
return false;
}
console.log(`📡 Sending to Ana: ${message}`);
// Ana responds after delay
this.scene.time.delayedCall(2000, () => {
const responses = [
"I heard you! Keep searching!",
"Kai... I'm trying to stay strong...",
"They don't know about our bond. Use that!",
"I can feel you getting closer!"
];
const response = Phaser.Utils.Array.GetRandom(responses);
this.showTelepathicMessage(response, 'determined');
});
// Set cooldown
this.abilities.telepathy.cooldown = this.abilities.telepathy.maxCooldown;
this.changeBondStrength(+2); // Strengthen bond
return true;
}
/**
* Ability: Sense Pulse (detect Ana's direction)
*/
useSensePulse() {
if (!this.abilities.sensePulse.unlocked) {
console.log('❌ Sense Pulse not unlocked');
return null;
}
if (this.abilities.sensePulse.cooldown > 0) {
console.log('⏸️ Sense Pulse on cooldown');
return null;
}
console.log('📍 Sensing Ana\'s location...');
// Calculate general direction
const playerX = this.scene.player?.x || 0;
const playerY = this.scene.player?.y || 0;
const angle = Phaser.Math.Angle.Between(
playerX, playerY,
this.anaState.direction.x, this.anaState.direction.y
);
const distance = this.anaState.distance;
// Show visual indicator
this.showDirectionIndicator(angle, distance);
// Set cooldown
this.abilities.sensePulse.cooldown = this.abilities.sensePulse.maxCooldown;
this.changeBondStrength(+5);
return {
angle: angle,
distance: distance,
distanceCategory: this.getDistanceCategory(distance)
};
}
/**
* Get distance category (for vague communication)
*/
getDistanceCategory(distance) {
if (distance < 500) return 'very_close';
if (distance < 1500) return 'close';
if (distance < 3000) return 'far';
return 'very_far';
}
/**
* Show direction indicator
*/
showDirectionIndicator(angle, distance) {
const category = this.getDistanceCategory(distance);
const messages = {
'very_close': 'Ana is VERY CLOSE! ⬆️',
'close': 'Ana is nearby 📍',
'far': 'Ana is far away 🔭',
'very_far': 'Ana is very far 🌌'
};
console.log(`📍 ${messages[category]} (${Math.round(distance)}px)`);
// TODO: Show UI arrow pointing in direction
}
/**
* Update Ana's danger level
*/
updateAnaDanger(deltaSeconds) {
// Danger level increases over time (captors getting desperate)
if (this.anaState.alive) {
this.anaState.dangerLevel = Math.min(
100,
this.anaState.dangerLevel + 0.1 * deltaSeconds
);
// Trigger danger events
if (this.anaState.dangerLevel > 70 && Math.random() < 0.01) {
const dangerEvent = this.bondEvents.find(e => e.id === 'danger_warning');
if (dangerEvent) {
this.showTelepathicMessage(dangerEvent.message, dangerEvent.emotion);
}
}
}
}
/**
* Update Ana's position (for story progression)
*/
updateAnaLocation(x, y, distance) {
this.anaState.direction.x = x;
this.anaState.direction.y = y;
this.anaState.distance = distance;
console.log(`📍 Ana's location updated: (${x}, ${y}), distance: ${distance}px`);
}
/**
* Create Twin Bond UI
*/
createBondUI() {
const width = this.scene.cameras.main.width;
// Bond meter (top-left)
const x = 20;
const y = 120;
// Background
const bg = this.scene.add.rectangle(x, y, 200, 40, 0x2d1b00, 0.8);
bg.setOrigin(0, 0);
bg.setScrollFactor(0);
bg.setDepth(100);
// Title
const title = this.scene.add.text(x + 10, y + 5, '💞 Twin Bond', {
fontSize: '14px',
fontFamily: 'Georgia, serif',
color: '#FFD700',
fontStyle: 'bold'
});
title.setScrollFactor(0);
title.setDepth(100);
// Bond bar
const barBg = this.scene.add.rectangle(x + 10, y + 25, 180, 8, 0x000000, 0.8);
barBg.setOrigin(0, 0);
barBg.setScrollFactor(0);
barBg.setDepth(100);
const barFill = this.scene.add.rectangle(
x + 10, y + 25,
180 * (this.bondStrength / 100),
8,
0xFF69B4,
1
);
barFill.setOrigin(0, 0);
barFill.setScrollFactor(0);
barFill.setDepth(100);
this.bondUI = { bg, title, barBg, barFill };
// Update bar every frame
this.scene.events.on('update', () => {
if (this.bondUI && this.bondUI.barFill) {
this.bondUI.barFill.width = 180 * (this.bondStrength / 100);
}
});
}
/**
* Getters
*/
getBondStrength() {
return this.bondStrength;
}
getAnaStatus() {
return this.anaState;
}
isAnaSafe() {
return this.anaState.dangerLevel < 50;
}
}

View File

@@ -0,0 +1,334 @@
/**
* TwinBondUISystem.js
* ====================
* KRVAVA ŽETEV - Twin Bond Heartbeat UI
*
* Features:
* - Visual heartbeat indicator on screen
* - Speeds up when near Ana's clues
* - Purple glow effect
* - Distance-based intensity
* - Twin bond strength indicator
*
* @author NovaFarma Team
* @date 2025-12-25
*/
export default class TwinBondUISystem {
constructor(scene) {
this.scene = scene;
// UI elements
this.heartContainer = null;
this.heartIcon = null;
this.glowEffect = null;
this.bondStrengthBar = null;
// State
this.baseHeartRate = 60; // bpm
this.currentHeartRate = 60;
this.bondStrength = 0; // 0-100
this.nearestClueDistance = Infinity;
// Animation
this.heartbeatTween = null;
this.lastBeat = 0;
console.log('💜 TwinBondUISystem initialized');
this.createUI();
this.startHeartbeat();
}
/**
* Create UI elements
*/
createUI() {
const ui = this.scene.scene.get('UIScene') || this.scene;
// Container (top-left corner)
this.heartContainer = ui.add.container(50, 50);
this.heartContainer.setDepth(1000); // Above everything
this.heartContainer.setScrollFactor(0); // Fixed to camera
// Purple glow effect (behind heart)
this.glowEffect = ui.add.circle(0, 0, 25, 0x9370DB, 0);
this.heartContainer.add(this.glowEffect);
// Heart icon (emoji-style)
this.heartIcon = ui.add.text(0, 0, '💜', {
fontSize: '32px',
fontFamily: 'Arial'
});
this.heartIcon.setOrigin(0.5);
this.heartContainer.add(this.heartIcon);
// Bond strength bar (below heart)
const barWidth = 50;
const barHeight = 5;
// Background bar
const barBg = ui.add.rectangle(0, 30, barWidth, barHeight, 0x333333);
this.heartContainer.add(barBg);
// Strength bar
this.bondStrengthBar = ui.add.rectangle(
-barWidth / 2,
30,
0, // Start at 0 width
barHeight,
0x9370DB
);
this.bondStrengthBar.setOrigin(0, 0.5);
this.heartContainer.add(this.bondStrengthBar);
// Bond strength text
this.bondStrengthText = ui.add.text(0, 45, '0%', {
fontSize: '10px',
fontFamily: 'Arial',
color: '#9370DB'
});
this.bondStrengthText.setOrigin(0.5);
this.heartContainer.add(this.bondStrengthText);
console.log('✅ Twin Bond UI created');
}
/**
* Start heartbeat animation
*/
startHeartbeat() {
const beatInterval = () => {
const bpm = this.currentHeartRate;
const msPerBeat = 60000 / bpm; // Convert BPM to ms
// Heart pump animation
if (this.heartIcon) {
this.scene.tweens.add({
targets: [this.heartIcon],
scale: { from: 1, to: 1.3 },
duration: 100,
yoyo: true,
ease: 'Quad.Out'
});
// Glow pulse
this.scene.tweens.add({
targets: [this.glowEffect],
alpha: { from: 0, to: 0.6 },
scale: { from: 1, to: 1.5 },
duration: 150,
yoyo: true,
ease: 'Quad.Out'
});
}
// Schedule next beat
this.heartbeatTimer = setTimeout(() => beatInterval(), msPerBeat);
};
// Start beating
beatInterval();
}
/**
* Update system (called every frame)
*/
update(time, delta) {
if (!this.scene.player) return;
// Calculate distance to nearest Ana clue
this.calculateNearestClue();
// Update heart rate based on distance
this.updateHeartRate();
// Update bond strength display
this.updateBondStrengthDisplay();
}
/**
* Calculate distance to nearest Ana's clue
*/
calculateNearestClue() {
if (!this.scene.anaClueSystem) {
this.nearestClueDistance = Infinity;
return;
}
const playerX = this.scene.player.sprite.x;
const playerY = this.scene.player.sprite.y;
let minDistance = Infinity;
// Check all clues
this.scene.anaClueSystem.clueLocations.forEach((position, clueId) => {
// Skip already discovered clues
if (this.scene.anaClueSystem.discovered.has(clueId)) return;
const dx = position.x - playerX;
const dy = position.y - playerY;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < minDistance) {
minDistance = distance;
}
});
this.nearestClueDistance = minDistance;
}
/**
* Update heart rate based on proximity to clues
*/
updateHeartRate() {
const distance = this.nearestClueDistance;
// Distance thresholds (in pixels)
const veryClose = 200; // 4-5 blocks
const close = 480; // 10 blocks
const medium = 960; // 20 blocks
let targetHeartRate = this.baseHeartRate;
if (distance < veryClose) {
// VERY CLOSE - Rapid heartbeat!
targetHeartRate = 120; // 2x normal
this.bondStrength = 100;
} else if (distance < close) {
// CLOSE - Fast heartbeat
targetHeartRate = 90;
this.bondStrength = 75;
} else if (distance < medium) {
// MEDIUM - Slightly elevated
targetHeartRate = 75;
this.bondStrength = 50;
} else {
// FAR - Normal heartbeat
targetHeartRate = 60;
this.bondStrength = Math.max(0, this.bondStrength - 1); // Slowly decrease
}
// Smooth transition
this.currentHeartRate += (targetHeartRate - this.currentHeartRate) * 0.1;
// Clamp
this.currentHeartRate = Phaser.Math.Clamp(this.currentHeartRate, 30, 150);
}
/**
* Update bond strength visual display
*/
updateBondStrengthDisplay() {
if (!this.bondStrengthBar) return;
// Update bar width
const maxWidth = 50;
const targetWidth = (this.bondStrength / 100) * maxWidth;
this.bondStrengthBar.width = targetWidth;
// Update text
if (this.bondStrengthText) {
this.bondStrengthText.setText(`${Math.floor(this.bondStrength)}%`);
}
// Color gradient based on strength
if (this.bondStrength > 75) {
this.bondStrengthBar.setFillStyle(0xFF69B4); // Hot pink - very strong
} else if (this.bondStrength > 50) {
this.bondStrengthBar.setFillStyle(0x9370DB); // Medium purple
} else if (this.bondStrength > 25) {
this.bondStrengthBar.setFillStyle(0x6A5ACD); // Slate blue
} else {
this.bondStrengthBar.setFillStyle(0x483D8B); // Dark slate blue
}
// Pulsing glow when strong bond
if (this.bondStrength > 75 && this.glowEffect) {
this.glowEffect.setAlpha(0.3 + Math.sin(Date.now() / 200) * 0.2);
}
}
/**
* Trigger special bond moment (e.g., flashback)
*/
triggerBondMoment(intensity = 'medium') {
console.log(`💜 Twin Bond Moment! Intensity: ${intensity}`);
// Screen flash
this.scene.cameras.main.flash(500, 147, 112, 219, false); // Purple flash
// Heart explosion effect
if (this.heartIcon) {
this.scene.tweens.add({
targets: [this.heartIcon],
scale: { from: 1, to: 2 },
alpha: { from: 1, to: 0 },
duration: 800,
ease: 'Quad.Out',
onComplete: () => {
this.heartIcon.setScale(1);
this.heartIcon.setAlpha(1);
}
});
}
// Glow burst
if (this.glowEffect) {
this.scene.tweens.add({
targets: [this.glowEffect],
scale: { from: 1, to: 5 },
alpha: { from: 0.8, to: 0 },
duration: 1000,
ease: 'Quad.Out',
onComplete: () => {
this.glowEffect.setScale(1);
this.glowEffect.setAlpha(0);
}
});
}
// Temporary heart rate spike
this.currentHeartRate = 150;
setTimeout(() => {
this.currentHeartRate = this.baseHeartRate;
}, 3000);
// Bond strength boost
this.bondStrength = 100;
// Show notification
this.scene.events.emit('show-notification', {
title: 'Twin Bond Activated!',
text: '💜 You feel Ana\'s presence intensify!',
icon: '💜',
color: '#9370DB'
});
}
/**
* Get current heart rate (for debugging)
*/
getCurrentHeartRate() {
return Math.floor(this.currentHeartRate);
}
/**
* Get bond strength (for other systems)
*/
getBondStrength() {
return this.bondStrength;
}
/**
* Cleanup
*/
destroy() {
if (this.heartbeatTimer) {
clearTimeout(this.heartbeatTimer);
}
if (this.heartContainer) {
this.heartContainer.destroy();
}
}
}

View File

@@ -0,0 +1,128 @@
/**
* VISUAL EFFECTS SYSTEM
* Handles juice effects: screenshake, particles, lighting
*/
class VisualEffectsSystem {
constructor(scene) {
this.scene = scene;
this.camera = scene.cameras.main;
this.isShaking = false;
}
/**
* Screenshake effect
* @param {number} intensity - Shake strength (default 0.005)
* @param {number} duration - Duration in ms (default 300)
*/
screenshake(intensity = 0.005, duration = 300) {
if (this.isShaking) return;
this.isShaking = true;
this.camera.shake(duration, intensity);
this.scene.time.delayedCall(duration, () => {
this.isShaking = false;
});
}
/**
* Hit particles (sparks)
*/
createHitParticles(x, y, color = 0xFFFFFF) {
const particles = this.scene.add.particles(x, y, 'particle_white', {
speed: { min: 100, max: 200 },
angle: { min: 0, max: 360 },
scale: { start: 0.5, end: 0 },
lifespan: 300,
quantity: 8,
tint: color
});
this.scene.time.delayedCall(500, () => {
particles.destroy();
});
}
/**
* Explosion particles
*/
createExplosion(x, y, color = 0xFF4444) {
const particles = this.scene.add.particles(x, y, 'particle_white', {
speed: { min: 200, max: 400 },
angle: { min: 0, max: 360 },
scale: { start: 1, end: 0 },
lifespan: 600,
quantity: 20,
tint: color,
gravityY: 300
});
this.scene.time.delayedCall(800, () => {
particles.destroy();
});
}
/**
* Dust particles (for movement)
*/
createDustPuff(x, y) {
const particles = this.scene.add.particles(x, y, 'particle_white', {
speed: { min: 30, max: 60 },
angle: { min: 0, max: 360 },
scale: { start: 0.3, end: 0 },
lifespan: 400,
quantity: 5,
tint: 0xCCBBAA,
alpha: { start: 0.5, end: 0 }
});
this.scene.time.delayedCall(500, () => {
particles.destroy();
});
}
/**
* Flash effect (for damage, powerups)
*/
flash(color = 0xFFFFFF, duration = 100) {
this.camera.flash(duration, ...this.hexToRGB(color));
}
/**
* Fade effect
*/
fadeOut(duration = 1000, callback) {
this.camera.fadeOut(duration, 0, 0, 0);
if (callback) {
this.scene.time.delayedCall(duration, callback);
}
}
fadeIn(duration = 1000) {
this.camera.fadeIn(duration, 0, 0, 0);
}
/**
* Utility: Hex to RGB
*/
hexToRGB(hex) {
return [
(hex >> 16) & 255,
(hex >> 8) & 255,
hex & 255
];
}
/**
* Create simple white pixel texture for particles
*/
static createParticleTexture(scene) {
if (scene.textures.exists('particle_white')) return;
const graphics = scene.make.graphics({ x: 0, y: 0, add: false });
graphics.fillStyle(0xFFFFFF, 1);
graphics.fillCircle(4, 4, 4);
graphics.generateTexture('particle_white', 8, 8);
graphics.destroy();
}
}

View File

@@ -0,0 +1,624 @@
/**
* VISUAL ENHANCEMENT SYSTEM
* Central system for managing visual effects, animations, and polish
*/
class VisualEnhancementSystem {
constructor(scene) {
this.scene = scene;
this.enabled = true;
// Sub-systems
this.animatedTextures = new Map();
this.weatherEffects = [];
this.lightSources = [];
this.shadows = [];
this.particles = [];
// Settings
this.settings = {
animatedTextures: true,
weatherEffects: true,
dynamicLighting: true,
shadows: true,
fogOfWar: false,
particleQuality: 'high', // low, medium, high, ultra
animationQuality: 'high',
screenShake: true,
transitions: true
};
// Animation timers
this.waterAnimTime = 0;
this.treeAnimTime = 0;
this.fireAnimTime = 0;
this.loadSettings();
this.init();
console.log('✅ Visual Enhancement System initialized');
}
init() {
this.initAnimatedTextures();
this.initWeatherEffects();
this.initLightingSystem();
this.initShadowSystem();
this.initParticleSystem();
}
// ========== ANIMATED TEXTURES ==========
initAnimatedTextures() {
if (!this.settings.animatedTextures) return;
// Create water animation frames
this.createWaterAnimation();
// Create fire animation
this.createFireAnimation();
// Create tree leaf animation
this.createTreeAnimation();
console.log('🎬 Animated textures initialized');
}
createWaterAnimation() {
// Water flow animation (4 frames)
const frames = [];
for (let i = 0; i < 4; i++) {
const graphics = this.scene.add.graphics();
graphics.fillStyle(0x4488ff, 1);
graphics.fillRect(0, 0, 64, 64);
// Add wave pattern
graphics.lineStyle(2, 0x6699ff, 0.5);
for (let y = 0; y < 64; y += 8) {
const offset = Math.sin((y + i * 16) * 0.1) * 4;
graphics.lineBetween(0, y + offset, 64, y + offset);
}
graphics.generateTexture('water_anim_' + i, 64, 64);
graphics.destroy();
frames.push('water_anim_' + i);
}
this.animatedTextures.set('water', {
frames,
currentFrame: 0,
speed: 200 // ms per frame
});
}
createFireAnimation() {
// Fire flickering (3 frames)
const frames = [];
const colors = [0xff6600, 0xff8800, 0xffaa00];
for (let i = 0; i < 3; i++) {
const graphics = this.scene.add.graphics();
graphics.fillStyle(colors[i], 1);
graphics.fillCircle(16, 16, 12 + i * 2);
graphics.generateTexture('fire_anim_' + i, 32, 32);
graphics.destroy();
frames.push('fire_anim_' + i);
}
this.animatedTextures.set('fire', {
frames,
currentFrame: 0,
speed: 150
});
}
createTreeAnimation() {
// Tree leaf rustling (subtle movement)
console.log('🌳 Tree animation ready');
}
updateAnimatedTextures(delta) {
if (!this.settings.animatedTextures) return;
this.waterAnimTime += delta;
this.fireAnimTime += delta;
// Update water
const water = this.animatedTextures.get('water');
if (water && this.waterAnimTime > water.speed) {
water.currentFrame = (water.currentFrame + 1) % water.frames.length;
this.waterAnimTime = 0;
}
// Update fire
const fire = this.animatedTextures.get('fire');
if (fire && this.fireAnimTime > fire.speed) {
fire.currentFrame = (fire.currentFrame + 1) % fire.frames.length;
this.fireAnimTime = 0;
}
}
// ========== WEATHER EFFECTS ==========
initWeatherEffects() {
if (!this.settings.weatherEffects) return;
console.log('🌦️ Weather effects initialized');
}
createSnowEffect() {
const emitter = this.scene.add.particles(0, 0, 'particle_white', {
x: { min: 0, max: this.scene.cameras.main.width },
y: -10,
speedY: { min: 50, max: 100 },
speedX: { min: -20, max: 20 },
scale: { min: 0.3, max: 0.8 },
alpha: { min: 0.5, max: 1 },
lifespan: 10000,
frequency: 50,
quantity: 2
});
emitter.setScrollFactor(0);
this.weatherEffects.push(emitter);
return emitter;
}
createRainEffect() {
const emitter = this.scene.add.particles(0, 0, 'particle_white', {
x: { min: 0, max: this.scene.cameras.main.width },
y: -10,
speedY: { min: 400, max: 600 },
speedX: { min: -50, max: -30 },
scaleX: 0.1,
scaleY: 0.5,
alpha: 0.6,
lifespan: 2000,
frequency: 10,
quantity: 5,
tint: 0x88ccff
});
emitter.setScrollFactor(0);
this.weatherEffects.push(emitter);
return emitter;
}
createLightningFlash() {
const flash = this.scene.add.rectangle(
this.scene.cameras.main.centerX,
this.scene.cameras.main.centerY,
this.scene.cameras.main.width,
this.scene.cameras.main.height,
0xffffff,
0.8
);
flash.setScrollFactor(0);
flash.setDepth(10000);
this.scene.tweens.add({
targets: flash,
alpha: 0,
duration: 200,
onComplete: () => flash.destroy()
});
}
createFogEffect() {
// Atmospheric fog overlay - covers entire screen
const fogOverlay = this.scene.add.graphics();
fogOverlay.setScrollFactor(0);
fogOverlay.setDepth(8000); // Above game, below UI
fogOverlay.setAlpha(0.4); // Semi-transparent
// Create fog particles for movement effect
const fogParticles = this.scene.add.particles(0, 0, 'particle_white', {
x: { min: 0, max: this.scene.cameras.main.width },
y: { min: 0, max: this.scene.cameras.main.height },
speedX: { min: -10, max: 10 },
speedY: { min: -5, max: 5 },
scale: { min: 2, max: 4 },
alpha: { min: 0.1, max: 0.3 },
lifespan: 10000,
frequency: 100,
quantity: 3,
tint: 0xcccccc,
blendMode: 'SCREEN'
});
fogParticles.setScrollFactor(0);
fogParticles.setDepth(8001);
// Animated fog overlay
let fogTime = 0;
const updateFog = () => {
fogTime += 0.01;
fogOverlay.clear();
const cam = this.scene.cameras.main;
// Draw multiple layers of fog for depth
for (let layer = 0; layer < 3; layer++) {
const offset = Math.sin(fogTime + layer) * 50;
const alpha = 0.1 + layer * 0.05;
fogOverlay.fillStyle(0xffffff, alpha);
// Draw wavy fog shapes
for (let i = 0; i < 5; i++) {
const x = (i * cam.width / 4) + offset;
const y = (layer * 100) + Math.cos(fogTime + i) * 30;
fogOverlay.fillCircle(x, y, 200 + layer * 50);
}
}
};
// Update fog animation every frame
this.scene.events.on('update', updateFog);
this.weatherEffects.push({
overlay: fogOverlay,
particles: fogParticles,
update: updateFog,
destroy: () => {
fogOverlay.destroy();
fogParticles.destroy();
this.scene.events.off('update', updateFog);
}
});
console.log('🌫️ Atmospheric fog effect created');
return { overlay: fogOverlay, particles: fogParticles };
}
// ========== LIGHTING SYSTEM ==========
initLightingSystem() {
if (!this.settings.dynamicLighting) return;
this.lightingLayer = this.scene.add.layer();
this.lightingLayer.setDepth(5000);
console.log('💡 Lighting system initialized');
}
addLight(x, y, radius = 100, color = 0xffaa00, intensity = 0.6) {
if (!this.settings.dynamicLighting) return null;
// Create radial gradient light
const light = this.scene.add.graphics();
const gradient = light.createRadialGradient(
radius, radius, 0,
radius, radius, radius
);
gradient.addColorStop(0, `rgba(${(color >> 16) & 0xff}, ${(color >> 8) & 0xff}, ${color & 0xff}, ${intensity})`);
gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
light.fillStyle(color, intensity);
light.fillCircle(radius, radius, radius);
light.setPosition(x - radius, y - radius);
light.setBlendMode(Phaser.BlendModes.ADD);
light.setDepth(5001);
const lightObj = {
graphics: light,
x, y, radius, color, intensity,
flickering: false
};
this.lightSources.push(lightObj);
return lightObj;
}
addTorch(x, y) {
const torch = this.addLight(x, y, 80, 0xff6600, 0.5);
if (torch) {
torch.flickering = true;
}
return torch;
}
updateLighting(delta) {
if (!this.settings.dynamicLighting) return;
// Update flickering lights
for (const light of this.lightSources) {
if (light.flickering) {
const flicker = 0.4 + Math.random() * 0.2;
light.graphics.setAlpha(flicker);
}
}
// Update ambient lighting based on time of day
if (this.scene.weatherSystem) {
const time = this.scene.weatherSystem.gameTime;
const isNight = time < 6 || time > 18;
if (isNight) {
// Darker at night
this.scene.cameras.main.setAlpha(0.7);
} else {
this.scene.cameras.main.setAlpha(1.0);
}
}
}
// ========== SHADOW SYSTEM ==========
initShadowSystem() {
if (!this.settings.shadows) return;
console.log('🌑 Shadow system initialized');
}
addShadow(entity, offsetX = 0, offsetY = 10, width = 40, height = 15) {
if (!this.settings.shadows) return null;
const shadow = this.scene.add.ellipse(
entity.x + offsetX,
entity.y + offsetY,
width,
height,
0x000000,
0.3
);
shadow.setDepth(0);
const shadowObj = { entity, shadow, offsetX, offsetY };
this.shadows.push(shadowObj);
return shadow;
}
updateShadows() {
if (!this.settings.shadows) return;
// Get time of day for shadow opacity
let opacity = 0.3;
if (this.scene.weatherSystem) {
const time = this.scene.weatherSystem.gameTime;
// Darker shadows at noon, lighter at dawn/dusk
opacity = 0.2 + Math.abs(Math.sin((time / 24) * Math.PI * 2)) * 0.3;
}
// Update shadow positions
for (const { entity, shadow, offsetX, offsetY } of this.shadows) {
if (entity.sprite) {
shadow.x = entity.sprite.x + offsetX;
shadow.y = entity.sprite.y + offsetY;
shadow.setAlpha(opacity);
}
}
}
// ========== PARTICLE SYSTEM ==========
initParticleSystem() {
// Create particle textures
this.createParticleTextures();
console.log('✨ Particle system initialized');
}
createParticleTextures() {
// White particle
const white = this.scene.add.graphics();
white.fillStyle(0xffffff, 1);
white.fillCircle(4, 4, 4);
white.generateTexture('particle_white', 8, 8);
white.destroy();
// Sparkle
const sparkle = this.scene.add.graphics();
sparkle.fillStyle(0xffff00, 1);
sparkle.fillCircle(3, 3, 3);
sparkle.generateTexture('particle_sparkle', 6, 6);
sparkle.destroy();
// Heart
const heart = this.scene.add.graphics();
heart.fillStyle(0xff0066, 1);
heart.fillCircle(3, 3, 3);
heart.fillCircle(5, 3, 3);
heart.fillTriangle(1, 4, 7, 4, 4, 8);
heart.generateTexture('particle_heart', 8, 8);
heart.destroy();
}
createHeartParticles(x, y) {
const emitter = this.scene.add.particles(x, y, 'particle_heart', {
speed: { min: 20, max: 50 },
angle: { min: -120, max: -60 },
scale: { start: 1, end: 0 },
alpha: { start: 1, end: 0 },
lifespan: 1000,
quantity: 5,
blendMode: 'ADD'
});
this.scene.time.delayedCall(1000, () => emitter.destroy());
return emitter;
}
createSparkleEffect(x, y) {
const emitter = this.scene.add.particles(x, y, 'particle_sparkle', {
speed: { min: 10, max: 30 },
scale: { start: 1, end: 0 },
alpha: { start: 1, end: 0 },
lifespan: 800,
quantity: 10,
blendMode: 'ADD'
});
this.scene.time.delayedCall(800, () => emitter.destroy());
return emitter;
}
createCheckmarkEffect(x, y) {
const checkmark = this.scene.add.text(x, y, '✓', {
fontSize: '32px',
color: '#00ff00',
fontStyle: 'bold'
});
checkmark.setOrigin(0.5);
checkmark.setDepth(10000);
this.scene.tweens.add({
targets: checkmark,
y: y - 50,
alpha: 0,
scale: 2,
duration: 1000,
ease: 'Power2',
onComplete: () => checkmark.destroy()
});
}
// ========== SCREEN EFFECTS ==========
screenShake(intensity = 10, duration = 300) {
if (!this.settings.screenShake) return;
this.scene.cameras.main.shake(duration, intensity / 1000);
}
screenFlash(color = 0xffffff, duration = 200) {
this.scene.cameras.main.flash(duration,
(color >> 16) & 0xff,
(color >> 8) & 0xff,
color & 0xff
);
}
fadeOut(duration = 500, callback) {
if (!this.settings.transitions) {
if (callback) callback();
return;
}
this.scene.cameras.main.fadeOut(duration, 0, 0, 0);
if (callback) {
this.scene.cameras.main.once('camerafadeoutcomplete', callback);
}
}
fadeIn(duration = 500) {
if (!this.settings.transitions) return;
this.scene.cameras.main.fadeIn(duration, 0, 0, 0);
}
// ========== BUILDING EFFECTS ==========
createConstructionEffect(x, y) {
// Dust particles during construction
const emitter = this.scene.add.particles(x, y, 'particle_white', {
speed: { min: 20, max: 40 },
scale: { start: 0.5, end: 0 },
alpha: { start: 0.5, end: 0 },
lifespan: 1000,
quantity: 3,
frequency: 100,
tint: 0x996633
});
return emitter;
}
createSmokeEffect(x, y) {
// Chimney smoke
const emitter = this.scene.add.particles(x, y, 'particle_white', {
speedY: { min: -30, max: -50 },
speedX: { min: -10, max: 10 },
scale: { start: 0.3, end: 1 },
alpha: { start: 0.5, end: 0 },
lifespan: 2000,
frequency: 500,
quantity: 1,
tint: 0x888888
});
return emitter;
}
// ========== FARM AUTOMATION VISUALS ==========
createPowerGridEffect(x1, y1, x2, y2) {
// Electric arc between power sources
const graphics = this.scene.add.graphics();
graphics.lineStyle(2, 0x00ffff, 0.8);
// Draw lightning-like connection
const steps = 5;
const points = [];
for (let i = 0; i <= steps; i++) {
const t = i / steps;
const x = x1 + (x2 - x1) * t + (Math.random() - 0.5) * 10;
const y = y1 + (y2 - y1) * t + (Math.random() - 0.5) * 10;
points.push({ x, y });
}
graphics.beginPath();
graphics.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length; i++) {
graphics.lineTo(points[i].x, points[i].y);
}
graphics.strokePath();
// Fade out
this.scene.tweens.add({
targets: graphics,
alpha: 0,
duration: 200,
onComplete: () => graphics.destroy()
});
}
createMutantGlow(entity, color = 0x00ff00) {
// Radioactive glow for mutants
const glow = this.scene.add.circle(
entity.x,
entity.y,
30,
color,
0.3
);
glow.setBlendMode(Phaser.BlendModes.ADD);
glow.setDepth(entity.depth - 1);
// Pulsing animation
this.scene.tweens.add({
targets: glow,
scale: { from: 1, to: 1.2 },
alpha: { from: 0.3, to: 0.1 },
duration: 1000,
yoyo: true,
repeat: -1
});
return glow;
}
// ========== UPDATE ==========
update(delta) {
this.updateAnimatedTextures(delta);
this.updateLighting(delta);
this.updateShadows();
}
// ========== SETTINGS ==========
saveSettings() {
localStorage.setItem('novafarma_visual_enhancements', JSON.stringify(this.settings));
}
loadSettings() {
const saved = localStorage.getItem('novafarma_visual_enhancements');
if (saved) {
this.settings = { ...this.settings, ...JSON.parse(saved) };
}
}
destroy() {
if (this.lightingLayer) this.lightingLayer.destroy();
for (const { shadow } of this.shadows) {
shadow.destroy();
}
for (const effect of this.weatherEffects) {
effect.destroy();
}
console.log('✨ Visual Enhancement System destroyed');
}
}

View File

@@ -0,0 +1,746 @@
/**
* VISUAL SOUND CUE SYSTEM
* Provides visual indicators for sounds (accessibility for deaf/hard-of-hearing players)
*/
class VisualSoundCueSystem {
constructor(scene) {
this.scene = scene;
this.enabled = true;
// Visual elements
this.heartbeatIndicator = null;
this.damageIndicators = [];
this.screenFlash = null;
this.subtitleBackground = null;
this.subtitleText = null;
this.subtitleSpeaker = null;
this.subtitleArrows = { left: null, right: null };
this.heartbeatTween = null;
this.fishingBobberIndicator = null;
// Speaker color mapping
this.speakerColors = {
'Player': '#00ff00',
'NPC': '#ffff00',
'Enemy': '#ff0000',
'System': '#00ffff',
'Narrator': '#ffffff'
};
// Subtitle size presets
this.subtitleSizes = {
'small': { main: 16, speaker: 12, arrow: 24 },
'medium': { main: 20, speaker: 16, arrow: 32 },
'large': { main: 28, speaker: 20, arrow: 40 },
'very-large': { main: 36, speaker: 24, arrow: 48 }
};
// Settings
this.settings = {
heartbeatEnabled: true,
damageIndicatorEnabled: true,
screenFlashEnabled: true,
subtitlesEnabled: true,
directionalArrowsEnabled: true,
speakerNamesEnabled: true,
subtitleOpacity: 0.8,
fishingBobberEnabled: true,
subtitleSize: 'medium' // 'small', 'medium', 'large', 'very-large'
};
this.loadSettings();
this.init();
console.log('✅ Visual Sound Cue System initialized');
}
init() {
// Create heartbeat indicator (top-left corner)
this.createHeartbeatIndicator();
// Create subtitle container (bottom center)
this.createSubtitleContainer();
// Load settings from localStorage
// this.loadSettings(); // Moved to constructor
}
createHeartbeatIndicator() {
const x = 200;
const y = 30;
// Heart emoji as sprite
this.heartbeatIndicator = this.scene.add.text(x, y, '❤️', {
fontSize: '48px'
});
this.heartbeatIndicator.setOrigin(0.5);
this.heartbeatIndicator.setDepth(10000);
this.heartbeatIndicator.setScrollFactor(0);
this.heartbeatIndicator.setVisible(false);
}
createSubtitleContainer() {
const width = this.scene.cameras.main.width;
const height = this.scene.cameras.main.height;
// Background
this.subtitleBackground = this.scene.add.rectangle(
width / 2,
height - 100,
width - 100,
100,
0x000000,
this.settings.subtitleOpacity
);
this.subtitleBackground.setOrigin(0.5);
this.subtitleBackground.setDepth(9999);
this.subtitleBackground.setScrollFactor(0);
this.subtitleBackground.setVisible(false);
// Speaker name text
this.subtitleSpeaker = this.scene.add.text(
width / 2,
height - 130,
'',
{
fontSize: '16px',
fontFamily: 'Arial',
fontStyle: 'bold',
color: '#ffffff',
align: 'center'
}
);
this.subtitleSpeaker.setOrigin(0.5);
this.subtitleSpeaker.setDepth(10001);
this.subtitleSpeaker.setScrollFactor(0);
this.subtitleSpeaker.setVisible(false);
// Main subtitle text
this.subtitleText = this.scene.add.text(
width / 2,
height - 100,
'',
{
fontSize: '20px',
fontFamily: 'Arial',
color: '#ffffff',
align: 'center',
wordWrap: { width: width - 160 }
}
);
this.subtitleText.setOrigin(0.5);
this.subtitleText.setDepth(10000);
this.subtitleText.setScrollFactor(0);
this.subtitleText.setVisible(false);
// Directional arrows
this.subtitleArrows.left = this.scene.add.text(
50,
height - 100,
'◄',
{
fontSize: '32px',
fontFamily: 'Arial',
color: '#ffff00'
}
);
this.subtitleArrows.left.setOrigin(0.5);
this.subtitleArrows.left.setDepth(10001);
this.subtitleArrows.left.setScrollFactor(0);
this.subtitleArrows.left.setVisible(false);
this.subtitleArrows.right = this.scene.add.text(
width - 50,
height - 100,
'►',
{
fontSize: '32px',
fontFamily: 'Arial',
color: '#ffff00'
}
);
this.subtitleArrows.right.setOrigin(0.5);
this.subtitleArrows.right.setDepth(10001);
this.subtitleArrows.right.setScrollFactor(0);
this.subtitleArrows.right.setVisible(false);
}
// ========== VISUAL HEARTBEAT (LOW HEALTH) ==========
updateHeartbeat(healthPercent) {
if (!this.settings.heartbeatEnabled) return;
// Show heartbeat when health < 30%
if (healthPercent < 30) {
if (!this.heartbeatActive) {
this.startHeartbeat(healthPercent);
} else {
this.updateHeartbeatSpeed(healthPercent);
}
} else {
this.stopHeartbeat();
}
}
startHeartbeat(healthPercent) {
this.heartbeatActive = true;
this.heartbeatIndicator.setVisible(true);
// Calculate speed based on health (lower health = faster beat)
const speed = Phaser.Math.Clamp(1000 - (healthPercent * 20), 300, 1000);
this.heartbeatTween = this.scene.tweens.add({
targets: this.heartbeatIndicator,
scale: 1.3,
alpha: 0.6,
duration: speed / 2,
yoyo: true,
repeat: -1,
ease: 'Sine.easeInOut'
});
console.log('💓 Visual heartbeat started (health:', healthPercent + '%)');
}
updateHeartbeatSpeed(healthPercent) {
if (!this.heartbeatTween) return;
const speed = Phaser.Math.Clamp(1000 - (healthPercent * 20), 300, 1000);
this.heartbeatTween.updateTo('duration', speed / 2, true);
}
stopHeartbeat() {
if (!this.heartbeatActive) return;
this.heartbeatActive = false;
this.heartbeatIndicator.setVisible(false);
if (this.heartbeatTween) {
this.heartbeatTween.stop();
this.heartbeatTween = null;
}
this.heartbeatIndicator.setScale(1);
this.heartbeatIndicator.setAlpha(1);
}
// ========== DAMAGE DIRECTION INDICATOR ==========
showDamageIndicator(direction, damage) {
if (!this.settings.damageIndicatorEnabled) return;
const width = this.scene.cameras.main.width;
const height = this.scene.cameras.main.height;
// Calculate position based on direction
let x = width / 2;
let y = height / 2;
let arrow = '↓';
let rotation = 0;
switch (direction) {
case 'up':
y = 100;
arrow = '↑';
rotation = 0;
break;
case 'down':
y = height - 100;
arrow = '↓';
rotation = Math.PI;
break;
case 'left':
x = 100;
arrow = '←';
rotation = -Math.PI / 2;
break;
case 'right':
x = width - 100;
arrow = '→';
rotation = Math.PI / 2;
break;
}
// Create damage indicator
const indicator = this.scene.add.text(x, y, arrow, {
fontSize: '64px',
color: '#ff0000',
fontStyle: 'bold'
});
indicator.setOrigin(0.5);
indicator.setDepth(10001);
indicator.setScrollFactor(0);
indicator.setRotation(rotation);
// Animate and destroy
this.scene.tweens.add({
targets: indicator,
alpha: 0,
scale: 1.5,
duration: 800,
ease: 'Power2',
onComplete: () => {
indicator.destroy();
}
});
console.log('🎯 Damage indicator shown:', direction, damage);
}
// ========== SCREEN FLASH NOTIFICATIONS ==========
showScreenFlash(type, message) {
if (!this.settings.screenFlashEnabled) return;
const width = this.scene.cameras.main.width;
const height = this.scene.cameras.main.height;
let color = 0xffffff;
let icon = '⚠️';
switch (type) {
case 'danger':
color = 0xff0000;
icon = '⚠️';
break;
case 'warning':
color = 0xffaa00;
icon = '⚡';
break;
case 'info':
color = 0x00aaff;
icon = '';
break;
case 'success':
color = 0x00ff00;
icon = '✓';
break;
}
// Flash overlay
const flash = this.scene.add.rectangle(0, 0, width, height, color, 0.3);
flash.setOrigin(0);
flash.setDepth(9998);
flash.setScrollFactor(0);
// Icon
const iconText = this.scene.add.text(width / 2, height / 2, icon, {
fontSize: '128px'
});
iconText.setOrigin(0.5);
iconText.setDepth(9999);
iconText.setScrollFactor(0);
// Message
if (message) {
this.showSubtitle(message, 2000);
}
// Fade out
this.scene.tweens.add({
targets: [flash, iconText],
alpha: 0,
duration: 500,
ease: 'Power2',
onComplete: () => {
flash.destroy();
iconText.destroy();
}
});
console.log('⚡ Screen flash shown:', type, message);
}
// ========== SUBTITLES ==========
showSubtitle(text, duration = 3000, speaker = null, direction = null) {
if (!this.settings.subtitlesEnabled) return;
// Set subtitle text
this.subtitleText.setText(text);
this.subtitleText.setVisible(true);
this.subtitleBackground.setVisible(true);
// Show speaker name with color if enabled
if (speaker && this.settings.speakerNamesEnabled) {
const color = this.speakerColors[speaker] || '#ffffff';
this.subtitleSpeaker.setText(speaker);
this.subtitleSpeaker.setColor(color);
this.subtitleSpeaker.setVisible(true);
} else {
this.subtitleSpeaker.setVisible(false);
}
// Show directional arrows if enabled and direction provided
if (direction && this.settings.directionalArrowsEnabled) {
this.showDirectionalArrows(direction);
} else {
this.hideDirectionalArrows();
}
// Auto-hide after duration
this.scene.time.delayedCall(duration, () => {
this.hideSubtitle();
});
console.log('💬 Subtitle shown:', speaker ? `[${speaker}] ${text}` : text);
}
showDirectionalArrows(direction) {
this.hideDirectionalArrows();
if (direction === 'left' || direction === 'west') {
this.subtitleArrows.left.setVisible(true);
// Pulse animation
this.scene.tweens.add({
targets: this.subtitleArrows.left,
alpha: { from: 1, to: 0.3 },
duration: 500,
yoyo: true,
repeat: -1
});
} else if (direction === 'right' || direction === 'east') {
this.subtitleArrows.right.setVisible(true);
// Pulse animation
this.scene.tweens.add({
targets: this.subtitleArrows.right,
alpha: { from: 1, to: 0.3 },
duration: 500,
yoyo: true,
repeat: -1
});
} else if (direction === 'both') {
this.subtitleArrows.left.setVisible(true);
this.subtitleArrows.right.setVisible(true);
// Pulse animation for both
this.scene.tweens.add({
targets: [this.subtitleArrows.left, this.subtitleArrows.right],
alpha: { from: 1, to: 0.3 },
duration: 500,
yoyo: true,
repeat: -1
});
}
}
hideDirectionalArrows() {
this.scene.tweens.killTweensOf(this.subtitleArrows.left);
this.scene.tweens.killTweensOf(this.subtitleArrows.right);
this.subtitleArrows.left.setVisible(false);
this.subtitleArrows.right.setVisible(false);
this.subtitleArrows.left.setAlpha(1);
this.subtitleArrows.right.setAlpha(1);
}
hideSubtitle() {
this.subtitleText.setVisible(false);
this.subtitleBackground.setVisible(false);
this.subtitleSpeaker.setVisible(false);
this.hideDirectionalArrows();
}
// ========== SOUND EVENT HANDLERS ==========
onSoundPlayed(soundType, data = {}) {
if (!this.enabled) return;
switch (soundType) {
case 'damage':
this.showDamageIndicator(data.direction || 'down', data.amount || 10);
this.showSubtitle('[DAMAGE TAKEN]', 1500, 'System', data.direction);
break;
case 'pickup':
this.showSubtitle(`[PICKED UP: ${data.item || 'Item'}]`, 1500, 'System');
break;
case 'harvest':
this.showSubtitle('[CROP HARVESTED]', 1500, 'System');
break;
case 'build':
this.showSubtitle('[BUILDING PLACED]', 1500, 'System');
break;
case 'dig':
this.showSubtitle('[DIGGING SOUND]', 1000, 'System');
break;
case 'plant':
this.showSubtitle('[PLANTING SOUND]', 1000, 'System');
break;
case 'footsteps':
this.showSubtitle('[FOOTSTEPS]', 500, null, data.direction);
break;
case 'door':
this.showSubtitle('[DOOR OPENS]', 1000, 'System');
break;
case 'chest':
this.showSubtitle('[CHEST OPENS]', 1000, 'System');
break;
case 'water':
this.showSubtitle('[WATER SPLASH]', 1000, 'System');
break;
case 'fire':
this.showSubtitle('[FIRE CRACKLING]', 2000, 'System');
break;
case 'explosion':
this.showSubtitle('[EXPLOSION!]', 1500, 'System');
this.showScreenFlash('danger', '[EXPLOSION!]');
break;
case 'npc_talk':
this.showSubtitle(data.text || '[NPC TALKING]', 3000, data.speaker || 'NPC', data.direction);
break;
case 'enemy_growl':
this.showSubtitle('[ENEMY GROWL]', 1500, 'Enemy', data.direction);
break;
case 'fishing_cast':
this.showSubtitle('[FISHING LINE CAST]', 1000, 'System');
break;
case 'fishing_bite':
this.showSubtitle('[FISH BITING!]', 1500, 'System');
this.showFishingBobberCue();
break;
case 'danger':
this.showScreenFlash('danger', '[DANGER!]');
this.showSubtitle('[DANGER NEARBY]', 2000, 'System');
break;
case 'night':
this.showScreenFlash('warning', '[NIGHT FALLING]');
this.showSubtitle('[NIGHT IS FALLING]', 2000, 'System');
break;
case 'achievement':
this.showScreenFlash('success', data.message || '[ACHIEVEMENT UNLOCKED]');
this.showSubtitle(data.message || '[ACHIEVEMENT UNLOCKED]', 3000, 'System');
break;
case 'ui_click':
this.showSubtitle('[CLICK]', 300, null);
break;
case 'ui_hover':
this.showSubtitle('[HOVER]', 200, null);
break;
}
}
/**
* Show fishing bobber visual cue
*/
showFishingBobberCue() {
if (!this.settings.fishingBobberEnabled) return;
const width = this.scene.cameras.main.width;
const height = this.scene.cameras.main.height;
// Create bobber indicator if it doesn't exist
if (!this.fishingBobberIndicator) {
this.fishingBobberIndicator = this.scene.add.container(width / 2, height / 2);
this.fishingBobberIndicator.setDepth(10002);
this.fishingBobberIndicator.setScrollFactor(0);
// Circle background
const circle = this.scene.add.circle(0, 0, 60, 0xff6600, 0.8);
this.fishingBobberIndicator.add(circle);
// Exclamation mark
const exclamation = this.scene.add.text(0, 0, '!', {
fontSize: '48px',
fontFamily: 'Arial',
fontStyle: 'bold',
color: '#ffffff'
});
exclamation.setOrigin(0.5);
this.fishingBobberIndicator.add(exclamation);
// Text below
const text = this.scene.add.text(0, 80, 'FISH BITING!\nPress E', {
fontSize: '20px',
fontFamily: 'Arial',
fontStyle: 'bold',
color: '#ffffff',
align: 'center'
});
text.setOrigin(0.5);
this.fishingBobberIndicator.add(text);
}
// Show and animate
this.fishingBobberIndicator.setVisible(true);
this.fishingBobberIndicator.setAlpha(0);
// Fade in and pulse
this.scene.tweens.add({
targets: this.fishingBobberIndicator,
alpha: 1,
duration: 200,
onComplete: () => {
// Pulse animation
this.scene.tweens.add({
targets: this.fishingBobberIndicator,
scale: { from: 1, to: 1.2 },
duration: 300,
yoyo: true,
repeat: 5,
onComplete: () => {
// Fade out
this.scene.tweens.add({
targets: this.fishingBobberIndicator,
alpha: 0,
duration: 300,
onComplete: () => {
this.fishingBobberIndicator.setVisible(false);
}
});
}
});
}
});
console.log('🎣 Fishing bobber cue shown');
}
/**
* Set subtitle background opacity
*/
setSubtitleOpacity(opacity) {
this.settings.subtitleOpacity = Phaser.Math.Clamp(opacity, 0, 1);
if (this.subtitleBackground) {
this.subtitleBackground.setAlpha(this.settings.subtitleOpacity);
}
this.saveSettings();
console.log('📊 Subtitle opacity set to:', this.settings.subtitleOpacity);
}
/**
* Add custom speaker color
*/
addSpeakerColor(speaker, color) {
this.speakerColors[speaker] = color;
console.log(`🎨 Added speaker color: ${speaker} = ${color}`);
}
// ========== SETTINGS ==========
toggleHeartbeat(enabled) {
this.settings.heartbeatEnabled = enabled;
if (!enabled) this.stopHeartbeat();
this.saveSettings();
}
toggleDamageIndicator(enabled) {
this.settings.damageIndicatorEnabled = enabled;
this.saveSettings();
}
toggleScreenFlash(enabled) {
this.settings.screenFlashEnabled = enabled;
this.saveSettings();
}
toggleSubtitles(enabled) {
this.settings.subtitlesEnabled = enabled;
if (!enabled) this.hideSubtitle();
this.saveSettings();
console.log('💬 Subtitles:', enabled ? 'ENABLED' : 'DISABLED');
}
toggleDirectionalArrows(enabled) {
this.settings.directionalArrowsEnabled = enabled;
if (!enabled) {
this.hideDirectionalArrows();
}
this.saveSettings();
console.log('➡️ Directional Arrows:', enabled ? 'ENABLED' : 'DISABLED');
}
toggleSpeakerNames(enabled) {
this.settings.speakerNamesEnabled = enabled;
this.saveSettings();
console.log('👤 Speaker Names:', enabled ? 'ENABLED' : 'DISABLED');
}
toggleFishingBobber(enabled) {
this.settings.fishingBobberEnabled = enabled;
this.saveSettings();
console.log('🎣 Fishing Bobber Cue:', enabled ? 'ENABLED' : 'DISABLED');
}
/**
* Set subtitle text size
* @param {string} size - 'small', 'medium', 'large', 'very-large'
*/
setSubtitleSize(size) {
if (!this.subtitleSizes[size]) {
console.error(`Invalid subtitle size: ${size}. Valid options: small, medium, large, very-large`);
return;
}
this.settings.subtitleSize = size;
const sizes = this.subtitleSizes[size];
// Update text sizes
if (this.subtitleText) {
this.subtitleText.setFontSize(sizes.main);
}
if (this.subtitleSpeaker) {
this.subtitleSpeaker.setFontSize(sizes.speaker);
}
if (this.subtitleArrows.left) {
this.subtitleArrows.left.setFontSize(sizes.arrow);
}
if (this.subtitleArrows.right) {
this.subtitleArrows.right.setFontSize(sizes.arrow);
}
// Adjust background height based on text size
if (this.subtitleBackground) {
const bgHeight = sizes.main * 4; // 4x font size for padding
this.subtitleBackground.setSize(this.subtitleBackground.width, bgHeight);
}
this.saveSettings();
console.log(`📏 Subtitle size set to: ${size.toUpperCase()} (${sizes.main}px)`);
}
saveSettings() {
localStorage.setItem('novafarma_visual_sound_cues', JSON.stringify(this.settings));
}
loadSettings() {
const saved = localStorage.getItem('novafarma_visual_sound_cues');
if (saved) {
this.settings = { ...this.settings, ...JSON.parse(saved) };
}
}
// ========== UPDATE ==========
update() {
// Update heartbeat based on player health
if (this.scene.player && this.scene.player.health !== undefined) {
const healthPercent = (this.scene.player.health / this.scene.player.maxHealth) * 100;
this.updateHeartbeat(healthPercent);
}
}
destroy() {
if (this.heartbeatTween) this.heartbeatTween.stop();
if (this.heartbeatSprite) this.heartbeatSprite.destroy();
if (this.subtitleText) this.subtitleText.destroy();
if (this.subtitleBackground) this.subtitleBackground.destroy();
}
}

View File

@@ -0,0 +1,361 @@
/**
* ZombieCommunicationSystem.js
* =============================
* KRVAVA ŽETEV - Zombie Communication System (Hybrid Skill)
*
* Features:
* - Level-based zombie understanding
* - Level 1: Groaning only ("Hnggg...")
* - Level 5: Keywords in subtitles
* - Level 10: Full sentences (warnings, memories)
* - Subtitle UI
* - Translation system
*
* @author NovaFarma Team
* @date 2025-12-23
*/
export default class ZombieCommunicationSystem {
constructor(scene) {
this.scene = scene;
// Player's communication level
this.communicationLevel = 0;
this.maxLevel = 10;
// Subtitle UI
this.subtitleText = null;
this.subtitleContainer = null;
this.currentSubtitle = null;
// Zombie speech library
this.zombiePhrases = new Map();
console.log('🧠 ZombieCommunicationSystem initialized');
// Create subtitle UI
this.createSubtitleUI();
// Load zombie phrases
this.loadZombiePhrases();
}
/**
* Create subtitle UI
*/
createSubtitleUI() {
const width = this.scene.cameras.main.width;
const height = this.scene.cameras.main.height;
// Container at bottom of screen
this.subtitleContainer = this.scene.add.container(width / 2, height - 100);
this.subtitleContainer.setScrollFactor(0);
this.subtitleContainer.setDepth(10000);
this.subtitleContainer.setAlpha(0);
// Background
const bg = this.scene.add.rectangle(0, 0, 800, 100, 0x000000, 0.8);
this.subtitleContainer.add(bg);
// Subtitle text
this.subtitleText = this.scene.add.text(0, 0, '', {
fontSize: '24px',
fontFamily: 'Arial',
color: '#00FF00', // Green zombie text
align: 'center',
wordWrap: { width: 750 }
});
this.subtitleText.setOrigin(0.5);
this.subtitleContainer.add(this.subtitleText);
console.log('✅ Subtitle UI created');
}
/**
* Load zombie phrase library
*/
loadZombiePhrases() {
// Level 1: Pure groaning
const level1Phrases = [
{ zombie: 'Hnggg...', translation: null },
{ zombie: 'Grrraaa...', translation: null },
{ zombie: 'Uuuhhh...', translation: null },
{ zombie: 'Aaarrgh...', translation: null }
];
// Level 5: Keywords visible
const level5Phrases = [
{ zombie: 'Hnggg... HUNGER... grrr...', translation: 'I am hungry...' },
{ zombie: 'Grrr... DANGER... hnggg...', translation: 'Danger nearby!' },
{ zombie: 'Uhhh... MASTER... grrr...', translation: 'Looking for master...' },
{ zombie: 'Aaah... PAIN... hnggg...', translation: 'I am in pain...' },
{ zombie: 'Grrr... HELP... uhhh...', translation: 'Help me...' },
{ zombie: 'Hnggg... FRIEND... grrr...', translation: 'You are my friend...' }
];
// Level 10: Full sentences
const level10Phrases = [
{ zombie: 'I remember... my family...', translation: 'I remember my family before I turned...' },
{ zombie: 'The darkness... it hurts...', translation: 'The curse is painful...' },
{ zombie: 'Thank you... for saving me...', translation: 'Thank you for taming me instead of killing me.' },
{ zombie: 'Enemies... coming from east...', translation: 'I sense enemies approaching from the east!' },
{ zombie: 'My name was... John...', translation: 'I remember my name was John...' },
{ zombie: 'Ana... she calls us...', translation: 'Ana\'s Twin Bond resonates with us zombies...' },
{ zombie: 'The Black Serpent... did this...', translation: 'The Black Serpent Initiative caused this outbreak.' },
{ zombie: 'I was a farmer... before...', translation: 'I was a farmer before the infection...' },
{ zombie: 'Danger! Big zombie nearby!', translation: 'WARNING: Boss zombie detected!' },
{ zombie: 'I protect you... master...', translation: 'I will protect you with my unlife, master.' }
];
// Special contextual phrases
const contextualPhrases = [
{ context: 'low_health', zombie: 'Hnggg... weak... dying...', translation: 'I am badly hurt!' },
{ context: 'enemy_near', zombie: 'Grrr! Intruders!', translation: 'Enemies detected!' },
{ context: 'happy', zombie: 'Grraaa... good... happy...', translation: 'I am happy serving you!' },
{ context: 'task_complete', zombie: 'Uhhh... done... master...', translation: 'Task completed, master!' },
{ context: 'hungry', zombie: 'Need... food... hnggg...', translation: 'I need to eat soon...' },
{ context: 'scared', zombie: 'Aaaah! Fear! Run!', translation: 'Something terrifying is here!' }
];
this.zombiePhrases.set('level1', level1Phrases);
this.zombiePhrases.set('level5', level5Phrases);
this.zombiePhrases.set('level10', level10Phrases);
this.zombiePhrases.set('contextual', contextualPhrases);
console.log(`✅ Loaded ${level1Phrases.length + level5Phrases.length + level10Phrases.length + contextualPhrases.length} zombie phrases`);
}
/**
* Set communication level
*/
setCommunicationLevel(level) {
this.communicationLevel = Math.min(this.maxLevel, Math.max(0, level));
console.log(`🧠 Communication level: ${this.communicationLevel}/10`);
this.showNotification({
title: 'Zombie Understanding Improved!',
text: `🧠 Level ${this.communicationLevel}: ${this.getLevelDescription()}`,
icon: '🧟'
});
}
/**
* Get level description
*/
getLevelDescription() {
if (this.communicationLevel >= 10) {
return 'Full sentences! You understand zombies completely!';
} else if (this.communicationLevel >= 5) {
return 'Keywords visible! You understand basic meanings!';
} else {
return 'Only groaning... You need more practice!';
}
}
/**
* Zombie speaks
*/
zombieSpeak(zombieId, context = null) {
let phrase;
// Get appropriate phrase based on level
if (context) {
phrase = this.getContextualPhrase(context);
} else if (this.communicationLevel >= 10) {
phrase = this.getRandomPhrase('level10');
} else if (this.communicationLevel >= 5) {
phrase = this.getRandomPhrase('level5');
} else {
phrase = this.getRandomPhrase('level1');
}
if (!phrase) return;
// Show subtitle
this.showSubtitle(phrase, zombieId);
}
/**
* Get random phrase from level
*/
getRandomPhrase(level) {
const phrases = this.zombiePhrases.get(level);
if (!phrases || phrases.length === 0) return null;
return Phaser.Utils.Array.GetRandom(phrases);
}
/**
* Get contextual phrase
*/
getContextualPhrase(context) {
const phrases = this.zombiePhrases.get('contextual');
const found = phrases.filter(p => p.context === context);
if (found.length === 0) return this.getRandomPhrase('level1');
return Phaser.Utils.Array.GetRandom(found);
}
/**
* Show subtitle
*/
showSubtitle(phrase, zombieId = 'Zombie') {
let displayText = phrase.zombie;
// Add translation if level is high enough
if (this.communicationLevel >= 5 && phrase.translation) {
displayText += `\n[${phrase.translation}]`;
}
// Show speaker name
displayText = `${zombieId}: ${displayText}`;
this.subtitleText.setText(displayText);
// Fade in
this.scene.tweens.add({
targets: this.subtitleContainer,
alpha: 1,
duration: 300
});
// Auto-hide after 3 seconds
if (this.currentSubtitle) {
clearTimeout(this.currentSubtitle);
}
this.currentSubtitle = setTimeout(() => {
this.hideSubtitle();
}, 3000);
console.log(`💬 ${displayText}`);
}
/**
* Hide subtitle
*/
hideSubtitle() {
this.scene.tweens.add({
targets: this.subtitleContainer,
alpha: 0,
duration: 300
});
this.currentSubtitle = null;
}
/**
* Zombie conversation (interactive)
*/
startConversation(zombieId) {
if (this.communicationLevel < 5) {
this.showSubtitle({
zombie: 'Hnggg... grrr...',
translation: null
}, zombieId);
this.showNotification({
title: 'Cannot Understand',
text: 'Your zombie communication skill is too low!',
icon: '🧠'
});
return false;
}
console.log(`💬 Conversation with ${zombieId}`);
// Show conversation phrases
const phrases = [
'What is your name?',
'What do you remember?',
'Are you loyal?',
'Do you feel pain?',
'Goodbye'
];
// TODO: Create actual dialogue UI with choices
console.log('Conversation options:', phrases);
return true;
}
/**
* Zombie warning (important messages)
*/
zombieWarning(message, urgency = 'normal') {
const urgencyIcons = {
low: '',
normal: '⚠️',
high: '🚨',
critical: '💀'
};
this.showSubtitle({
zombie: message,
translation: message
}, `${urgencyIcons[urgency]} ZOMBIE ALERT`);
// Play alert sound for high/critical
if (urgency === 'high' || urgency === 'critical') {
// TODO: Play alert sound
this.scene.cameras.main.shake(200, 0.005);
}
}
/**
* Level up communication skill
*/
levelUpCommunication() {
if (this.communicationLevel >= this.maxLevel) {
console.log('🧠 Already at max level!');
return false;
}
this.setCommunicationLevel(this.communicationLevel + 1);
// Show what's unlocked
if (this.communicationLevel === 5) {
this.showNotification({
title: 'Keywords Unlocked!',
text: '🧠 You can now see KEYWORDS in zombie speech!',
icon: '✨'
});
} else if (this.communicationLevel === 10) {
this.showNotification({
title: 'Full Understanding!',
text: '🧠 You can now understand COMPLETE zombie sentences!',
icon: '👑'
});
}
return true;
}
/**
* Get communication level
*/
getCommunicationLevel() {
return this.communicationLevel;
}
/**
* Can understand zombie
*/
canUnderstandZombie(requiredLevel = 1) {
return this.communicationLevel >= requiredLevel;
}
/**
* Helper: Show notification
*/
showNotification(notification) {
console.log(`📢 ${notification.icon} ${notification.title}: ${notification.text}`);
const ui = this.scene.scene.get('UIScene');
if (ui && ui.showNotification) {
ui.showNotification(notification);
}
}
}

View File

@@ -0,0 +1,569 @@
/**
* ZOMBIE ECONOMY & CITY SANITATION SYSTEM
* Mrtva Dolina - Worker Zombies, Smetarji, Redka Darila
*
* Features:
* - Worker Zombies (heavy labor, construction)
* - Sanitation System (Smetar cleans city)
* - Rare Gift System (unique rewards from zombie work)
* - Zombie Maintenance (Brain feeding)
* - Contract System (loan zombies to NPCs)
* - Happiness impact on city growth
*/
export class ZombieEconomySystem {
constructor(scene) {
this.scene = scene;
// Worker zombie types
this.zombieTypes = {
scout: {
name: 'Zombi Skavt',
strength: 30,
speed: 60,
brainConsumption: 0.5, // per hour
tasks: ['scouting', 'light_work']
},
worker: {
name: 'Delovni Zombi',
strength: 80,
speed: 40,
brainConsumption: 1.5, // per hour
tasks: ['construction', 'hauling', 'sanitation']
},
sanitation: {
name: 'Smetar Zombi',
strength: 50,
speed: 50,
brainConsumption: 1.0, // per hour
tasks: ['cleaning', 'graffiti_removal']
}
};
// Active zombies
this.activeZombies = [];
// Contracts (zombies loaned to NPCs)
this.activeContracts = [];
// Rare gifts catalogue
this.rareGifts = [
{ id: 'family_heirloom', name: 'Starinski družinski artefakt', rarity: 'legendary', value: 1000 },
{ id: 'ancient_dye', name: 'Starodavno barvilo', rarity: 'epic', value: 500 },
{ id: 'rare_seed_pack', name: 'Paket redkih semen', rarity: 'rare', value: 300 },
{ id: 'kai_memory_photo', name: 'Kaijeva skrita fotografija', rarity: 'legendary', value: 2000 },
{ id: 'mystical_paint', name: 'Mistično barvilo', rarity: 'epic', value: 600 }
];
// City cleanliness tracking
this.cityTrash = [];
this.cityGraffiti = [];
this.cleanlinessScore = 50; // 0-100
// Happiness impact
this.citizenHappiness = 50; // 0-100
this.init();
}
init() {
// Listen for trash generation
this.scene.events.on('npc:activity', this.onNPCActivity, this);
this.scene.events.on('vandal:graffiti', this.onGraffitiCreated, this);
// Start passive systems
this.startBrainConsumption();
this.startCleaningSweep();
console.log('✅ ZombieEconomySystem initialized');
}
/**
* RECRUIT ZOMBIE
*/
recruitZombie(type, name = null) {
const zombieData = this.zombieTypes[type];
if (!zombieData) {
console.error(`Unknown zombie type: ${type}`);
return null;
}
const zombie = {
id: `zombie_${Date.now()}`,
type: type,
name: name || zombieData.name,
strength: zombieData.strength,
speed: zombieData.speed,
brainLevel: 100, // 0-100, energy/hunger
brainConsumption: zombieData.brainConsumption,
tasks: zombieData.tasks,
currentTask: null,
isContracted: false,
grumbleTimer: null,
x: this.scene.player.x,
y: this.scene.player.y,
sprite: null
};
// Create sprite
zombie.sprite = this.scene.add.sprite(zombie.x, zombie.y, `zombie_${type}`);
zombie.sprite.setDepth(5);
this.activeZombies.push(zombie);
console.log(`🧟 Recruited: ${zombie.name}`);
return zombie;
}
/**
* FEED ZOMBIE (with brains)
*/
feedZombie(zombieId, brainsAmount = 50) {
const zombie = this.activeZombies.find(z => z.id === zombieId);
if (!zombie) return false;
zombie.brainLevel = Math.min(100, zombie.brainLevel + brainsAmount);
// Happy zombie sound
this.scene.sound.play('zombie_satisfied', { volume: 0.5 });
// Show feedback
this.scene.events.emit('show-floating-text', {
x: zombie.sprite.x,
y: zombie.sprite.y - 30,
text: '*munch* ...dobr!',
color: '#90EE90'
});
// Stop grumbling
if (zombie.grumbleTimer) {
clearInterval(zombie.grumbleTimer);
zombie.grumbleTimer = null;
}
return true;
}
/**
* BRAIN CONSUMPTION - Passive hunger system
*/
startBrainConsumption() {
setInterval(() => {
this.activeZombies.forEach(zombie => {
// Reduce brain level based on consumption rate
zombie.brainLevel = Math.max(0, zombie.brainLevel - (zombie.brainConsumption / 60)); // per minute
// Check hunger
if (zombie.brainLevel < 30) {
this.zombieHungry(zombie);
}
// Critical hunger - zombie becomes slow
if (zombie.brainLevel < 10) {
zombie.speed *= 0.5; // 50% slower
if (zombie.sprite) {
zombie.sprite.setTint(0x666666); // Darken sprite
}
}
});
}, 60000); // Every minute
}
zombieHungry(zombie) {
if (zombie.grumbleTimer) return; // Already grumbling
// Start periodic grumbling
zombie.grumbleTimer = setInterval(() => {
const hungerLines = [
'Braaaaains...',
'Možgaaaaani...',
'Hrrrngh... lačen...',
'*godrnja o hrani*'
];
const line = Phaser.Utils.Array.GetRandom(hungerLines);
// Show speech bubble
this.scene.events.emit('show-speech-bubble', {
x: zombie.sprite.x,
y: zombie.sprite.y - 50,
text: line,
duration: 3000
});
// Play hungry sound
if (this.scene.sound) {
this.scene.sound.play('zombie_groan', { volume: 0.4 });
}
}, 15000); // Every 15 seconds
}
/**
* CONTRACT SYSTEM - Loan zombies to NPCs
*/
createContract(zombieId, npc, task, duration, payment) {
const zombie = this.activeZombies.find(z => z.id === zombieId);
if (!zombie || zombie.isContracted) {
console.warn('Zombie not available for contract');
return false;
}
const contract = {
id: `contract_${Date.now()}`,
zombieId: zombieId,
npc: npc, // 'ivan_kovac', 'tehnik', etc.
task: task, // 'wall_construction', 'steel_hauling', etc.
duration: duration, // in game hours
payment: payment, // gold or rare gift
startTime: this.scene.gameTime || Date.now(),
completed: false
};
zombie.isContracted = true;
zombie.currentTask = task;
this.activeContracts.push(contract);
// Move zombie to work site
this.moveZombieToWorkSite(zombie, npc);
console.log(`📄 Contract created: ${zombie.name}${npc} (${task})`);
// Start work visuals
this.startWorkAnimation(zombie, task);
return contract;
}
moveZombieToWorkSite(zombie, npc) {
const workSites = {
ivan_kovac: { x: 300, y: 200 }, // Blacksmith
tehnik: { x: 500, y: 250 }, // Tech Workshop
mayor: { x: 400, y: 300 } // Town Hall
};
const site = workSites[npc] || { x: 400, y: 300 };
if (zombie.sprite) {
this.scene.tweens.add({
targets: zombie.sprite,
x: site.x,
y: site.y,
duration: 2000,
ease: 'Sine.easeInOut'
});
}
}
startWorkAnimation(zombie, task) {
// Different animations for different tasks
const animations = {
wall_construction: 'zombie_hammering',
steel_hauling: 'zombie_carrying',
sanitation: 'zombie_sweeping',
graffiti_removal: 'zombie_scrubbing'
};
const anim = animations[task] || 'zombie_working';
if (zombie.sprite && zombie.sprite.anims) {
zombie.sprite.play(anim, true);
}
}
/**
* COMPLETE CONTRACT - Award payment
*/
completeContract(contractId) {
const contract = this.activeContracts.find(c => c.id === contractId);
if (!contract || contract.completed) return;
const zombie = this.activeZombies.find(z => z.id === contract.zombieId);
contract.completed = true;
zombie.isContracted = false;
zombie.currentTask = null;
// Award payment
if (contract.payment.type === 'gold') {
this.scene.inventorySystem.addGold(contract.payment.amount);
this.scene.events.emit('show-notification', {
title: 'Pogodba Končana',
message: `${zombie.name} je končal delo! +${contract.payment.amount} zlata.`,
icon: '💰',
duration: 3000
});
} else if (contract.payment.type === 'rare_gift') {
this.awardRareGift(contract.payment.giftId);
}
// Return zombie to player
if (zombie.sprite) {
this.scene.tweens.add({
targets: zombie.sprite,
x: this.scene.player.x + 50,
y: this.scene.player.y,
duration: 2000,
ease: 'Sine.easeOut'
});
}
console.log(`✅ Contract completed: ${contract.task}`);
}
/**
* RARE GIFT SYSTEM
*/
awardRareGift(giftId = null) {
// Random gift if not specified
if (!giftId) {
const gift = Phaser.Utils.Array.GetRandom(this.rareGifts);
giftId = gift.id;
}
const gift = this.rareGifts.find(g => g.id === giftId);
if (!gift) return;
// Add to inventory
if (this.scene.inventorySystem) {
this.scene.inventorySystem.addItem(gift.id, 1);
}
// Show special notification
this.scene.events.emit('show-notification', {
title: '🎁 REDKO DARILO!',
message: `Prejel si: ${gift.name} (${gift.rarity})`,
icon: '✨',
duration: 7000,
color: this.getRarityColor(gift.rarity)
});
// Play special sound
if (this.scene.sound) {
this.scene.sound.play('rare_gift_fanfare', { volume: 0.7 });
}
console.log(`🎁 Awarded rare gift: ${gift.name}`);
}
getRarityColor(rarity) {
const colors = {
legendary: '#FFD700', // Gold
epic: '#9B59B6', // Purple
rare: '#3498DB' // Blue
};
return colors[rarity] || '#FFFFFF';
}
/**
* SANITATION SYSTEM - Trash & Graffiti
*/
onNPCActivity(npcData) {
// NPCs randomly drop trash
if (Math.random() < 0.1) { // 10% chance
this.spawnTrash(npcData.x, npcData.y);
}
}
spawnTrash(x, y) {
const trashTypes = ['trash_bag', 'litter', 'broken_bottle', 'paper_waste'];
const type = Phaser.Utils.Array.GetRandom(trashTypes);
const trash = this.scene.add.sprite(x, y, type);
trash.setDepth(1);
this.cityTrash.push({
id: `trash_${Date.now()}_${Math.random()}`,
x: x,
y: y,
type: type,
sprite: trash
});
// Lower cleanliness
this.cleanlinessScore = Math.max(0, this.cleanlinessScore - 2);
this.updateCityStats();
}
onGraffitiCreated(graffitiData) {
const graffiti = this.scene.add.sprite(graffitiData.x, graffitiData.y, 'graffiti_tag');
graffiti.setDepth(2);
this.cityGraffiti.push({
id: `graffiti_${Date.now()}`,
x: graffitiData.x,
y: graffitiData.y,
sprite: graffiti
});
// Lower cleanliness
this.cleanlinessScore = Math.max(0, this.cleanlinessScore - 5);
this.updateCityStats();
}
/**
* CLEANING SWEEP - Zombies clean autonomously
*/
startCleaningSweep() {
setInterval(() => {
// Find sanitation zombies
const cleaners = this.activeZombies.filter(z =>
z.tasks.includes('cleaning') &&
!z.isContracted &&
z.brainLevel > 20
);
cleaners.forEach(cleaner => {
// Clean nearest trash
const nearestTrash = this.findNearestTrash(cleaner.sprite.x, cleaner.sprite.y);
if (nearestTrash) {
this.cleanTrash(cleaner, nearestTrash);
}
// Clean nearest graffiti
const nearestGraffiti = this.findNearestGraffiti(cleaner.sprite.x, cleaner.sprite.y);
if (nearestGraffiti) {
this.cleanGraffiti(cleaner, nearestGraffiti);
}
});
}, 10000); // Every 10 seconds
}
findNearestTrash(x, y) {
let nearest = null;
let minDist = Infinity;
this.cityTrash.forEach(trash => {
const dist = Phaser.Math.Distance.Between(x, y, trash.x, trash.y);
if (dist < minDist) {
minDist = dist;
nearest = trash;
}
});
return minDist < 200 ? nearest : null; // Within 200px
}
findNearestGraffiti(x, y) {
let nearest = null;
let minDist = Infinity;
this.cityGraffiti.forEach(graffiti => {
const dist = Phaser.Math.Distance.Between(x, y, graffiti.x, graffiti.y);
if (dist < minDist) {
minDist = dist;
nearest = graffiti;
}
});
return minDist < 200 ? nearest : null;
}
cleanTrash(zombie, trash) {
// Move to trash
this.scene.tweens.add({
targets: zombie.sprite,
x: trash.x,
y: trash.y,
duration: 1000,
onComplete: () => {
// Play cleaning animation
if (zombie.sprite.anims) {
zombie.sprite.play('zombie_sweeping', true);
}
// Remove trash after delay
setTimeout(() => {
trash.sprite.destroy();
this.cityTrash = this.cityTrash.filter(t => t.id !== trash.id);
// Increase cleanliness
this.cleanlinessScore = Math.min(100, this.cleanlinessScore + 3);
this.updateCityStats();
console.log(`🧹 ${zombie.name} cleaned trash`);
}, 2000);
}
});
}
cleanGraffiti(zombie, graffiti) {
// Move to graffiti
this.scene.tweens.add({
targets: zombie.sprite,
x: graffiti.x,
y: graffiti.y,
duration: 1000,
onComplete: () => {
// Play scrubbing animation
if (zombie.sprite.anims) {
zombie.sprite.play('zombie_scrubbing', true);
}
// Remove graffiti with fade
this.scene.tweens.add({
targets: graffiti.sprite,
alpha: 0,
duration: 3000,
onComplete: () => {
graffiti.sprite.destroy();
this.cityGraffiti = this.cityGraffiti.filter(g => g.id !== graffiti.id);
// Increase cleanliness
this.cleanlinessScore = Math.min(100, this.cleanlinessScore + 5);
this.updateCityStats();
console.log(`🧽 ${zombie.name} removed graffiti`);
}
});
}
});
}
/**
* CITY STATS UPDATE
*/
updateCityStats() {
// Calculate happiness based on cleanliness
this.citizenHappiness = Math.min(100, this.cleanlinessScore * 1.2);
// Emit stats update
this.scene.events.emit('city:stats_updated', {
cleanliness: this.cleanlinessScore,
happiness: this.citizenHappiness,
trashCount: this.cityTrash.length,
graffitiCount: this.cityGraffiti.length
});
// Happiness affects NPC arrival rate
if (this.citizenHappiness > 70) {
// Faster settler arrivals
this.scene.events.emit('city:boost_immigration');
}
}
/**
* Get zombie status for UI
*/
getZombieStatus() {
return this.activeZombies.map(z => ({
id: z.id,
name: z.name,
type: z.type,
brainLevel: z.brainLevel,
currentTask: z.currentTask,
isContracted: z.isContracted
}));
}
destroy() {
this.activeZombies.forEach(zombie => {
if (zombie.sprite) zombie.sprite.destroy();
if (zombie.grumbleTimer) clearInterval(zombie.grumbleTimer);
});
this.cityTrash.forEach(trash => trash.sprite.destroy());
this.cityGraffiti.forEach(graffiti => graffiti.sprite.destroy());
}
}

View File

@@ -0,0 +1,464 @@
/**
* ZOMBIE MINER AUTOMATION SYSTEM
* Part of: Mining System Expansion
* Created: January 4, 2026
*
* Features:
* - Hire zombie miners for passive resource generation
* - Assign miners to specific mine depths
* - Efficiency & loyalty mechanics
* - Automated ore collection
* - Zombie equipment upgrades
*/
class ZombieMinerAutomationSystem {
constructor(game) {
this.game = game;
this.player = game.player;
// Zombie miners
this.zombieMiners = [];
this.maxZombieMiners = 10;
this.zombieMinerCost = 5000;
// Automation settings
this.automationActive = false;
this.totalYieldPerHour = 0;
this.lastCollectionTime = null;
// Equipment for zombies
this.zombieEquipment = {
pickaxe_tier: 1, // 1-5
helmet_lamp: false, // Better visibility
oxygen_tank: false, // Deeper mining
cart: false // Auto-transport
};
}
/**
* Hire a zombie miner
*/
hireZombieMiner() {
if (this.zombieMiners.length >= this.maxZombieMiners) {
return {
success: false,
message: `Maximum ${this.maxZombieMiners} zombie miners allowed!`
};
}
if (this.player.money < this.zombieMinerCost) {
return {
success: false,
message: `Need ${this.zombieMinerCost}g to hire zombie miner!`
};
}
// Hire zombie
this.player.money -= this.zombieMinerCost;
const miner = {
id: `zombie_miner_${this.zombieMiners.length + 1}`,
name: this.generateZombieName(),
assignedMine: null,
assignedDepth: 0,
efficiency: 1.0, // 0.5 - 2.0
loyalty: 50, // 0-100
yieldPerHour: 5, // Base yield
level: 1,
experience: 0
};
this.zombieMiners.push(miner);
// Recalculate automation
this.updateAutomationYield();
this.game.showMessage(
`Hired ${miner.name}! (${this.zombieMiners.length}/${this.maxZombieMiners})`
);
return { success: true, miner: miner };
}
/**
* Generate random zombie miner name
*/
generateZombieName() {
const prefixes = ['Grumpy', 'Rusty', 'Dusty', 'Grumbly', 'Moaning', 'Shuffling'];
const suffixes = ['Zed', 'Mort', 'Bones', 'Guts', 'Picks', 'Drills'];
const prefix = Phaser.Utils.Array.GetRandom(prefixes);
const suffix = Phaser.Utils.Array.GetRandom(suffixes);
return `${prefix} ${suffix}`;
}
/**
* Assign zombie miner to specific mine & depth
*/
assignZombieMiner(minerId, mineId, depth) {
const miner = this.zombieMiners.find(m => m.id === minerId);
if (!miner) {
return { success: false, message: 'Zombie miner not found!' };
}
// Check if mine exists
const mine = this.game.miningSystem.getMineInfo(mineId);
if (!mine) {
return { success: false, message: 'Mine not found!' };
}
// Check if depth is unlocked
const maxDepth = this.game.miningSystem.maxDepthReached || 0;
if (depth > maxDepth) {
return {
success: false,
message: `Must explore depth ${depth} first!`
};
}
// Assign miner
miner.assignedMine = mineId;
miner.assignedDepth = depth;
// Update automation
this.updateAutomationYield();
this.game.showMessage(
`${miner.name} assigned to ${mine.name} - Depth ${depth}`
);
return { success: true };
}
/**
* Unassign zombie miner (return to surface)
*/
unassignZombieMiner(minerId) {
const miner = this.zombieMiners.find(m => m.id === minerId);
if (!miner) {
return { success: false };
}
miner.assignedMine = null;
miner.assignedDepth = 0;
// Update automation
this.updateAutomationYield();
this.game.showMessage(
`${miner.name} returned to surface.`
);
return { success: true };
}
/**
* Update total automation yield
*/
updateAutomationYield() {
let totalYield = 0;
this.zombieMiners.forEach(miner => {
if (miner.assignedMine && miner.assignedDepth > 0) {
// Base yield
let hourlyYield = miner.yieldPerHour;
// Depth bonus (+10% per 10 levels)
const depthBonus = (miner.assignedDepth / 10) * 0.1;
hourlyYield *= (1 + depthBonus);
// Efficiency factor
hourlyYield *= miner.efficiency;
// Loyalty factor (50% loyalty = 0.5x yield, 100% = 1.5x yield)
const loyaltyFactor = 0.5 + (miner.loyalty / 100);
hourlyYield *= loyaltyFactor;
// Equipment bonuses
if (this.zombieEquipment.pickaxe_tier > 1) {
hourlyYield *= (1 + (this.zombieEquipment.pickaxe_tier - 1) * 0.25);
}
if (this.zombieEquipment.cart) {
hourlyYield *= 1.5; // 50% faster collection
}
totalYield += hourlyYield;
}
});
this.totalYieldPerHour = Math.floor(totalYield);
this.automationActive = (totalYield > 0);
}
/**
* Collect automated mining resources
*/
collectAutomatedYield() {
if (!this.automationActive) {
return {
success: false,
message: 'No zombie miners assigned!'
};
}
// Calculate time since last collection
const hoursSinceLastCollection = this.getHoursSinceLastCollection();
if (hoursSinceLastCollection < 0.1) {
return {
success: false,
message: 'Collected too recently! Wait a bit.'
};
}
const resources = {};
// Collect from each zombie
this.zombieMiners.forEach(miner => {
if (miner.assignedMine && miner.assignedDepth > 0) {
// Get mine info
const mine = this.game.miningSystem.getMineInfo(miner.assignedMine);
// Determine ore type based on depth
const oreType = this.getOreTypeForDepth(mine, miner.assignedDepth);
// Calculate yield
const hourlyYield = miner.yieldPerHour * miner.efficiency *
(0.5 + miner.loyalty / 100);
const amount = Math.floor(hourlyYield * hoursSinceLastCollection);
if (amount > 0) {
resources[oreType] = (resources[oreType] || 0) + amount;
// Grant XP to miner
miner.experience += amount;
this.checkMinerLevelUp(miner);
}
}
});
// Add resources to inventory
let totalCollected = 0;
for (const [ore, amount] of Object.entries(resources)) {
this.player.inventory.addItem(ore, amount);
totalCollected += amount;
}
// Update last collection time
this.lastCollectionTime = this.game.time.currentTime;
// Show collection message
const resourceList = Object.entries(resources)
.map(([ore, amount]) => `${amount}x ${ore}`)
.join(', ');
this.game.showMessage(
`Collected: ${resourceList} (${hoursSinceLastCollection.toFixed(1)}h)`
);
return {
success: true,
resources: resources,
totalAmount: totalCollected,
hours: hoursSinceLastCollection
};
}
/**
* Get ore type based on mine and depth
*/
getOreTypeForDepth(mine, depth) {
// Logic from mine zones
if (depth <= 25) return 'copper_ore';
if (depth <= 50) return 'iron_ore';
if (depth <= 75) return 'gold_ore';
return 'diamond_ore';
}
/**
* Check if miner levels up
*/
checkMinerLevelUp(miner) {
const xpRequired = miner.level * 100;
if (miner.experience >= xpRequired) {
miner.level++;
miner.experience = 0;
// Bonuses per level
miner.yieldPerHour += 1;
miner.efficiency += 0.05;
this.game.showMessage(
`${miner.name} leveled up! (Lv.${miner.level})`
);
this.updateAutomationYield();
}
}
/**
* Feed zombie miner (restore loyalty)
*/
feedZombieMiner(minerId, foodType) {
const miner = this.zombieMiners.find(m => m.id === minerId);
if (!miner) {
return { success: false };
}
// Check if player has food
if (!this.player.inventory.hasItem(foodType)) {
return {
success: false,
message: 'You don\'t have this food!'
};
}
// Remove food
this.player.inventory.removeItem(foodType, 1);
// Loyalty bonus based on food quality
const loyaltyGain = this.getFoodLoyaltyValue(foodType);
miner.loyalty = Math.min(100, miner.loyalty + loyaltyGain);
this.game.showMessage(
`${miner.name} ate ${foodType}. Loyalty +${loyaltyGain} (${miner.loyalty}/100)`
);
// Update automation
this.updateAutomationYield();
return { success: true, loyaltyGain: loyaltyGain };
}
/**
* Get food loyalty value
*/
getFoodLoyaltyValue(foodType) {
const foodValues = {
'brain': 20, // Best food
'meat': 15,
'bread': 10,
'vegetables': 5
};
return foodValues[foodType] || 5;
}
/**
* Upgrade zombie equipment
*/
upgradeEquipment(equipmentType) {
const costs = {
pickaxe_tier: 3000,
helmet_lamp: 2000,
oxygen_tank: 2500,
cart: 4000
};
const cost = costs[equipmentType];
if (!cost) {
return { success: false, message: 'Invalid equipment!' };
}
// Check if already owned (except pickaxe tiers)
if (equipmentType !== 'pickaxe_tier') {
if (this.zombieEquipment[equipmentType]) {
return {
success: false,
message: 'Already owned!'
};
}
} else {
// Check pickaxe tier limit
if (this.zombieEquipment.pickaxe_tier >= 5) {
return {
success: false,
message: 'Pickaxe already max tier!'
};
}
}
// Check money
if (this.player.money < cost) {
return {
success: false,
message: `Need ${cost}g!`
};
}
// Purchase
this.player.money -= cost;
if (equipmentType === 'pickaxe_tier') {
this.zombieEquipment.pickaxe_tier++;
} else {
this.zombieEquipment[equipmentType] = true;
}
// Update automation
this.updateAutomationYield();
this.game.showMessage(
`Upgraded zombie equipment: ${equipmentType}! ${cost}g`
);
return { success: true };
}
/**
* Get hours since last collection
*/
getHoursSinceLastCollection() {
if (!this.lastCollectionTime) {
this.lastCollectionTime = this.game.time.currentTime;
return 0;
}
const secondsElapsed = this.game.time.currentTime - this.lastCollectionTime;
return secondsElapsed / 3600;
}
/**
* Get automation UI data
*/
getAutomationUIData() {
return {
zombieMiners: this.zombieMiners,
maxZombieMiners: this.maxZombieMiners,
totalYieldPerHour: this.totalYieldPerHour,
automationActive: this.automationActive,
equipment: this.zombieEquipment,
canHireMore: this.zombieMiners.length < this.maxZombieMiners,
hireCost: this.zombieMinerCost,
hoursSinceCollection: this.getHoursSinceLastCollection()
};
}
/**
* Update (passive decay of loyalty)
*/
update(deltaTime) {
// Loyalty slowly decays if zombies are working
this.zombieMiners.forEach(miner => {
if (miner.assignedMine && miner.assignedDepth > 0) {
// Lose 1 loyalty per hour worked
const loyaltyLoss = (deltaTime / 3600);
miner.loyalty = Math.max(0, miner.loyalty - loyaltyLoss);
}
});
// Recalculate if needed
if (this.automationActive) {
this.updateAutomationYield();
}
}
}

View File

@@ -0,0 +1,340 @@
/**
* ZOMBIE SCOUT LEVELING SYSTEM (1-20)
* XP progression, stat increases, level-up rewards
* Integrates with ZombieScout companion
*/
export class ZombieScoutLevelingSystem {
constructor(scene) {
this.scene = scene;
// Current stats
this.currentLevel = 1;
this.currentXP = 0;
this.skillPoints = 0;
// Stats per level
this.baseStats = {
health: 50,
attack: 5,
digSpeed: 1.0,
moveSpeed: 100,
defense: 0
};
this.currentStats = { ...this.baseStats };
// XP curve
this.xpRequirements = this.generateXPCurve();
// Level rewards
this.levelRewards = new Map();
this.init();
}
init() {
this.initializeLevelRewards();
}
/**
* Generate XP requirements for each level (exponential curve)
*/
generateXPCurve() {
const curve = [0]; // Level 1 = 0 XP
for (let level = 2; level <= 20; level++) {
// Formula: baseXP * (level ^ 1.5)
const baseXP = 100;
const requiredXP = Math.floor(baseXP * Math.pow(level, 1.5));
curve.push(requiredXP);
}
return curve;
}
/**
* Initialize rewards for each level
*/
initializeLevelRewards() {
// Skill points every level
for (let level = 2; level <= 20; level++) {
this.levelRewards.set(level, {
skillPoints: 1,
statBonus: this.getStatBonusForLevel(level),
specialUnlock: this.getSpecialUnlockForLevel(level)
});
}
}
/**
* Get stat bonus for specific level
*/
getStatBonusForLevel(level) {
const bonuses = {
health: 10, // +10 HP per level
attack: 1, // +1 attack per level
digSpeed: 0.05, // +5% dig speed per level
defense: 0.5 // +0.5 defense per level
};
// Every 5 levels: bonus move speed
if (level % 5 === 0) {
bonuses.moveSpeed = 10; // +10 speed at levels 5, 10, 15, 20
}
return bonuses;
}
/**
* Get special unlock for milestone levels
*/
getSpecialUnlockForLevel(level) {
const unlocks = {
5: { type: 'skill', name: 'Basic Attack', description: 'Zombie can attack enemies' },
10: { type: 'skill', name: 'Speed Dig', description: 'Dig 50% faster for 10 seconds' },
15: { type: 'skill', name: 'Treasure Sense', description: 'Reveal nearby buried items' },
20: { type: 'evolution', name: 'Scout Commander', description: 'Zombie becomes elite unit' }
};
return unlocks[level] || null;
}
/**
* Award XP to Zombie Scout
*/
awardXP(amount, source = 'general') {
this.currentXP += amount;
// Visual feedback
this.showXPGain(amount, source);
// Check for level up
this.checkLevelUp();
console.log(`+${amount} XP from ${source} (${this.currentXP}/${this.getRequiredXP()})`);
}
/**
* Check if Zombie Scout should level up
*/
checkLevelUp() {
if (this.currentLevel >= 20) return; // Max level cap
const requiredXP = this.getRequiredXP();
if (this.currentXP >= requiredXP) {
this.levelUp();
}
}
/**
* Level up Zombie Scout
*/
levelUp() {
this.currentLevel++;
this.currentXP -= this.getRequiredXP(this.currentLevel - 1); // Carry over excess XP
// Apply stat bonuses
const rewards = this.levelRewards.get(this.currentLevel);
if (rewards) {
// Apply stat increases
for (const [stat, bonus] of Object.entries(rewards.statBonus)) {
this.currentStats[stat] += bonus;
}
// Award skill points
this.skillPoints += rewards.skillPoints;
// Special unlock
if (rewards.specialUnlock) {
this.unlockSpecial(rewards.specialUnlock);
}
}
// Visual effects
this.playLevelUpEffects();
// Notification
this.scene.uiSystem?.showNotification(
`Zombie Scout reached Level ${this.currentLevel}!`,
'level_up',
{ skillPoints: this.skillPoints }
);
// Update Zombie Scout sprite if evolution
if (this.currentLevel === 20) {
this.evolveScout();
}
console.log(`🎉 LEVEL UP! Zombie Scout is now Level ${this.currentLevel}`);
console.log(`Stats:`, this.currentStats);
console.log(`Skill Points: ${this.skillPoints}`);
// Quest check
this.scene.questSystem?.checkScoutLevel(this.currentLevel);
}
/**
* Unlock special ability or feature
*/
unlockSpecial(unlock) {
console.log(`✨ Special Unlock: ${unlock.name} - ${unlock.description}`);
if (unlock.type === 'skill') {
this.scene.zombieScoutSkills?.unlockSkill(unlock.name);
} else if (unlock.type === 'evolution') {
// Mark for visual evolution
this.isEvolved = true;
}
this.scene.uiSystem?.showNotification(
`Unlocked: ${unlock.name}!`,
'unlock',
{ description: unlock.description }
);
}
/**
* Visual evolution at level 20
*/
evolveScout() {
// Update sprite to "Commander" version
const scout = this.scene.zombieScout;
if (scout) {
scout.setTexture('zombie_scout_commander');
scout.setScale(scout.scale * 1.2); // Slightly larger
// VFX: Evolution animation
this.scene.vfxSystem?.playEffect('evolution', scout.x, scout.y);
}
}
/**
* Award XP for different actions
*/
onCombatKill(enemyType) {
const xpValues = {
'zombie': 10,
'raider': 25,
'mutant': 50,
'boss': 200
};
this.awardXP(xpValues[enemyType] || 10, 'combat');
}
onDigComplete(itemRarity) {
const xpValues = {
'common': 5,
'uncommon': 15,
'rare': 30,
'legendary': 100
};
this.awardXP(xpValues[itemRarity] || 5, 'digging');
}
onExplorationProgress() {
// Award XP for exploring new areas
this.awardXP(20, 'exploration');
}
onQuestComplete(questDifficulty) {
const xpValues = {
'easy': 50,
'medium': 100,
'hard': 200,
'story': 300
};
this.awardXP(xpValues[questDifficulty] || 50, 'quest');
}
/**
* Get required XP for current or specific level
*/
getRequiredXP(level = this.currentLevel) {
return this.xpRequirements[level] || 0;
}
/**
* Get XP progress percentage
*/
getXPProgress() {
if (this.currentLevel >= 20) return 100;
const required = this.getRequiredXP();
return Math.min(100, Math.floor((this.currentXP / required) * 100));
}
/**
* Visual effects
*/
showXPGain(amount, source) {
// Floating text above Zombie Scout
const scout = this.scene.zombieScout;
if (scout) {
this.scene.vfxSystem?.floatingText(`+${amount} XP`, scout.x, scout.y - 30, '#FFD700');
}
}
playLevelUpEffects() {
const scout = this.scene.zombieScout;
if (!scout) return;
// VFX: Level up particles
this.scene.vfxSystem?.playEffect('level_up', scout.x, scout.y);
// SFX: Level up sound
this.scene.soundSystem?.play('level_up');
// Camera shake
this.scene.cameras.main.shake(200, 0.005);
// Flash
this.scene.cameras.main.flash(300, 255, 215, 0); // Gold flash
}
/**
* Get current level data
*/
getLevelData() {
return {
level: this.currentLevel,
xp: this.currentXP,
requiredXP: this.getRequiredXP(),
progress: this.getXPProgress(),
stats: { ...this.currentStats },
skillPoints: this.skillPoints,
isMaxLevel: this.currentLevel >= 20,
nextUnlock: this.levelRewards.get(this.currentLevel + 1)?.specialUnlock || null
};
}
/**
* Save/Load support
*/
getSaveData() {
return {
currentLevel: this.currentLevel,
currentXP: this.currentXP,
skillPoints: this.skillPoints,
currentStats: this.currentStats,
isEvolved: this.isEvolved || false
};
}
loadSaveData(data) {
this.currentLevel = data.currentLevel || 1;
this.currentXP = data.currentXP || 0;
this.skillPoints = data.skillPoints || 0;
this.currentStats = data.currentStats || { ...this.baseStats };
this.isEvolved = data.isEvolved || false;
// Apply visual evolution if loaded at level 20
if (this.isEvolved) {
this.evolveScout();
}
}
}

View 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();
}
});
}
}

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();
}
}

View File

@@ -0,0 +1,239 @@
/**
* ZOMBIE WORKER AI SYSTEM
* Tamed zombies lahko opravljajo delo (farming, mining)
*/
class ZombieWorkerSystem {
constructor(scene) {
this.scene = scene;
this.workers = [];
}
assignWork(zombie, workType, workRadius = 5) {
if (!zombie.isTamed) {
console.warn('⚠️ Cannot assign work to untamed zombie!');
return false;
}
zombie.workerData = {
type: workType,
radius: workRadius,
centerX: zombie.gridX,
centerY: zombie.gridY,
energy: 100,
decayRate: 0.5,
workTimer: 0,
workInterval: 5000,
status: 'IDLE',
inventory: {
seeds: 0,
hoe: 0,
watering_can: 0,
pickaxe: 0
}
};
this.workers.push(zombie);
console.log(`🧟 Assigned ${zombie.type} as ${workType} worker`);
return true;
}
removeWorker(zombie) {
const index = this.workers.indexOf(zombie);
if (index > -1) {
this.workers.splice(index, 1);
zombie.workerData = null;
}
}
update(delta) {
this.workers = this.workers.filter(w =>
this.scene.npcs.includes(w) && w.hp > 0
);
for (const worker of this.workers) {
if (!worker.workerData) continue;
this.applyDecay(worker, delta);
worker.workerData.workTimer += delta;
if (worker.workerData.workTimer >= worker.workerData.workInterval) {
worker.workerData.workTimer = 0;
if (worker.workerData.type === 'FARM') {
this.performFarmWork(worker);
} else if (worker.workerData.type === 'MINE') {
this.performMineWork(worker);
} else if (worker.workerData.type === 'CLEAR') {
this.performClearWork(worker);
}
}
}
}
applyDecay(zombie, delta) {
const wd = zombie.workerData;
wd.energy -= (wd.decayRate * delta) / 1000;
if (wd.energy <= 0) {
wd.energy = 0;
zombie.hp -= (wd.decayRate * delta) / 1000;
if (zombie.sprite) zombie.sprite.setTint(0x666666);
}
if (zombie.hp <= 0) this.onWorkerDeath(zombie);
}
performFarmWork(zombie) {
const wd = zombie.workerData;
const terrain = this.scene.terrainSystem;
const farming = this.scene.farmingSystem;
if (!terrain || !farming) return;
// 1. Water/Harvest existing crops
if (terrain.cropsMap) {
for (let dx = -wd.radius; dx <= wd.radius; dx++) {
for (let dy = -wd.radius; dy <= wd.radius; dy++) {
const key = `${wd.centerX + dx},${wd.centerY + dy}`;
if (terrain.cropsMap.has(key)) {
const crop = terrain.cropsMap.get(key);
if (!crop.isWatered) {
farming.waterCrop(wd.centerX + dx, wd.centerY + dy);
console.log(`🧟💧 Worker watered`);
wd.status = 'WORKING';
return;
}
if (crop.stage >= 4) {
farming.harvest(wd.centerX + dx, wd.centerY + dy);
console.log(`🧟🌾 Worker harvested`);
wd.status = 'WORKING';
return;
}
}
}
}
}
// 2. Plant on empty tilled soil (if has seeds)
if (wd.inventory.seeds > 0) {
for (let dx = -wd.radius; dx <= wd.radius; dx++) {
for (let dy = -wd.radius; dy <= wd.radius; dy++) {
const tile = terrain.getTile(wd.centerX + dx, wd.centerY + dy);
if (tile && tile.isTilled && !tile.hasCrop) {
farming.plant(wd.centerX + dx, wd.centerY + dy, 'wheat');
wd.inventory.seeds--;
console.log(`🧟🌱 Worker planted (Seeds: ${wd.inventory.seeds})`);
wd.status = 'WORKING';
return;
}
}
}
}
// 3. Till grass/dirt
for (let dx = -wd.radius; dx <= wd.radius; dx++) {
for (let dy = -wd.radius; dy <= wd.radius; dy++) {
const tile = terrain.getTile(wd.centerX + dx, wd.centerY + dy);
const typeName = tile?.type?.name || tile?.type;
if ((typeName === 'grass' || typeName === 'dirt') && !tile.isTilled) {
tile.isTilled = true;
if (tile.sprite) tile.sprite.setTint(0x8B4513);
console.log(`🧟🔨 Worker tilled soil`);
wd.status = 'WORKING';
return;
}
}
}
wd.status = 'IDLE';
}
performMineWork(zombie) {
const wd = zombie.workerData;
const terrain = this.scene.terrainSystem;
if (!terrain || !terrain.decorationsMap) return;
for (let dx = -wd.radius; dx <= wd.radius; dx++) {
for (let dy = -wd.radius; dy <= wd.radius; dy++) {
const key = `${wd.centerX + dx},${wd.centerY + dy}`;
if (terrain.decorationsMap.has(key)) {
const decor = terrain.decorationsMap.get(key);
if (decor.type === 'stone' || decor.type.includes('rock')) {
terrain.removeDecoration(wd.centerX + dx, wd.centerY + dy);
if (this.scene.inventorySystem) {
this.scene.inventorySystem.addItem('stone', 1);
}
console.log(`🧟⛏️ Worker mined stone`);
wd.status = 'WORKING';
return;
}
}
}
}
wd.status = 'IDLE';
}
performClearWork(zombie) {
const wd = zombie.workerData;
const terrain = this.scene.terrainSystem;
if (!terrain || !terrain.decorationsMap) return;
for (let dx = -wd.radius; dx <= wd.radius; dx++) {
for (let dy = -wd.radius; dy <= wd.radius; dy++) {
const key = `${wd.centerX + dx},${wd.centerY + dy}`;
if (terrain.decorationsMap.has(key)) {
const decor = terrain.decorationsMap.get(key);
const t = decor.type;
// Clear trees, bushes, logs, rocks
if (t.startsWith('tree') || t.startsWith('bush') || t === 'fallen_log' || t === 'stone' || t.startsWith('small_rock')) {
terrain.removeDecoration(wd.centerX + dx, wd.centerY + dy);
// Give some resources
if (this.scene.inventorySystem) {
if (t.startsWith('tree') || t === 'fallen_log' || t.startsWith('bush')) {
this.scene.inventorySystem.addItem('wood', 1);
} else {
this.scene.inventorySystem.addItem('stone', 1);
}
}
console.log(`🧟🪓 Worker CLEARED ${t}`);
wd.status = 'WORKING';
return; // One per tick
}
}
}
}
wd.status = 'IDLE';
}
onWorkerDeath(zombie) {
console.log(`💀 Worker died at ${zombie.gridX},${zombie.gridY}`);
if (this.scene.interactionSystem && this.scene.interactionSystem.spawnLoot) {
this.scene.interactionSystem.spawnLoot(zombie.gridX, zombie.gridY, 'fertilizer', 1);
}
if (this.scene.statsSystem) {
this.scene.statsSystem.addXP(10);
}
this.removeWorker(zombie);
}
feedWorker(zombie, amount = 50) {
if (!zombie.workerData) return false;
zombie.workerData.energy = Math.min(100, zombie.workerData.energy + amount);
if (zombie.sprite) zombie.sprite.clearTint();
console.log(`🍖 Fed worker`);
return true;
}
}