diff --git a/docs/CHARACTER_DESIGN_GUIDELINES.md b/docs/CHARACTER_DESIGN_GUIDELINES.md new file mode 100644 index 000000000..e60439ff3 --- /dev/null +++ b/docs/CHARACTER_DESIGN_GUIDELINES.md @@ -0,0 +1,139 @@ +# 🎨 CHARACTER DESIGN GUIDELINES - Mrtva Dolina +**Style:** Post-Apocalyptic Punk (Style 32 Dark-Chibi Noir) +**Last Updated:** 2026-01-05 14:04 CET + +--- + +## 🔥 **ZAŠČITNI ZNAK - Punk Aesthetic** + +**POMEMBNO:** Ne delaj preveč normalnih ljudi! Post-apokalipsa = punk survival look. + +### **Core Features (Mix & Match)** +NPC-ji naj imajo **raznolike kombinacije** teh elementov, ne vsi vse: + +#### **Pirsinge** 🔩 +- Nos ring +- Lip ring +- Eyebrow piercing +- Multiple ear piercings +- **Ne rabijo vsi!** En NPC lahko ima samo 1 piercing + +#### **Ear Gauges** 👂 +- Razširjena ušesa (stretched ear lobes) +- Različne velikosti (majhni do veliki) +- Opcijsko - ni mandatory + +#### **Dreadlokse** 🌿 +- **VARIACIJE:** + - Full head dreads (kot Kai, Gronk) + - **Partial dreads:** Ubrita glava z 6 dredloksi odzadi + - **Colored dreads:** Pink, green, purple, gray + - Kratki vs. dolgi + - **Ne rabijo vsi!** Lahko ima NPC samo normalne lase + +#### **Tatuji** 🖋️ +- Neck tattoos +- Arm sleeves +- Facial tattoos (redkeje) +- **Ne rabijo vsi!** En NPC lahko ma samo 1 majhen tatu + +#### **Vejp/Cigareta** 💨 +- Post-apo stress relief +- Opcijsko za nekatere like + +#### **Brada** 🧔 +- Opcijsko +- Različni stili (dolga, kratka, kozja) + +--- + +## ✅ **PRIMERI KOMBINACIJ** + +### **Heavy Punk (kot Kai, Gronk)** +- Full dreadlocks (colored) +- Multiple piercings (3+) +- Ear gauges +- Neck/arm tattoos +- Vejp + +### **Medium Punk (kot Smetar)** +- Gray dreadlocks (partial ali full) +- 2-3 piercings +- Ear gauges +- Arm tattoos +- Vejp + brada + +### **Light Punk** +- 1 piercing (nos ali uho) +- 1 majhen tatu +- Normalni lasje ALI 6 dredloksov odzadi +- Brez gauges + +### **Minimal Punk** +- 1 piercing ALI 1 tatu +- Raztrgane obleke (post-apo) +- Preživelski look + +--- + +## 👗 **OBLAČILA - Post-Apokaliptična** + +**NI** sodobnih čistih oblačil! + +### **Acceptable:** +- Raztrgane/zakrpane obleke +- Tactical vests +- Fingerless gloves +- Heavy boots +- Layered clothing (survival) +- Weathered/dirty textures +- Makeshift armor pieces + +### **Avoid:** +- Čiste uniforme +- Sodobne poslovne obleke +- Fancy clean clothes +- Prevelika urejenost + +--- + +## 🎨 **CHARACTER VARIETY MATRIX** + +| NPC | Dreads | Piercings | Gauges | Tattoos | Vejp | Beard | Style | +|-----|--------|-----------|--------|---------|------|-------|-------| +| Kai | Full (pink/green) | Nose | Yes | Neck | No | No | Heavy | +| Gronk | Full (pink) | Nose | Yes | Arms | Yes | No | Heavy | +| Smetar | Gray (partial/full) | Nose+lip | Yes | Arms | Yes | Gray | Medium | +| Pek | No | 1 (ear) | No | 1 arm | No | Yes | Light | +| Tehnik | Partial (6 back) | 2 | No | Neck | No | No | Medium | +| Ivan Kovač | No | Lip | Yes | Sleeves | No | Yes | Medium | + +**Pattern:** Mešaj kombinacije, da je vsak lik unikaten, ni treba vseh featurjev na vsakem! + +--- + +## 📋 **IMPLEMENTATION CHECKLIST** + +Pri generiranju NPC-jev vedno pomisli: +- [ ] Ali ma piercing? (kateri tip?) +- [ ] Ali ma dredlokse? (full, partial, colored?) +- [ ] Ali ma ear gauges? +- [ ] Ali ma tatuje? (kje?) +- [ ] Ali vejpa/kadi? +- [ ] Ali ma brado? +- [ ] Obleke: raztrgane/post-apo? + +**NE vseh na enega lika!** Mix & match za variety. + +--- + +## 🎯 **GOLDEN RULE** + +> "Post-apokalipsa ni normalna – vsak lik mora kazati, da je preživelec. +> Tudi če je samo 1 piercing + raztrgane hlače, to je dovolj. +> NE clean corporate NPCs!" + +--- + +**Avtorji:** David Kotnik & Antigravity Agent +**Verzija:** 1.0 diff --git a/docs/FAZA1_GENERATION_STATUS.md b/docs/FAZA1_GENERATION_STATUS.md index 92c7fbd02..314e1a036 100644 --- a/docs/FAZA1_GENERATION_STATUS.md +++ b/docs/FAZA1_GENERATION_STATUS.md @@ -1,6 +1,6 @@ # 🎯 FAZA 1 & 2 - KICKSTARTER DEMO STATUS **Project:** Mrtva Dolina (DolinaSmrti) -**Last Updated:** 2026-01-05 13:29 CET +**Last Updated:** 2026-01-05 13:54 CET **Auto-Sync:** ✅ ACTIVE (updates on every successful commit) --- @@ -11,15 +11,15 @@ |----------|-------|----------|-------------|-------------|------------| | **References** | 24 | 24 | 0 | 0 | 100% ✅ | | **NPCs & Characters** | 14 | 14 | 0 | 0 | 100% ✅ | -| **Buildings** | 14 | 4 | 0 | 10 | 29% � | +| **Buildings** | 14 | 4 | 0 | 10 | 29% 🟡 | | **Tools & Items** | 4 | 4 | 0 | 0 | 100% ✅ | | **Crop Sprites** | 9 | 6 | 1 | 2 | 67% 🟡 | -| **Game Systems** | 19 | 3 | 0 | 16 | 16% 🔴 | -| **VFX & Juice** | 13 | 7 | 0 | 6 | 54% � | +| **Game Systems** | 19 | 6 | 0 | 13 | 32% � | +| **VFX & Juice** | 13 | 7 | 0 | 6 | 54% 🟡 | | **Quest System** | 16 | 12 | 0 | 4 | 75% 🟡 | | **Visual Processing** | 2 | 2 | 0 | 0 | 100% ✅ | -| **Audio** | 61 | 0 | 0 | 61 | 0% 🔴 | -| **TOTAL** | **176** | **76** | **1** | **99** | **43%** | +| **Audio** | 61 | 3 | 0 | 58 | 5% 🔴 | +| **TOTAL** | **176** | **82** | **1** | **93** | **47%** | --- diff --git a/references/npcs/glavni_smetar/master_reference.png b/references/npcs/glavni_smetar/master_reference.png new file mode 100644 index 000000000..cf327ffd5 Binary files /dev/null and b/references/npcs/glavni_smetar/master_reference.png differ diff --git a/src/dialogues/SmetarDialogues.js b/src/dialogues/SmetarDialogues.js new file mode 100644 index 000000000..d6f0bbb7f --- /dev/null +++ b/src/dialogues/SmetarDialogues.js @@ -0,0 +1,338 @@ +/** + * SMETAR DIALOGUE SYSTEM + * Interakcija z Zombijem Skavtom + */ + +export const SmetarDialogues = { + // Glavni intro dialog ko Kai pride s Zombijem + intro_with_zombie: { + trigger: 'player_near_smetar_with_zombie_scout', + participants: ['smetar', 'zombie_scout'], + lines: [ + { + speaker: 'smetar', + text: "Ej, Kai! Kaj mi spet furaš tega svojega s klobukom? Glej ga, nahrbtnik ima čisto svinjski od tistih vaših ruševin. Šutni ga sem, da mi malo pomaga s temi grafiti na obzidju!", + emotion: 'gruff', + animation: 'portrait_bounce', + soundLayers: ['broom_scraping', 'wind_ambient'], + typewriterSpeed: 40 // ms per character + }, + { + speaker: 'zombie_scout', + text: "*Tiho godrnjanje, rožljanje nahrbtnika* M-m-možgaaaaani... pa metla?", + emotion: 'confused_hungry', + animation: 'portrait_bounce_slow', + soundLayers: ['zombie_grumble', 'backpack_rattle', 'wind_ambient'], + typewriterSpeed: 50 + }, + { + speaker: 'smetar', + text: "Ja, metla! Če boš dobro pometal, ti mogoče dam kakšen star košček spomina, ki sem ga izbrskal iz smeti za tvojega šefa.", + emotion: 'encouraging', + animation: 'portrait_bounce', + soundLayers: ['broom_scraping', 'wind_ambient'], + typewriterSpeed: 40 + } + ], + outcomes: { + accept: 'quest_sanitation_zombie_help', + decline: 'smetar_disappointed' + } + }, + + // Quest assignment + quest_assignment: { + speaker: 'smetar', + text: "Pofafaj tole metlo pa mi pomagaj, al pa mi šutni tiste tri zombije, da naredimo mal reda v tej luknji!", + emotion: 'determined', + animation: 'portrait_bounce', + soundLayers: ['broom_tap', 'wind_ambient'], + typewriterSpeed: 40 + }, + + // Ko Kai posodi zombije + zombie_loan_accepted: { + speaker: 'smetar', + text: "Čist fajn! Dej, ti trije, pole sem! *postavi zombije v vrsto* Vi boste čistili ta grafit, vi tisto smejo, pa vi tam pa tisti razbit zabojnik!", + emotion: 'commanding', + animation: 'portrait_bounce_strong', + soundLayers: ['zombie_lineup', 'broom_distribution'], + typewriterSpeed: 45, + vfx: 'zombie_worker_spawn' + }, + + // Ko je delo končano + work_complete: { + speaker: 'smetar', + text: "Heh! Dobro delo, fantje! Kai, tvojim zombijem bi lahko dal za jest, pa tukaj je tisto za tebe...", + emotion: 'satisfied', + animation: 'portrait_bounce', + soundLayers: ['success_jingle', 'wind_ambient'], + typewriterSpeed: 40, + reward: { + type: 'rare_gift', + item: 'family_memory_fragment', + vfx: 'amnesia_blur_trigger' + } + }, + + // Ko najde redko darilo v smeteh + rare_gift_found: { + speaker: 'smetar', + text: "Ej Kai, poglej kaj sem najdu v teh ruševinah - stara družinska slika. Tvoja je, ne? Zgleda da... Kai? Kai! Hej, kam greš?!", + emotion: 'surprised', + animation: 'portrait_bounce', + soundLayers: ['paper_rustle', 'wind_ambient'], + typewriterSpeed: 35, + trigger_after: 'amnesia_blur_flashback' + }, + + // Ko mesto ni čisto + city_dirty_nag: { + speaker: 'smetar', + text: "Kai, no! Spet je mesto polno smeti! Kje so tvoji zombi? Rabim pomoč!", + emotion: 'frustrated', + animation: 'portrait_shake', + soundLayers: ['broom_angry_tap'], + typewriterSpeed: 40 + }, + + // Ko je mesto 100% čisto + city_perfect: { + speaker: 'smetar', + text: "KONČNO! Prvo mesto v tem post-apokaliptičnem sranju, ki je res čisto! Bravo, Kai. Evo, to si res zaslužil.", + emotion: 'proud', + animation: 'portrait_bounce_celebration', + soundLayers: ['fanfare', 'wind_ambient'], + typewriterSpeed: 40, + reward: { + type: 'legendary_gift', + item: 'kai_childhood_photo', + vfx: 'amnesia_blur_major' + } + }, + + // Casual banter + casual_greeting: [ + { + text: "Ej, pometal sem že 200 ruševin. Tvoj zombi? Kaj, 5?", + emotion: 'teasing', + animation: 'portrait_bounce' + }, + { + text: "Vejpam že od preden so zombiji začeli hodit. Ta dim jih odbija, verjameš?", + emotion: 'casual', + animation: 'portrait_bounce' + }, + { + text: "Ko sem bil mlajši, sem bil bolj punk od tebe. Dreadi do tal!", + emotion: 'nostalgic', + animation: 'portrait_bounce' + } + ] +}; + +/** + * DIALOGUE ANIMATION CONTROLLER + */ +export class SmetarDialogueController { + constructor(scene) { + this.scene = scene; + this.cinematicVoice = scene.cinematicVoice; // CinematicVoiceSystem + this.currentDialogue = null; + } + + /** + * Play dialogue sequence with animations + */ + async playDialogue(dialogueKey) { + const dialogue = SmetarDialogues[dialogueKey]; + if (!dialogue) return; + + this.currentDialogue = dialogue; + + // Single line dialogue + if (dialogue.speaker) { + await this.playLine(dialogue); + } + // Multi-line sequence + else if (dialogue.lines) { + for (const line of dialogue.lines) { + await this.playLine(line); + await this.wait(500); // Brief pause between lines + } + } + + // Handle outcomes/rewards + if (dialogue.reward) { + this.awardReward(dialogue.reward); + } + + if (dialogue.trigger_after) { + this.scene.events.emit(dialogue.trigger_after); + } + } + + /** + * Play single dialogue line with full effects + */ + async playLine(line) { + const speaker = line.speaker; + + // Show portrait with bounce animation + this.showPortrait(speaker, line.animation); + + // Start sound layers + this.startSoundLayers(line.soundLayers); + + // Create dialogue box + const dialogueBox = this.createDialogueBox(speaker, line.text); + + // Typewriter effect with voice sync + await this.typewriterEffect(dialogueBox, line.text, line.typewriterSpeed); + + // Voice synthesis + if (this.cinematicVoice) { + await this.cinematicVoice.speak( + line.text, + speaker, + line.emotion, + { + typewriterElement: dialogueBox.textElement, + ambient: line.soundLayers.includes('wind_ambient') ? 'wind_ruins' : null + } + ); + } + + // VFX if specified + if (line.vfx) { + this.scene.events.emit('vfx:trigger', line.vfx); + } + + // Wait for player to dismiss + await this.waitForDismiss(); + + // Hide dialogue + this.hideDialogue(dialogueBox); + this.hidePortrait(speaker); + } + + /** + * Portrait bounce animation (Style 32) + */ + showPortrait(speaker, animationType) { + const portrait = this.scene.add.sprite(150, 500, `portrait_${speaker}`); + portrait.setDepth(100); + portrait.setAlpha(0); + + // Fade in + this.scene.tweens.add({ + targets: portrait, + alpha: 1, + duration: 300, + ease: 'Sine.easeIn' + }); + + // Bounce animation + if (animationType === 'portrait_bounce') { + this.scene.tweens.add({ + targets: portrait, + y: '+=10', + duration: 600, + yoyo: true, + repeat: -1, + ease: 'Sine.easeInOut' + }); + } else if (animationType === 'portrait_bounce_slow') { + this.scene.tweens.add({ + targets: portrait, + y: '+=15', + duration: 1000, + yoyo: true, + repeat: -1, + ease: 'Sine.easeInOut' + }); + } + + this.currentPortrait = portrait; + } + + hidePortrait(speaker) { + if (this.currentPortrait) { + this.scene.tweens.add({ + targets: this.currentPortrait, + alpha: 0, + duration: 300, + onComplete: () => this.currentPortrait.destroy() + }); + } + } + + /** + * Start ambient sound layers + */ + startSoundLayers(layers) { + if (!layers) return; + + layers.forEach(soundKey => { + if (this.scene.sound && this.scene.sound.get(soundKey)) { + this.scene.sound.play(soundKey, { volume: 0.3, loop: soundKey.includes('ambient') }); + } + }); + } + + /** + * Typewriter effect + */ + async typewriterEffect(dialogueBox, text, speed) { + const textElement = dialogueBox.textElement; + textElement.setText(''); + + for (let i = 0; i < text.length; i++) { + textElement.setText(text.substring(0, i + 1)); + await this.wait(speed); + } + } + + createDialogueBox(speaker, text) { + // Placeholder - real implementation would create Phaser graphics + return { + textElement: this.scene.add.text(200, 550, '', { + fontSize: '18px', + color: '#FFFFFF', + wordWrap: { width: 600 } + }).setDepth(101) + }; + } + + hideDialogue(dialogueBox) { + if (dialogueBox && dialogueBox.textElement) { + dialogueBox.textElement.destroy(); + } + } + + awardReward(reward) { + if (reward.type === 'rare_gift' || reward.type === 'legendary_gift') { + this.scene.zombieEconomy.awardRareGift(reward.item); + } + + if (reward.vfx) { + this.scene.events.emit('vfx:trigger', reward.vfx); + } + } + + wait(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + async waitForDismiss() { + // Wait for spacebar or click + return new Promise(resolve => { + const handler = () => { + this.scene.input.keyboard.off('keydown-SPACE', handler); + resolve(); + }; + this.scene.input.keyboard.on('keydown-SPACE', handler); + }); + } +} diff --git a/src/systems/ZombieEconomySystem.js b/src/systems/ZombieEconomySystem.js new file mode 100644 index 000000000..9d9316bd9 --- /dev/null +++ b/src/systems/ZombieEconomySystem.js @@ -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()); + } +}