Added Glavni Smetar (post-apocalyptic punk janitor), Character Design Guidelines (piercings/dreads/tattoos variety), Smetar dialogue system with Zombie Scout interaction, portrait animations, sound layers, typewriter sync. Updated FAZA1 status to 47%. Includes ZombieEconomySystem with Worker Zombies, Sanitation, Contracts, Rare Gifts, Brain feeding.
This commit is contained in:
139
docs/CHARACTER_DESIGN_GUIDELINES.md
Normal file
139
docs/CHARACTER_DESIGN_GUIDELINES.md
Normal file
@@ -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
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# 🎯 FAZA 1 & 2 - KICKSTARTER DEMO STATUS
|
# 🎯 FAZA 1 & 2 - KICKSTARTER DEMO STATUS
|
||||||
**Project:** Mrtva Dolina (DolinaSmrti)
|
**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)
|
**Auto-Sync:** ✅ ACTIVE (updates on every successful commit)
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -11,15 +11,15 @@
|
|||||||
|----------|-------|----------|-------------|-------------|------------|
|
|----------|-------|----------|-------------|-------------|------------|
|
||||||
| **References** | 24 | 24 | 0 | 0 | 100% ✅ |
|
| **References** | 24 | 24 | 0 | 0 | 100% ✅ |
|
||||||
| **NPCs & Characters** | 14 | 14 | 0 | 0 | 100% ✅ |
|
| **NPCs & Characters** | 14 | 14 | 0 | 0 | 100% ✅ |
|
||||||
| **Buildings** | 14 | 4 | 0 | 10 | 29% <EFBFBD> |
|
| **Buildings** | 14 | 4 | 0 | 10 | 29% 🟡 |
|
||||||
| **Tools & Items** | 4 | 4 | 0 | 0 | 100% ✅ |
|
| **Tools & Items** | 4 | 4 | 0 | 0 | 100% ✅ |
|
||||||
| **Crop Sprites** | 9 | 6 | 1 | 2 | 67% 🟡 |
|
| **Crop Sprites** | 9 | 6 | 1 | 2 | 67% 🟡 |
|
||||||
| **Game Systems** | 19 | 3 | 0 | 16 | 16% 🔴 |
|
| **Game Systems** | 19 | 6 | 0 | 13 | 32% <EFBFBD> |
|
||||||
| **VFX & Juice** | 13 | 7 | 0 | 6 | 54% <EFBFBD> |
|
| **VFX & Juice** | 13 | 7 | 0 | 6 | 54% 🟡 |
|
||||||
| **Quest System** | 16 | 12 | 0 | 4 | 75% 🟡 |
|
| **Quest System** | 16 | 12 | 0 | 4 | 75% 🟡 |
|
||||||
| **Visual Processing** | 2 | 2 | 0 | 0 | 100% ✅ |
|
| **Visual Processing** | 2 | 2 | 0 | 0 | 100% ✅ |
|
||||||
| **Audio** | 61 | 0 | 0 | 61 | 0% 🔴 |
|
| **Audio** | 61 | 3 | 0 | 58 | 5% 🔴 |
|
||||||
| **TOTAL** | **176** | **76** | **1** | **99** | **43%** |
|
| **TOTAL** | **176** | **82** | **1** | **93** | **47%** |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
BIN
references/npcs/glavni_smetar/master_reference.png
Normal file
BIN
references/npcs/glavni_smetar/master_reference.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 668 KiB |
338
src/dialogues/SmetarDialogues.js
Normal file
338
src/dialogues/SmetarDialogues.js
Normal file
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
569
src/systems/ZombieEconomySystem.js
Normal file
569
src/systems/ZombieEconomySystem.js
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user