🔥 FAZE SPRINT 1-3 COMPLETION: DayNight, Inventory, Core Farming System, Mrtvi Zombiji (ShamblerAI & Alfa Moć Taming), Sredili assets & daily UI

This commit is contained in:
2026-03-03 17:46:41 +01:00
parent 35153eeacf
commit 188bd769cf
49 changed files with 5054 additions and 286 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 648 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 565 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 425 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

BIN
nova farma TRAE/assets/DEMO_FAZA1/Trees/drevo_faza_2.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 61 KiB

BIN
nova farma TRAE/assets/DEMO_FAZA1/Trees/drevo_veliko.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 KiB

View File

@@ -0,0 +1,42 @@
# ⏱️ DEVLOG IN DNEVNIK DELA
**Datum:** 3. Marec 2026
**Zadeva:** Nova Farma Demo - Sprint 1, 2, 3 in Asseti (Zaključek dneva)
**Status:** ✅ FAZA DEMO 65% COMPLETION
## 📝 Povzetek današnjega dela
Danes smo opravili masiven premik naprej v razvoju "Nova Farma" DEMO verzije, pri čemer smo v celoti zaključili prve tri sprinte od petih, ki so potrebni za izdajo Demo verzije igre.
### 🎯 Kaj je bilo narejeno:
**✅ SPRINT 1 — "Osnove Preživetja" (100% DONE)**
- Dodan `DayNightSystem.js` (ambientna svetloba, zvezde na nočnem nebu).
- Implementirano spanje (tipka `[Z]` ob postelji/vreči za preskok noči do 06:00 zjutraj).
- Čas in dnevi sedaj dejansko tečejo in se avtomatsko sinhronizirajo z `UIScene`.
- Ustvarjen `InventorySystem.js` s funkcionalnim 7-slot hotbarom (tipke `[1]-[7]` ali kolešček na miški).
- Dodeljeni začetni starter itemi (Motika, Zalivalka, Semena: Pšenica in Korenje).
**✅ SPRINT 2 — "Kmetovanje" (100% DONE)**
- Refaktoriran `FarmingSystem.js` za uporabo Phaser Graphics za natančne 48x24px pikselske tilled soil (poorana tla) ploščice brez popačenj.
- Urejeno sajenje semen iz inventarja.
- Urejen `WaterSystem.js` z zbiralnikom deževnice (Rain Catcher), kjer Kai zajema vodo (tipka `[F]`) in zaliva sode `[E]`.
- Implementirana vizualna metrika rasti in odvisnost rasti rastlin od časa (`onNewDay`) in vode.
- Razrešeni ERR_FILE_NOT_FOUND preloading manjki glede dreves in uveljavljen pixel-perfect z-index sistem rastlinstva.
**✅ SPRINT 3 — "Zombi AI Sistem" (100% DONE)**
- Generiran nov `zombie_shambler_walk_sheet.png` preko AI v Dark Chibi Gothic stilu (4 smeri x 4 frames).
- Skripta je samodejno odstranila belo ozadje in pripravila sheet za engine.
- Skripta je uspešno skopirala že obstoječe in narejene dobre zombije iz starega repozitorija v trenutnega (`assets/DEMO_FAZA1/Characters`).
- Narejen modul `ZombieSystem.js`. Zombiji sedaj prespawnajo ponoči na robovih otoka.
- Zombi AI logika urejena: Wandering (naključno), Chasing (zasleduje Kaia).
- Dodan sistem **Alfa Moč**: ko se igralec približa nezavestnemu zombiju in drži presledek (`[SPACE]`), vizualna "progress bar" koda (koordinate, taming bar) igralcu prepusti udomačenega zombija z zlatim vizualnim partikli efektom.
### 📦 Status Datotek in Sistemski Pregled:
Posodobili smo `STATUS_DEMO_FAZA1_2026_03_03.md`, ki sedaj kaže na uradnih 65% preboja k izidu samega dema.
## 🚀 Naslednji koraki (za naslednjo sejo):
- **Sprint 4 (Zgodba):** 20 polaroidnih story slik z AI, uvoz in vzpostavitev `IntroScene.js`. Dodatek "Amnesia" glitches na ekran ob začetku.
- **Sprint 5 (Poliranje):** Generacija FX soundboarda - dež (web audio noise), stopinje in ambientni zvoki. Postavitev Demo menija in ureditve za Steam/Kickstarter predogled.
- **Dialog System:** Typewriter interakcija z NPC-jem (Gronk) za mali tutorial v igri.
Dnevnik končan in sistem prepuščen v roke git repozitorija.

View File

@@ -0,0 +1,404 @@
# 🎮 NOVA FARMA — KOMPLETNA STRUKTURA FAZ
## Prompt za Gemini | Avtor: David Kotnik | 2026-03-03
---
## ⚡ KAJ JE IGRA (Hitri opis)
**Nova Farma (Krvava Žetev)** je dark survival farming RPG.
- **Protagonist:** Kai Marković, 14 let, Alpha Hybrid — ugriznjen od zombija, a ni postal eden. Dobi moč kontrolirati zombije.
- **Cilj:** Preživi, zgradi kmetijo, obnovi civilizacijo, najdi ugrabljeno sestro Ano.
- **Svet:** Post-apokaliptična Slovenija, leto 2084. Začneš na malem otoku, nato odkrivaš celinski svet.
- **Styl:** 2.5D Dark Chibi Noir. Hand-drawn ilustracija (NI pixel art). Gothic dark barve.
- **Platform:** Desktop (Electron + Phaser 3)
- **Inspiracija:** Stardew Valley × The Last of Us × Don't Starve × My Time at Portia
---
## 🗺️ STRUKTURA SVET
```
[DEMO] Kmetijski Otok (8×8 area)
↓ po 200 dneh
[FAZA 1] Celoten Otok odkrit (Fog of War se dvigne)
↓ po nakupu igre
[FAZA 2+] Celinska Mapa: 27 Razrušenih Mest + 20 Biome-ov
↓ endgame
[FAZA 3+] Černobil, Ana reši, Finalni konec
```
**Kmetijski Otok** = tvoja stalna baza/dom, nikoli ga ne zapustiš. Ampak v Fazi 2+ greš na celino in obnavljaš mesta.
---
## 📅 KOMPLETNIH 10 FAZ — TOČNA VSEBINA
---
### 🆓 DEMO — "The Hook" (Brezplačno)
**Cilj:** Preživi 200 dni → Achievement "Trmasti Preživeli" → Odkleni Fazo 1
**Svet:** Samo Farma (8×8 tilov) na kmetijskem otoku. Vse ostalo je Fog of War.
**Vsebina:**
- **Bivanje:** Spalna Vreča → Šotor (Hiša zaklenjena)
- **Orodja:** Samo Lesena (Tier 1)
- **Rastline:** Pšenica (3 dni), Korenje (4 dni), **Konoplja (7 dni — GLAVNI ZASLUŽEK!)**
- **Zombiji:** Max 3 Shamblerji. Samo sledijo, ne delajo. [SPACE] = Alfa Moč = Udomačiš.
- **Voda:** VSA voda je strupena. Edini vir = Rain Catcher (lovilec deževnice)
- **Zgodba:** 20 Polaroid intro, Twin Bond signali od Ane (1×/dan, 10% chance), 3 Flashback spomini
**Economy:** Konoplja = 200g/bud. Pšenica = 15g, Korenje = 40g. Brez konoplje ne preživiš.
**Save file se prenese v polno igro!**
---
### 💰 FAZA 1 — "Early Access: Odprtje Sveta" (Nakup igre)
**Cilj:** Razišči celoten otok, upravljaj Zombi Workforce, popravi Ruševine
**Svet:** CELOTEN OTOK odkrit (Fog of War se dvigne). Izvidnik Zombi gre v meglo in odkriva svet.
**Vsebina:**
**🧟 Zombi Workforce (5 tipov):**
| Tip | Naloga | Max |
|-----|--------|-----|
| 🕵️ Scout (Izvidnik) | Gre v Fog, odkriva mapo, prinaša material | 1 |
| 🌾 Farmer (Kmet) | Zaliva, žanje | 3 |
| 🪓 Lumberjack (Gozdar) | Seka drevesa | 2 |
| ⛏️ Miner (Rudar) | Koplje rudo, Lv5+ najde Železo | 2 |
| 📦 Porter (Nosač) | Pobira Drop Boxe → v skrinje | 1 |
**Drop Box sistem:** Zombiji ne nosijo v tvoje skrinje direktno. Odlagajo v Drop Box → ti pobereš.
**🏚️ Ruševine (popravi jih!):**
- Garaža → Crafting hub (Motorka, Laksarca, Vodni Filter, Stiskalnica)
- Rastlinjak → Gobe (indoor), zimske rastline
- Hlev → Krave, Gronk, Susi
**🏠 Bivanje:** Lepena Koča (Faza 1 MAX)
**⛏️ Rudnik:** Baker, Premog, Glina, Železo (samo Rudar Lv5+!)
**🌱 Nove rastline:** 80+ vrst. Sončnice → Bio-olje. Konoplja full unlock. Gobe (Klet).
**🍄 Skrita Klet:**
- Vhod skrit (za omaro / pod preprogo)
- Grow Room: UV luči, Hidroponika, Police za Gobe
- The Lab: Predelava → Mind Expansion napitki
**🐔 Živali:** Kure (Kokošnjak), Krave (Hlev) — dostava v škatli
**👻 Oltar:** Zgradi Oltar + najdi Inženirsko uro (Marko) + Medaljon (Elena) → Duhova Starša se pojavita kot Force Ghost, dajata nasvete in permanentne buffe
**🧟 Gronk:** (Demo Locked — izjema: prvih 20 kupcev + Streamerji takoj!)
- Trol z roza dreadlocki, vape v ustih, ima psičko Susi (Jazbečarko)
- Mentor, govori v 3. osebi: "Gronk... pomaga šefu?"
---
### 🏛️ FAZA 2 — "Town Restoration & Museum"
**Cilj:** Razišči celinski svet, obnovi 27 razrušenih mest, odpri Muzej
**Svet:** Celinska Mapa (prej vidna samo kmetija/otok). 27 Razrušenih Mest razkropljenih po mapi.
**🏚️ Town Restoration Loop:**
```
1. Explore Celino → Najdi Ruined Town
2. Poberi material (Les, Kamen, Železo) + čas (zombiji pomagajo)
3. Obnovi stavbe:
- Kovačnica → Ivan (Kovač) se vseli
- Pekarna → Marija (Pekarka) se vseli
- Klinika → Dr. Chen se vseli
...in tako naprej
4. Posodi Zombi Delavce NPCjem → dobijo pomoč
5. NPC-ji ti dajejo nagrade + zgodba clues o Ani
```
**27 Mest × 5-15 hiš = 300+ stavb za obnovo**
**180 NPC-jev skupaj čez vsa mesta**
**🏛️ Muzej:**
- Vodi ga Kustos (NPC)
- Doniraš redke predmete (Artifakti, Legendarne Ribe, Zlatega Hroščca...)
- Nagrada: Sova prinaša darila podnevi, Netopir ponoči
**🎣 Ribnik (domači):**
- Prej samo divje ribarjenje (ocean)
- Zdaj gradiš ribnik na kmetiji
- Vzreja rib za hrano in prodajo
**🔧 Napredni Stroji (iz Garaže naprej):**
- Avtomatska Zalivalka (Solar powered)
- Bio-Rafinerija (predelava odpadkov v gorivo)
- Konzervarna (hrana za dolgo)
---
### ⚡ FAZA 3 — "Elektrika & Avtomatizacija"
**Cilj:** Električno omrežje, avtomatska pridelava
**Vsebina:**
- **Power Grid:** Generator → Električni vodi → Naprave
- **Avtomatizacija:** Kombajn, Robotski kmet, Avtomatski packer
- **Energija:** Solar paneli, Vodni generator (ob reki)
- **Upgrade:** Vse prejšnje zgradbe dobijo električno varjanto (2× hitrost)
---
### 🌀 FAZA 4 — "Portali & Biomi (Tier 1)"
**Cilj:** Odpri 9 Normalnih Biome-ov prek portalov
**9 Normalni Biomi:**
1. 🌾 Grassland — Travnik (Spawn area)
2. 🌲 Forest — Gozd (les, volkovi, jeleni)
3. 🌿 Swamp — Močvirje (strupeno)
4. 🏜️ Desert — Puščava (vročina, škorpijoni)
5. 🏔️ Mountain — Gora (rudnik, orli)
6. ❄️ Snow — Sneg (mraz, led medvedi)
7. 🏚️ Wasteland — Puščoba (ruševine, mutanti)
8. 🌴 Tropical — Tropski (plaža, kokoši)
9. ☢️ Radioactive — Radioaktivno (mutacije, sij)
**Za vsak biom:** Edinstvene rastline, živali, materiali, Boss, NPCji, Romance opcija
---
### 🐉 FAZA 5 — "Anomalni Biomi (Tier 2)"
**Cilj:** Odpri 11 Posebnih/Anomalnih Biome-ov
**11 Anomalni Biomi:**
10. 🦖 Dino Valley — T-Rex, raptorji, dinozavri
11. 🐉 Mythical Highlands — Zmaji, Griffini, Enorogovi
12. 🌲 Endless Forest — Bigfoot, Wendigo, kriptidi
13. 🦕 Loch Ness — Nessie, Škotska, gradu
14. 💀 Catacombs — Skeletne armade, duhovi, Nekromancija
15. 🏺 Egyptian Desert — Piramide, Mumije, Sfinga
16. 🌴 Amazon Rainforest — Piranhe, Anaconde, Pleme
17. 🌊 Atlantis — Podvodni svet, Morske deklice, Podmornica
18. ☢️ Chernobyl — FINALNA ZONA, Ana je tu!
19. 🇲🇽 Mexican Cenotes — Aksolotli, Majevske ruševine
20. 🧙‍♀️ Witch Forest — Baba Jaga, Čarovnice, Magija
**Za Portal Unlock:** Vsak biom ima edinstven pogoj (npr. Atlantis = popravi potapljačko opremo + 7 kristalov)
---
### 💍 FAZA 6 — "Romanca, Otroci & Generacije"
**Cilj:** Vzpostavi družino, generacijsko igranje
**12 Romance Opcij:**
- Lena (Kmetova hčerka), Katarina (Trgovka), Sonya (Zdravnikova asistentka)...
- Eksotičnih 7: Plemenita Princesa (Amazon), Morska Deklica (Atlantis), Valkira (Mythical), Egipčanska Svečenica, Škotska Deklica, Čuvajka Dinozavrov, Duh Deklica (Catacombs)
**Otroci — 5 stopenj rasti:**
1. Dojenček (0-1) → Zibelka, skrb
2. Malček (1-3) → Hodi, igra
3. Otrok (3-10) → Pomaga na farmi!
4. Najstnik (10-18) → Polno delo
5. Odrasel (18+) → **Postane Playable!**
**Generacije:** Kai → otroci → vnuki → ... 100+ realnih let možno
---
### 🏗️ FAZA 7 — "Mega Razvoj Mest"
**Cilj:** Nadgradi vsa 27 mest v polno mesti
**Town Upgrade System:**
```
Ruined → Repaired → Developed → Thriving → City
(Faza 2) (Faza 7) (Endgame)
```
**Mega Projekti:**
- Elektrarna za celotno mesto
- Šola (uči otroke, +skills)
- Bolnišnica (zdravi NPC-je in Kai)
- Trg (teden tržnica, 50+ prodajalcev)
- Stara Dvorana (NPC odloča z Kai-em skupaj)
---
### 🔬 FAZA 8 — "Research & Advanced Tech"
**Cilj:** Napredna tehnologija + Ana's Science System
**Ana (ko je rešena v Fazi 8):**
- Odpre Laboratorij na kmetiji
- Research tree: Bio-tech, Radiation tech, Nano-tech
- Crops 2.0 (genetsko modificirane, 2× yield)
- Zombie Evolution (zombiji dobijo upgrade)
**Skrita Klet FULL UPGRADE:**
- Industrijska Gojilnica (200× kapaciteta)
- Kemični Laboratorij (kompleksne formule)
- Tveganje: Policija/Župan kaznuje!
---
### ☢️ FAZA 9 — "Chernobyl & The Rescue"
**Cilj:** Prodri v Černobil, reši Ano
**Zgodba:**
1. Kai najde vse 9 Key Fragmentov (iz biome bosšev)
2. Portal do Černobila se odklene
3. Fight through: Radiation Zones + Zmaj Volk boss (emotionalen)
4. Giant Troll King Phase 1 (HP: 2500) — premagan, beži
5. **ANA REŠENA** 💜
**The Reunion:**
```
Kai enters Ana's cell.
Ana turns around.
ANA: "...Kai?"
KAI: "Ana... I found you."
[They embrace, crying]
ANA: "You came for me..."
KAI: "Always. Twin Bond, remember?"
[Twin Bond FULLY AWAKENS — zlatem svit!]
```
**Ana postane Playable!** Unlock vseh 6 Twin Bond abilityjev.
---
### 🌟 FAZA 10 — "Final Choice & 4 Endings" (ENDGAME)
**Cilj:** Premagi finale, izberi konec
**Dr. Krnić Ultimatum:** *"Pridi v Reactor Core. Ali spustim SUPER VIRUS. 7 dni."*
**Boss Rush:**
1. Dr. Krnić (HP: 1500)
2. Giant Troll King Phase 2 (HP: 5000 — Ultimate Form!)
**CURE MACHINE:**
```
Dr. Krnić (dying): "Edini način zdravila = Alpha Hybrid žrtev."
```
**4 KONCI — Player Choice:**
| Konec | Pogoj | Kaj se zgodi |
|-------|-------|--------------|
| 💔 Kai se žrtvuje | Pritisni A | Kai umre, Ana vodi novi svet, hčer imenuje "Kai" |
| 💔 Ana se žrtvuje | Pritisni B | Ana umre, Kai vodi novi svet, hčer imenuje "Ana" |
| 💀 Dark World | Pritisni C | Oba zavrneta. Živita skupaj večno, zombiji ostanejo. |
| 🌟 PERFECT ✨ | Vse 50 Aninih Clues + 9 Key Fragmentov | Ana izračuna alternativo — oba preživita! |
**Perfect Ending zadnja scena:** Piknik na kmetiji. Kai, Ana, partnerji, 6 otrok, smeh.
```
KAI: "We did it."
ANA: "Together. Always."
[Children laughing in background]
```
---
## 👥 KOMPLETNI LIKI
| Lik | Starost | Vloga | Videz | Kdaj |
|-----|---------|-------|-------|------|
| **Kai Marković** | 14 | Protagonist, Alpha Hybrid | Pink/zeleni dreadlocki, rdeče oči, gaugi, nos ring, hoodie, cunje | Demo+ |
| **Ana Marković** | 14 | Ugrabljena sestra | Kratki temni lasje, bela lab majica, pametna, urejena | Faza 9 (rešena) |
| **Marko Marković** | ~45 | Mrtev oče → Duh | Sivi lasje, brada, halatna | Faza 1 (Oltar) |
| **Elena Marković** | ~43 | Mrtva mama → Duh | Dolgi temni lasje, topel nasmeh | Faza 1 (Oltar) |
| **Gronk** | ? | Trol pomočnik | Ogromen, roza dreadlocki, vape, Susi psička | Faza 1 |
| **Dr. Krnić** | ~60 | Zlobnež | Mad scientist, halatna, divji pogled | Faza 9-10 |
| **Giant Troll King** | ? | Ugrabil Ano | 3× Kai, oklopljen, rdeče oči | Faza 9-10 |
---
## 🎨 VIZUALNI STIL — PRAVILA (NIKOLI KRŠI)
1. **NI PIXEL ART** — High-res smooth ilustracija
2. **Thick black outlines** (~3-5px) na VSEH likih
3. **Gothic Dark Chibi** — veliki glave, majhna telesa, ekspresivne oči
4. **Muted gothic palette:** Temne modre, vijolične, rjave + neon accenti (teal, zelena, vijolična)
5. **Transparent ozadje** (Alpha PNG) za VSE like in objekte
6. **Kai DNA — NIKOLI NE MENJAJ:** Pink/zeleni dreadlocki, rdeče oči, gaugi, nos ring — to so konstante skozi VSO igro in VSE starosti
---
## 📊 ŠTEVILKE
| | |
|-|-|
| **Faz skupaj** | DEMO + 10 FAZ |
| **Biome-ov** | 20 (9 Normal + 11 Anomalous) |
| **Boss-ov** | 24 (mini-boss/biome + Finalni) |
| **Mest** | 27 (po celinski mapi) |
| **NPC-jev** | 180 (čez vsa mesta) |
| **Romance opcij** | 12 |
| **Stopenj rasti otrok** | 5 |
| **Koncev** | 4 |
| **Zombi worker tipov** | 5 |
| **Rastlin** | 80+ |
---
## 💬 KLJUČNI DIALOGJI (Kopiraj direktno)
**Twin Bond signali (Ana, šibki):**
```
"Kai... čutiš me? Sem ujeta... ampak ŽIVIM. Ne obupaj!"
"Troll me čuva... ampak nisem sama..."
"Kai... najdi me. Twin Bond... deluje..."
```
**Gronk:**
```
"Gronk... pomaga šefu?"
"Gronk... dober zombi?"
"Gronk... šef... žalosten? Gronk pomagal... najti sestro!"
```
**Marko (Duh):**
```
"Kai... Sin... Utri ograjo. Zombiji so šibki, ampak horda..."
"Skupaj sva verjela v boljši svet. Ne nehaj verjeti."
```
**Elena (Duh):**
```
"Rastline potrebujejo vodo, Kai. Kot ti ljubezen."
"Nisi kriv. Nikoli nisi bil kriv."
```
**The Reunion (Faza 9):**
```
ANA: "...Kai?"
KAI: "Ana... I found you."
ANA: "You came for me..."
KAI: "Always. Twin Bond, remember?"
ANA: "I felt you. Every day. Searching..."
[Twin Bond fully awakens — zlatem svit]
```
**Kai dnevnik:**
```
Dan 1: "Končno... Nov dom. Majhna kmetija, ampak dovolj za začetek."
Dan 30: "En mesec. Preživim. Ampak Ana... kje si?"
Dan 100: "Sto dni. Pol poti. Držim se."
Dan 200: "Dvesto dni. Zdaj se začne pravi boj."
```
---
*"Twin Bond. Zdaj in vedno."* 💜
*© 2026 HipoDevil666 | Nova Farma / Krvava Žetev*
*Posodobljeno: 2026-03-03 ob 14:54*

View File

@@ -0,0 +1,448 @@
# 🎮 NOVA FARMA (KRVAVA ŽETEV) — KOMPLETNI MASTER BRIEF
## Za: Gemini / AI asistenta | Datum: 2026-03-03
## Avtor: David Kotnik (HipoDevil666)
---
## ⚡ TL;DR (Preberi najprej!)
**Nova Farma** je dark survival farming RPG z zombie kontrolo, postavljeno v post-apokaliptično Slovenijo leta 2084. Igraš **Kai Markovića** (14 let), ki je edini preživeli alfa hibrid — ugriznjen od zombija, a ne postane eden. Namesto tega dobi moč **kontrolirati zombije**. Cilj: preživi, zgradi kmetijo, najdi ugrabljeno sestro Ano.
**Vizualni stil:** 2.5D Dark Chibi Noir — Hand-drawn ilustracija, NI pixel art. Clean lines, thick outlines, muted gothic barve.
**Platform:** Desktop (Electron + Phaser 3)
**Inspired by:** Stardew Valley × The Last of Us × Don't Starve × My Time at Portia
---
## 🌍 SVET
- **Leto:** 2084
- **Lokacija:** Slovenija, majhen skriti otok v dolini
- **Svet:** Otok (6400×6400px) sredi oceana (10400×10400px)
- **Atmosfera:** Post-apokalipsa. Civilizacija je padla. Narava se vrača. Zombiji povsod.
- **Vreme:** Dež (edini vir čiste vode!), sončno, nevihta — dinamično
---
## 👥 LIKI (KOMPLET)
### 🧒 KAI MARKOVIĆ — Protagonist
- **Starost:** 14 let
- **Spol:** Moški
- **Videz:** Pink/zeleni dreadlocki, rdeče oči (genetska lastnost Marković linije), gaugi v ušesih, nos ring, majhni vampirski zobki. Hoodie, raztrgane kavbojke, cunje. Ruksak. Drži magično knjigo v eni roki.
- **Osebnost:** Tih, določen, redkobeseden — ko govori, je globoko. Ne joče pred drugimi. Skrbi se za druge bolj kot zase.
- **Posebnosti:**
- **Alpha Hybrid:** Ugriznjen a ni postal zombi. Imun.
- **Zombie Control (Alfa Moč):** [SPACE] → kontrolira zombije
- **Twin Bond:** Telepatija z Ano (čuti jo, dobiva šibke signale)
### 👧 ANA MARKOVIĆ — Sestra (UGRABLJENA)
- **Starost:** 14 let (Kai-eva dvojčica)
- **Status:** Ugrabljena od Giant Troll Kinga, zaprta v Černobilu
- **V igri:** Samo kot glas (Twin Bond signali) v Demo + Faza 1
- **Karakter:** Genialna, radovedna, optimistična. Nasprotje Kai-a.
- **Posebnost:** Njena genijskost bo rešila svet (alternativni konec)
### 👨‍🔬 MARKO MARKOVIĆ — Oče (MRTEV, Dan 3)
- **Videz:** Sredovečni znanstvenik s sivo-rjavimi lasmi, brada
- **Status:** Umrl od zombie ugrizov Dan 3, brani družino
- **V igri:** Duh (Force Ghost) — pojavi se ko Kai zgradi Oltar + najde Inženirsko uro
- **Vloga duha:** Inženirski nasveti, buff: +15% Craft hitrost
### 👩‍🔬 ELENA MARKOVIĆ — Mama (MRTVA, Dan 3)
- **Videz:** Visoka ženska, dolgi temni lasje, topel nasmeh
- **Status:** Umrla isti dan kot Marko
- **V igri:** Duh — pojavi se ko Kai najde Medaljon
- **Vloga duha:** Kmetijski nasveti, buff: +10% Energy regen na kmetiji
### 🧟 GRONK — Prvi zombi pomočnik (FAZA 1)
- **Tip:** Trol (udomačen velikan, ne navaden zombi)
- **Videz:** Ogromen, roza dreadlocki, vape v ustih (permanent). Vedno chill.
- **Osebnost:** "Chill stric". Govori počasi, v 3. osebi. Srčen pod grdo zunanjostjo.
- **Primer govora:** "Gronk... pomaga šefu? Gronk... dober zombi?"
- **Unlock:** Faza 1 (Demo: Locked — razen prvih 20 kupcev + streamerji)
- **Ima psa:** Susi (Jazbečarka) — spi pri njegovih nogah
### 👨‍🔬 DR. KRNIĆ — Glavni zlobnež
- **Starost:** ~60 let
- **Videz:** Mad scientist aesthetics — halatna, redki lasje, divji pogled
- **Dejanje:** NAMENOMA sprostil zombie virus iz Black Serpent Laboratory
- **Cilj:** Kontrola nad preostalim svetom
- **V igri:** Faza 2+, finalni boss fight (skupaj z Giant Troll Kingom)
### 🦍 GIANT TROLL KING — Ugrabil Ano
- **Videz:** Ogromen trol, 3× večji od Kai-a, oklopljen, rdeče oči
- **Stats:** HP 5000, seizmični napadi, kliče zombije, ognjevit dih
- **Lokacija:** Černobilski Reaktor (Faza 2+)
- **V Demu/Fazi 1:** Samo omenjeni
---
## 🎬 ZGODBA — AKT PO AKT
---
### 📼 PROLOG: DAN 030 (Pred igro — Intro sekvenca)
Prikaže se samo enkrat, ko prvič zagneš igro:
**20 Polaroid fotografij z typewriter komentarji:**
| # | Slika | Komentar |
|---|-------|----------|
| 1 | Srečna družina pred virusom | "Pred virusom... bili smo srečni." |
| 2 | Marko v laboratoriju | "Oče je delal na cepilu..." |
| 3 | Red Alert alarm | "Potem je prišel DAN 0..." |
| 4 | Kaos na ulicah | "Svet se je sesula v 48 urah." |
| 5 | Dom je obkoljen | "DAN 3: Prišli so k nam..." |
| 6 | Oče se bori | "Oče: 'Kai... varuj sestro... beži...'" |
| 7 | Mama pada | "Mama: 'Ana... Kai... ljubiva vaju...'" |
| 8 | Kai ugriznjen | "Ugriznili so me... ampak nisem postal zombi." |
| 9 | Ana ugrabljena | "Ana: 'KAIII! POMAGAJ!'" |
| 10 | Giant Troll King | "Nekega... POŠASTI jo je odnesel." |
| 11 | Kai sam | "Starši mrtvi. Sestra ugrabljena. Sam." |
| 12 | Alfa moč se zbudi | "Nekaj se je zbudilo v meni..." |
| 13 | Kai z zombijem | "Zombiji... me poslušajo?" |
| 14 | Pobeg iz mesta | "Moral sem pobegniti... preživeti..." |
| 15 | Potovanje | "7 dni pešačenja skozi peklo..." |
| 16 | Odkrita dolina | "Našel sem jo. Mojo dolino." |
| 17 | Zapuščena kmetija | "Majhna kmetija. Nov začetek." |
| 18 | Kai drži semena | "Če bom preživel... jo bom našel." |
| 19 | Twin Bond signal | "Čutim jo... Ana je ŽIVA." |
| 20 | Kai zapriseže | "Prisegli sem... Najti jo bom. Ne glede na ceno." |
**Po polaroidih:** Kai se prebudi v Spalni Vreči → blur efekt (amnezija) → Twin Bond signal → igra začne.
---
### 🏝️ AKT 1: DEMO — "PREŽIVETJE" (Dan 1200)
**Cilj:** Preživi 200 dni → Odkleni Fazo 1
**Gameplay Loop:**
```
Jutro → Zalij rastline → Zberi pridelek → Popravi/Zgradi
Podnevi → Research, Crafting, Explore otok
Zvečer → Pospravi, odnesi v skrinje, skozi dialog
Noč → Zpati v šotoru (ali Kai omedli = kazen)
```
**Zgodba teče skozi 3 kanale:**
1. **KAI DNEVNIK** — ob vsakem Sleep, Kai napiše en stavek:
- Dan 1: *"Končno... Nov dom. Majhna kmetija, ampak dovolj za začetek."*
- Dan 7: *"En teden sam. Twin Bond signal je bil šibak nocoj."*
- Dan 30: *"En mesec. Preživim. Kmetija raste. Ampak Ana... kje si?"*
- Dan 100: *"Sto dni. Pol poti. Ana... držim se."*
- Dan 200: *"Dvesto dni. Naredil sem, kar sem si obljubil. Zdaj se začne pravi boj."*
2. **TWIN BOND SIGNALI** — 1× na dan, 10% chance, šibek Anin glas:
- *"Kai... čutiš me? Sem ujeta... ampak ŽIVIM."*
- *"Ne obupaj... Twin Bond... deluje..."*
- *"Troll me čuva... ampak nisem sama..."*
- Visual: vijolični blisk na zaslonu, Kai zamrzne za sekundo
3. **FLASHBACK SPOMINI** — Kai stopi 80px blizu posebnih predmetov:
- Star foto → *"Srečni časi"* — Ana z metuljem
- Epruveta → *"Očetov laboratorij"* — "Nikoli ne dotikaj teh vzorčkov"
- Stara sveča → *"Zadnja večerja"* — Anin rojstni dan
**Omejitve v Demu:**
- Samo šotor (ne hiša)
- Samo Tier 1 orodja (lesena)
- Max 3 zombiji (ne morejo delati — samo sledijo)
- 2 rastlini: Pšenica (3 dni) + Korenje (4 dni)
- Voda samo iz dežja (Rain Catcher)
---
### 🌫️ AKT 2: FAZA 1 — "ODPRTJE" (Dan 200+)
**Trigger:** 200 dni preživetih → Fog of War se začne dvigovati
**Nove mehanike:**
| Mehanika | Opis |
|----------|------|
| **Fog of War** | Svet okrog otoka je bil skrit. Zdaj se odkriva. |
| **Gronk Unlock** | Prvi pravi pomočnik (Trol, demo locked) |
| **Scout Zombi** | Edini zombi ki gre v Fog. Vrne se z materiali. |
| **Zombi Workforce** | 5 tipov zombi delavcev z leveli |
| **Ruševine** | Garaža, Rastlinjak, Hlev — najdeš in popraviš |
| **Rudnik** | Majhen rudnik — Baker, Premog, Glina |
| **Crafting napredek** | Motorka, Vodni Filter, Stiskalnica |
**Zombi Workforce (5 tipov):**
| Tip | HP | Hitrost | Naloga | Max | Level Up |
|-----|----|---------|--------|-----|----------|
| 🕵️ Izvidnik (Scout) | 50 | Hiter | Gre v Fog, odkriva mapo, prinaša material | 1 | Hitrejše iskanje, manj izgub |
| 🌾 Kmet (Farmer) | 30 | Počasen | Zaliva, žanje | 3 | Hitrejše, večji pridelek |
| 🪓 Gozdar (Lumberjack) | 40 | Normalen | Seka drevesa | 2 | Hitrejše, več lesa/drevo |
| ⛏️ Rudar (Miner) | 60 | Počasen | Koplje rudo | 2 | Lv5+ najde Železo! |
| 📦 Nosač (Porter) | 40 | Normalen | Pobira Drop Boxe → polaga v skrinje | 1 | Lv7+ nosi 20 predmetov |
**Drop Box sistem:**
- Zombiji ne nosijo direktno v tvoje skrinje (balance)
- Delo → Drop Box → Ti pobereš → Tvoj inventar
- 3 Drop Boxy: ob Workshopu, Rudniku, Farmi
**Parental Ghost Quest:**
```
Kai zgradi Oltar (20 Kamen + 10 Les + 2 Sveči)
+ Najde Inženirsko uro (Markov predmet) → Marko se pojavi
+ Najde Medaljon (Elenin predmet) → Elena se pojavi
Marko: "Kai... Sin... Utri ograjo."
Elena: "Rastline potrebujejo vodo, Kai. Kot ti ljubezen."
Buffi so stalni ko si na kmetiji.
```
---
### 🔵 AKT 3: FAZA 2+ — "ISKANJE ANG" (Prihodnost)
**Unlock:** Faza 2 = Nova območja, portali do biome-ov
**20 Biome-ov:**
**Tier 1 — Normalni:**
Grassland, Forest, Swamp, Desert, Mountain, Snow, Wasteland, Tropical, Radioactive
**Tier 2 — Anomalni (Super rare portali):**
Dino Valley 🦖, Mythical Highlands 🐉, Endless Forest 🌲, Loch Ness 🦕, Catacombs 💀, Egyptian Desert 🏺, Amazon 🌴, Atlantis 🌊, Chernobyl ☢️, Mexican Cenotes 🇲🇽, Witch Forest 🧙‍♀️
**Chernobyl = FINAL ZONE**:
- Kai najde Ano v Reactor Core
- Boss Fight: Dr. Krnić + Giant Troll King (Phase 1)
- **ANA REŠENA** — postane playable character
---
### 💜 AKT 4: REUNIJA & KONEC (Faza 3)
**THE REUNION:**
```
Kai enters Ana's cell.
Ana turns around.
ANA: "...Kai?"
KAI: "Ana... I found you."
[They embrace]
ANA: "You came for me..."
KAI: "Always. Twin Bond, remember?"
[Twin Bond FULLY AWAKENS — zlatem svit]
```
**Ana se pridruži** — dobita Twin Bond sposobnosti:
- Twin Telepathy (komunicirata brez razdalje)
- Twin Strike (skupni napad, 2× poškodbe)
- Twin Shield (preneseta damage drug na drugega)
- Twin Sense (vidita skozi zidove)
- Resurrection Ultimate (vsak reši drugega 1×/dan)
**4 konci (player choice):**
```
Dr. Krnić (dying): "Edini način zdravila zahteva Alpha Hybrid žrtev."
[A] Kai se žrtvuje → Ana vodi novi svet, ima hčer "Kai"
[B] Ana se žrtvuje → Kai vodi novi svet, ima hčer "Ana"
[C] Oba zavrneta → Živita skupaj na kmetiji večno, zombiji ostanejo
[D] Alternativa* → Oba preživita! Svet ozdravljan. PERFECT ENDING ✨
*Samo z vsemi 50 Aninimi clues + 9 Key Fragmenti
```
---
## ⚙️ GAME SISTEMI (Komplet)
### 🌱 Farming
- **Saditev:** Kai ore tla (E tipka) → posadi semena → zaliva vsak dan → žanje
- **Rastline umrejo** brez vode po 1 dnevu
- **Demo:** Pšenica (3 dni), Korenje (4 dni)
- **Faza 1+:** Konoplja (7 dni, MAIN ECONOMY), Sončnice, 80+ other crops
### 💧 Voda
- **VSA VODA JE STRUPENA** (ocean, reke, luže)
- **EDINI VIR:** Deževnica (Rain Catcher)
- Brez dežja = kriza → stockpile!
- **Faza 1:** Vodni Filter (Oglje+Pesek+Plastika) → mora voda postane pitna
### 🏠 Housing
| Tier | Struktura | Energy restore |
|------|-----------|---------------|
| 0 | Spalna Vreča | 40% |
| 1 | Šotor ⛺ | 70% (Demo MAX) |
| 2 | Lesena Koča 🏚️ | 85% (Faza 1 MAX) |
| 3 | Kamnita Hiša 🏠 | 95% |
| 4 | Moderna Hiša 🏡 | 100% |
### 🕰️ Dan/Noč
- 1 in-game dan = 20 realnih minut
- **Noč:** Zombiji 2× močnejši, več spawn-ov
- **02:00:** Kai mora spati (ali omedli = energy loss + money loss)
### 🧟 Zombie Taming
- [SPACE] blizu zombija → Alfa Moč → Screen flash → Zombij podrejen
- Demo: Max 3 zombiji
- Faza 1: Max 8 zombijev
- Assign Task: Interact z zombijem → izberi nalogo
### 🩸 Survival Stats
- **HP:** 100 max, regeneracija s hrano
- **Energy:** 100%, dreni z delom, regenerira s spanjem/hrano
-低 Energy (< 20%): Kai se upočasni
### 🔧 Crafting
- **Campfire:** Osnovno kuhanje
- **Workbench (Garaža Faza 1):** Motorka, Laksarca, Filter
- **Furnace:** Sand + Coal = Glass, Iron Ore = Iron Bar
---
## 🎨 VIZUALNI STIL — PRAVILA ZA GENERIRANJE ASSETOV
### ABSOLUTNA PRAVILA (nikoli krši):
1. **NI PIXEL ART** — High-res ilustracija, smooth lines
2. **Thick black outlines** na vseh likih (~3-5px)
3. **Gothic Dark Chibi** stil — veliki glave, majhna telesa, ekspresivne oči
4. **Muted gothic palette:** Temne modre, vijolične, rjave, sive + neon accenti (teal, zelena)
5. **Transparentno ozadje** (Alpha 24 PNG) za vse like in predmete
6. **Kai DNA:** Vedno pink/zeleni dreadlocki, rdeče oči, gaugi, nos ring — nikoli drugačen!
### Skale (relativno na Kai = 64px visok):
- Gronk: 2.5× Kai = 160px
- Šotor: 2.5× Kai = 160px
- Hiša: 3× Kai = 192px
- Garaža/Hlev: 4× Kai = 256px
- Piščanec: 0.2× Kai = 13px
- Kura: 0.5× Kai = 32px
- Krava: 2× Kai = 128px
### Y-Sorting (globina):
- Vse se sortira po Y koordinati nog
- Kai hodi ZA drevesi ko je višje na ekranu
- Kai hodi PRED drevesi ko je nižje na ekranu
---
## 📋 FAZE — KDAJ SE KAJ ZGODI
```
┌─────────────────────────────────────────────────────────────┐
│ DEMO │ Dan 1200 │ Preživi, kmetuj, spoznaj │
│ │ │ otok, Twin Bond signali │
├─────────────────────────────────────────────────────────────┤
│ FAZA 1 │ Dan 200+ │ Fog se dvigne, Gronk, │
│ (Purchase) │ │ Zombi delavci, Ruševine, │
│ │ │ Rudnik, Scout │
├─────────────────────────────────────────────────────────────┤
│ FAZA 2 │ After Faza 1 │ Portali, 20 Biome-ov, │
│ │ │ Ana lokacija razjasnjena │
├─────────────────────────────────────────────────────────────┤
│ FAZA 3 │ End Game │ Černobil, Ana rešena, │
│ │ │ Twin Bond full power, │
│ │ │ Dr. Krnić boss │
├─────────────────────────────────────────────────────────────┤
│ KONEC │ Player Choice │ 4 različni konci │
└─────────────────────────────────────────────────────────────┘
```
---
## 🛠️ TEHNIČNI STACK
- **Engine:** Phaser 3 (JavaScript)
- **Wrapper:** Electron (Desktop app, macOS/Windows/Linux)
- **Resolucija:** 1920×1080
- **Svet:** 10400×10400px (otok 6400×6400px center)
- **Fizika:** Phaser Arcade Physics (brez gravitacije)
- **Audio:** Web Audio API (proceduralni) + .ogg za glasbo
- **Save:** localStorage (za demo), JSON save file (Faza 1+)
### Scene struktura:
```
MenuScene → (IGRAJ gumb) → GrassSceneClean + UIScene (overlay)
→ (prihodnost) → IntroScene → GrassSceneClean
```
---
## 🎯 KAR JE ZDAJ V IGRI (2026-03-03)
| Sistem | Status |
|--------|--------|
| Phaser 3 + Electron wrapper | ✅ |
| Otok (50×50 tilov, jagged obod) | ✅ |
| Ocean (animiran, pena) | ✅ |
| Kai — hoja vse 4 smeri | ✅ |
| Kai — Y-sorting | ✅ |
| Drevesa (3 stopnje rasti) | ✅ |
| Proceduralni dež (160 črtičnih kapljic) | ✅ |
| **Glavni meni (MenuScene)** | ✅ |
| **Building System [B]** | ✅ ghost preview + Y-sort + collider |
| **UIScene** (HP, energy, hotbar, minimap, inventory) | ✅ |
| Farming sistem | ❌ |
| Zombi AI | ❌ |
| Dan/Noč cikel | ❌ |
| Inventar logika | ❌ |
| Intro sekvenca (20 polaroidov) | ❌ |
| Twin Bond signali | ❌ |
| Flashback spomini | ❌ |
---
## 💬 KLJUČNI DIALOGI (kopiraj direktno)
### Twin Bond signali (Ana):
```
"Kai... čutiš me? Sem ujeta... ampak ŽIVIM. Ne obupaj!"
"Ne obupaj... Twin Bond... deluje..."
"Troll me čuva... ampak nisem sama..."
"Kai... najdi me..."
```
### Gronk:
```
"Gronk... pomaga šefu?"
"Gronk... dober zombi?"
"Gronk... sadi... rastline..."
"Gronk... šef... žalosten? Gronk... pomagal... najti sestro!"
"Gronk... rad... ogenj. Toplo."
```
### Marko (duh):
```
"Kai... Sin... Utri ograjo."
"Skupaj sva verjela v boljši svet. Ne nehaj verjeti."
```
### Elena (duh):
```
"Rastline potrebujejo vodo, Kai. Kot ti ljubezen."
"Nisi kriv. Nikoli nisi bil kriv."
```
### Kai (dnevnik):
```
Dan 1: "Končno... Nov dom. Majhna kmetija, ampak dovolj za začetek."
Dan 30: "En mesec. Preživim. Ampak Ana... kje si?"
Dan 200: "Dvesto dni. Zdaj se začne pravi boj."
```
### THE REUNION:
```
Kai: "Ana... I found you."
Ana: "...Kai? You came for me..."
Kai: "Always. Twin Bond, remember?"
Ana: "I felt you. Every day. Searching..."
[Twin Bond fully awakens]
```
---
*"Twin Bond. Zdaj in vedno."* 💜
*© 2026 HipoDevil666 | Nova Farma / Krvava Žetev*

View File

@@ -0,0 +1,300 @@
# 🎮 NOVA FARMA — KOMPLET STATUS PLAN
## Demo + Faza 1 | Posodobljeno: 2026-03-03
---
> [!IMPORTANT]
> Ta dokument je **živi status** — tukaj je vse kar je narejeno, kaj manjka, in v kakšnem vrstnem redu se dela.
---
## 🗺️ BIG PICTURE — Kaj je igra?
**Nova Farma (Mrtva Dolina)** je survival farming RPG v Phaser 3 / Electron.
```
DEMO → čez 200 dni se odklene → FAZA 1 → Faza 2+ (prihodnost)
```
- **Platforma:** Desktop (Electron + Phaser 3)
- **Svet:** Otok 50×50 tilov (6400px) v oceanu (10400px)
- **Protagonista:** Kai Marković (14 let) | sestra Ana ugrabljena
- **Vizualni stil:** 2.5D Hand-Drawn / Dark Chibi Noir (NI pixel art!)
---
## ✅ KAJ JE ŽE NAREJENEGA (Engine & World)
### 🟢 DELUJE ZDAJ (2026-03-03)
| Sistem | Status | Opomba |
|--------|--------|--------|
| Electron + Phaser 3 wrapper | ✅ Dela | Stabilno |
| Otok (50×50 grid, jagged obod) | ✅ Dela | 6400×6400px |
| Ocean (deep blue ozadje) | ✅ Dela | Klif efekt |
| Pena ob otoku (foam animation) | ✅ Dela | Animirana |
| Kai — hoja vse 4 smeri | ✅ Dela | Walk sheet 16 frames |
| Kai — Y-sorting (globina) | ✅ Dela | `setDepth(this.y)` |
| Kai — trki z robom otoka | ✅ Dela | Physics bounds |
| Zoom (mouse wheel 0.25×) | ✅ Dela | |
| Tla (grass tile grid) | ✅ Dela | Island mask |
| Drevesa (sapling → adult) | ✅ Dela | 3 stopnje rasti |
| Proceduralni dež | ✅ Dela | 160 črtičnih kapljic, naraven |
| 🆕 Building System [B] | ✅ Dela | Ghost + Y-sort + collider |
| Auto-loader vegetacije | ✅ Dela | 28px/160px pixel-perfect |
### 🟡 DELNO / OKVIRNO
| Sistem | Status | Kaj manjka |
|--------|--------|-----------|
| GrassScene — rast trave | 🟡 Partial | Deluje, ampak brez vizualnega feedback-a |
| Gronk sprite | 🟡 Asset je | Koda odstranjena (clean start) |
| Rain Catcher asset | 🟡 Asset je | Ni integriran v gameplay |
---
## ❌ DEMO — KAJ MANJKA (Prioritetni seznam)
Demo = **prvo kar bo videl igralec**. Mora vsebovati:
### 🔴 KRITIČNO — Brez tega Demo ne more iti ven
#### 1. 🎬 INTRO SEKVENCA (20 Polaroid slik)
- [ ] `polaroid_01_druzina.png` — Družina pred virusom
- [ ] `polaroid_02_lab.png` — Oče v laboratoriju
- [ ] `polaroid_03_izbruh.png` — Red Alert
- [ ] `polaroid_04_kaos.png` — Kaos na ulicah
- [ ] `polaroid_05_dom.png` — Dom obkoljen
- [ ] `polaroid_06_oce.png` — Oče se bori
- [ ] `polaroid_07_mama.png` — Mama pada
- [ ] `polaroid_08_kai_ugriznjen.png` — Kai ugriznjen
- [ ] `polaroid_09_ana.png` — Ana ugrabljena
- [ ] `polaroid_10_trol.png` — Veliki Trol Kralj
- [ ] `polaroid_11_sam.png` — Sam
- [ ] `polaroid_12_alfa.png` — Alfa Moč
- [ ] `polaroid_13_zombi.png` — Prvi kontrolirani zombi
- [ ] `polaroid_14_pobeg.png` — Pobeg
- [ ] `polaroid_15_potovanje.png` — Potovanje
- [ ] `polaroid_16_dolina.png` — Najdena dolina
- [ ] `polaroid_17_kmetija.png` — Opuščena kmetija
- [ ] `polaroid_18_seme.png` — Prvi semenski
- [ ] `polaroid_19_bond.png` — Twin Bond
- [ ] `polaroid_20_obljuba.png` — Obljuba
- [ ] **Koda:** IntroScene.js — prikaz polaroidov s tipografiko
#### 2. 🌱 FARMING SISTEM
- [ ] Kai puede s Hoem (E tipka na tleh)
- [ ] `crop_wheat_stage0-3.png` — Pšenica (4 stopnje rasti)
- [ ] `crop_carrot_stage0-4.png` — Korenje (5 stopenj rasti)
- [ ] Zalivanje (Zalivalka iz inventarja)
- [ ] Harvest (klik na zrelo rastlino)
- [ ] Rastlina umre brez vode po 1 dnevu
#### 3. 🧟 ZOMBI SISTEM (Basic)
- [ ] `shambler_walk_sheet.png` — Osnova zombi (4 smeri × 4 frame)
- [ ] Spawn zombijev (max 3 na otoku)
- [ ] AI: Shambler — počasi sledi Kai-u
- [ ] SPACE tipka → Alfa Moč → zombi udomačen
- [ ] Udomačen zombi "sledi" Kai-u
#### 4. 🕰️ DAN/NOČ CIKEL
- [ ] 20-minutni cikel (1 in-game dan)
- [ ] Vizualna sprememba svetlobe (ambient tint)
- [ ] Noč: Kai mora v šotor (ali omedli)
- [ ] Šotor: Sleep → preskoči na jutro
#### 5. ❤️ UI / HUD
- [ ] Zdravje (5 src, HP bar)
- [ ] Energija (Energy bar)
- [ ] Dan + Ura (top right)
- [ ] Trial Version banner (top left)
- [ ] Inventar ozadje (hotbar bottom)
- [ ] Inventar popup (I tipka)
#### 6. 📦 START INVENTAR
- [ ] Kai se pojavi z: Kruh×1, Jabolko×1, Bakla×1, Semena pšenica×10, korenje×5
- [ ] Začetna skrinja na kmetiji (chest object + open menu)
#### 7. 🧠 AMNEZIJA SISTEM
- [ ] Blur efekt ob startu
- [ ] 3 Memory Fragments (blizu predmetov)
- [ ] Screen glitch flash → Polaroid spomin
---
### 🟠 VISOKA PRIORITETA — Demo je bolj popoln z njimi
#### 8. 🌧️ RAIN CATCHER (Lovilec deževnice)
- [ ] Integracija rain_catcher.png v gameplay
- [ ] Ko dežuje → zbira vodo
- [ ] Voda potrebna za zalivanje in pitje
#### 9. 🏠 ŠOTOR (Tent) — SLEEP SISTEM
- [ ] Klik na šotor → "Spati?" dialog
- [ ] Potrditev → Fade to black → jutro (preskoči noč)
- [ ] Energy restore: 70%
#### 10. 💬 DIALOG + TYPEWRITER
- [ ] Typewriter efekt za tekst
- [ ] První dialog: Gronk (tutorial NPC)
- [ ] Twin Bond sporočila od Ane (random 1/dan)
#### 11. 🎵 ZVOK
- [ ] Proceduralni zvok dežja (Web Audio šum)
- [ ] Koračaji (`footstep.ogg` ali proceduralni)
- [ ] Sekanje dreves
- [ ] Ambientna glasba (žuborenje oceana)
---
## 🔵 FAZA 1 — Odkleni se po 200 dneh
> Faza 1 se začne na **istem save filu**, samo se odklene vsebina.
### FAZA 1 CHECKLIST
#### Svet & Mapa
- [ ] **Fog of War** — skrita mapa okrog otoka
- [ ] **Fog odpiranje** — Zombi Izvidnik razkrije območja
- [ ] **3 Ruševine** v fogu: Garaža, Rastlinjak, Hlev
- [ ] **Rudnik** (5×5 area, vhod zrušen)
#### Zombi Workforce (5 tipov)
- [ ] 🕵️ **Izvidnik (Scout)** — Gre v Fog, prinese material, odpira meglo (Max 1)
- [ ] 🌾 **Kmet (Farmer)** — Zaliva, žanje (Max 3)
- [ ] 🪓 **Gozdar (Lumberjack)** — Seka drevesa (Max 2)
- [ ] ⛏️ **Rudar (Miner)** — Koplje rudo, Lv5+ najde železo (Max 2)
- [ ] 📦 **Nosač (Porter)** — Pobira Drop Boxe → polaga v skrinje (Max 1)
#### Drop Box Sistem
- [ ] Drop Box objekt (Zombiji odlagajo sem)
- [ ] 3 lokacije: ob workshopu, rudniku, farmi
- [ ] Kai pobere ročno iz Drop Boxa
#### Crafting (v Garaži)
- [ ] 🪚 **Motorka/Laksarca** (Motor + Veriga + Glava + Bencin)
- [ ] 🌿 **Trav-rezalnik** (Motor + Rotating Head + Palica)
- [ ] 💧 **Vodni Filter** (Oglje + Pesek + Plastika) — Ocean water → Clean water + Sol
- [ ] 🛢️ **Stiskalnica** (Iron×5 + Wood×20 + Stone×10) — Sončnice → Bio-olje
#### Rudarjenje
- [ ] Kop z Motiko (E blizu kamnine → zbira material)
- [ ] Baker (Copper) — common
- [ ] Premog (Coal) — common
- [ ] Glina (Clay) — blizu vode
- [ ] Železo (Iron) — samo Rudar Lv5+!
#### Živali (Nakup)
- [ ] Dve živali dostopni v Fazi 1: Kura + Krava
- [ ] Delivery sistem (živali pridejo v škatli)
- [ ] Kokošnjak (Coop) za Kure
- [ ] Hlev (Barn) za Krave + Gronka + Susi
#### Nove Crops (Faza 1 odkleni)
- [ ] Konoplja (Cannabis) — 7 dni, Main Economy
- [ ] Sončnice (Sunflowers) → Bio-olje
- [ ] Krompir, Paradižnik, Bučke + 80+ more (postopno)
#### Gronk & Duhovi
- [ ] Gronk odkleni v Fazi 1 (Demo: Locked)
- Izjema: Prvih 20 kupcev + Streamerji → takoj!
- [ ] Susi Jazbečarka (Gronkova psička)
- [ ] Oltar → Duh Očeta in Matere
---
## 📊 REALNI NAPREDEK (2026-03-03)
```
DEMO NAPREDEK:
████░░░░░░░░░░░░░░░░░░ ~20%
Kaj je: Svet, Kai, Drevesa, Building sistem, Dež
Kaj ni: Farming, Zombi AI, UI, Intro, Dan/Noč, Inventar
FAZA 1 NAPREDEK:
█░░░░░░░░░░░░░░░░░░░░░ ~5%
Kaj je: Building mood (temelj za zgradbe)
Kaj ni: Vse ostalo
```
---
## 🎯 PRIPOROČENI VRSTNI RED DELA
### 📅 SPRINT 1 — "Osnove Preživetja" (~23 seje)
1. **HUD** — hearts, energy bar, dan/ura text (brez slike, samo tekst ok)
2. **Inventar** — Kai dobi start items ob spawnu
3. **Šotor** — Sleep interakcija (fade → jutro)
4. **Dan/Noč** — ambient tint + ura teče
### 📅 SPRINT 2 — "Kmetovanje" (~34 seje)
5. **Tla Farming** — Kai hora s hoem (E tipka)
6. **Crop assets** — Wheat (4 stopnje) + Carrot (5 stopenj) generate
7. **Planting + Watering** — logika rasti
8. **Harvest** — klik na zrelo → inventar
### 📅 SPRINT 3 — "Zombi" (~23 seje)
9. **Shambler sprite** — generirati walk sheet
10. **Zombi AI** — follow Kai, basic pathfinding
11. **Alfa Moč** (SPACE) — udomačitev
### 📅 SPRINT 4 — "Zgodba" (~2 seje + asset generiranje)
12. **20 Polaroid slik** — generirati z AI
13. **IntroScene.js** — prikaz polaroidov
14. **Amnezija sistem** — blur + 3 spomini
### 📅 SPRINT 5 — "Demo Polish" (~12 seje)
15. **Rain Catcher** — gameplay integracija
16. **Zvoki** (proceduralni ali AI generated)
17. **Trial Version UI** banner
> Šele po tem → Faza 1 razvoj!
---
## 📁 ASSET STATUS
### ✅ Imamo
- `kai_walk_sheet.png` — 16 frame walk (4 smeri × 4)
- `gronk_walk_sheet.png` — asset je, koda disabled
- `sotor.png`, `campfire` (taborni_ogenj), `rain_catcher`, `foundation_concrete`
- `tree_adult_0-5.png`, `drevo_faza_1/2/veliko.png`
- `grass_ref_1`, `visoka_trava_v2`, `grass_cluster_*`
- `dead_nature_0-8.png`, `fence_sign_0-2.png`
- `rain_drops.png`
- UI asseti: hotbar, inventory_panel, health_bar, dialog_panel, itd.
### ❌ Manjka (DEMO KRITIČNO)
- `shambler_walk_sheet.png` (zombi, 4 smeri × 4 frame)
- `crop_wheat_stage0-3.png` (4 stopnje rasti)
- `crop_carrot_stage0-4.png` (5 stopenj rasti)
- `polaroid_01-20.png` (20 polaroidnih slik — biggest work)
- `heart_full.png` / `heart_empty.png` (za UI)
- `sleeping_bag_interact.png` (animirana verzija)
### ❌ Manjka (FAZA 1)
- `shambler_idle.png`, `shambler_tamed.png`
- `zombie_scout_*.png`, `zombie_farmer_*.png`, itd.
- `garage_ruin.png`, `greenhouse_ruin.png`, `barn_ruin.png`
- `drop_box.png`
- `konoplja_stage0-7.png`
- Kura, Krave spritei
---
## 💡 SKUPNI POVZETEK
| Faza | Napredek | Ostaja |
|------|----------|--------|
| **Svet/Engine** | 🟢 85% | Samo polish |
| **Demo — Gameplay** | 🔴 15% | Farming, AI, Inventar |
| **Demo — Vsebina** | 🔴 5% | Intro, dialogi, UI |
| **Demo — Assets** | 🟡 30% | Manjka farming + zombi sprites |
| **Faza 1** | 🔴 5% | Praktično vse |
---
*Posodobljeno: 2026-03-03 ob 13:04*
*Za detajle: `DEMO_FAZA1_COMPLETE.md` | `GAME_BIBLE_FINAL_2026.md`*

View File

@@ -0,0 +1,394 @@
# 📖 NOVA FARMA — KOMPLETNA ZGODBA & IMPLEMENTACIJSKI PLAN
## Krvava Žetev | Verzija: 2026-03-03
> Tukaj je VSE: zgodba, kako se prikaže v igri, v kakšnem vrstnem redu jo gradiva, in kateri assets & koda so potrebni za vsak del.
---
## 🌍 SVET & KONTEKST
| | |
|-|-|
| **Leto** | 2084 |
| **Lokacija** | Slovenija → Skriti otok v Dolini |
| **Žanr** | Survival Farming RPG + Zombie Control |
| **Core Theme** | Preživetje, Iskanje sestre, Obnova civilizacije |
| **Visual Style** | 2.5D Dark Chibi Noir |
---
## 👥 LIKI
| Lik | Starost | Vloga | Status v Demu |
|-----|---------|-------|--------------|
| **Kai Marković** | 14 | Protagonist, Alpha Hybrid | ✅ Player character |
| **Ana Marković** | 14 | Dvojčica, UGRABLJENA | 💜 Samo glas (Twin Bond) |
| **Marko Marković** | ~45 | Oče, znanstvenik | 👻 Duh (po oltar questu) |
| **Elena Marković** | ~43 | Mama, znanstvenica | 👻 Duh (po oltar questu) |
| **Gronk** | ? | Zombi, prvi pomočnik | 🟡 Faza 1 unlock |
| **Dr. Krnić** | ~60 | Glavni zlobnik | 🔒 Faza 2+ |
| **Giant Troll King** | ? | Ugrabil Ano | 🔒 Faza 2+ |
### Kai — DNA & Osebnost
- **Lasje:** Pink/Zeleni dreadlocki
- **Oči:** Rdeče (genetska lastnost Marković linije)
- **Outfit:** Raztrgane cunje (start), mogoče upgrade
- **Karakter:** Tih, določen, redko govori — ampak ko govori, je globoko
- **Posebnost:** Alfa Hybrid — ugriznjen, a ni postal zombi → dobi moč *kontrolirati* zombije
### Ana — Zakaj je vse drugače brez nje
- Kai jo čuti skozi **Twin Bond** (pasivna telepatija)
- Random sporočila enkrat na dan (redka, šibka)
- Ves Kai-ev gameplay je motiviran z "najti Ano"
---
## 🎬 ZGODBA — AKT PO AKT
---
### 📼 PROLOG (Pred igro — Intro Sekvenca)
**Prikaže se samo enkrat ob prvem zagonu** — 20 Polaroid slik s typewriter komentarji.
```
[Slika 1] Družina pred virusom → "Pred virusom... bili smo srečni."
[Slika 2] Oče v laboratoriju → "Oče je delal na cepilu..."
[Slika 3] Red Alert → "Potem je prišel DAN 0..."
[Slika 4] Kaos na ulicah → "Svet se je sesula v 48 urah."
[Slika 5] Dom obkoljen → "DAN 3: Prišli so k nam..."
[Slika 6] Oče se bori → "Oče: 'Kai... varuj sestro... beži...'"
[Slika 7] Mama pada → "Mama: 'Ana... Kai... ljubiva vaju...'"
[Slika 8] Kai ugriznjen → "Ugriznili so me... ampak nisem postal zombi."
[Slika 9] Ana ugrabljena → "Ana: 'KAIII! POMAGAJ!'"
[Slika 10] Troll King → "Nekega... POŠASTI jo je odnesel."
[Slika 11] Sam → "Starši mrtvi. Sestra ugrabljena. Sam."
[Slika 12] Alfa Moč → "Nekaj se je zbudilo v meni..."
[Slika 13] Prvi zombi → "Zombiji... me poslušajo?"
[Slika 14] Pobeg → "Moral sem pobegniti... preživeti..."
[Slika 15] Potovanje → "7 dni pešačenja skozi peklo..."
[Slika 16] Dolina → "Našel sem jo. Mojo dolino."
[Slika 17] Kmetija → "Majhna kmetija. Nov začetek."
[Slika 18] Semena → "Če bom preživel... jo bom našel."
[Slika 19] Twin Bond → "Čutim jo... Ana je ŽIVA."
[Slika 20] Obljuba → "Prisegli sem... Najti jo bom. Ne glede na ceno."
```
**Po polaroidih:**
- Fade to black
- Kai se prebudi v **Spalni vreči** ← prvi frame igre
- Vid zamegljen (amnezija blur efekt)
- Zveni šibak signal: *"Kai... sem tu..."* (Ana)
- Blur se razjasni → **IGRA SE ZAČNE**
---
### 🏝️ AKT 1: PREŽIVETJE (Demo — Dan 1200)
**Cilj:** Preživi 200 dni → odkleni Fazo 1
**Kaj se dogaja:**
Kai sam na otoku. Gradi, kmetuje, zbira material. Vsak dan je boj.
Voda je samo iz dežja (Rain Catcher). Zombiji napadajo ponoči.
#### Zgodba teče skozi 3 kanale:
**1. DNEVNIK (Kai-ev notranji monolog)**
Vsak dan ob prvem pritisku na Sleep:
```
DAN 1: "Končno... Nov dom. Majhna kmetija, ampak dovolj za začetek."
DAN 2: "Danes sem srečal zombija. Ime sem mu dal Gronk. (Faza 1 unlock)"
DAN 3: "Flashback... Vidim mamo, očeta, Ano... Boli. Ampak moram naprej."
DAN 7: "Teden dni sam. Twin Bond signal je bil šibak nocoj."
DAN 30: "En mesec. Preživim. Kmetija raste. Ampak Ana... KJE SI?"
DAN 100:"Sto dni. Pol poti. Zbral sem dovolj za... Moram naprej."
DAN 200:"Dvesto dni. Naredil sem, kar sem si obljubil. Zdaj se začne pravi boj."
```
**2. TWIN BOND SIGNALI** (naključno 1× na dan, ~10% chance)
```
[Šibek telepatski glas]
Ana: "Kai... čutiš me? Sem ujeta... ampak ŽIVIM. Ne obupaj!"
Ana: "Ne obupaj... Twin Bond... deluje..."
Ana: "Troll me čuva... ampak nisem sama..."
Ana: "Kai... najdi me..."
[Signal izgubljen]
```
Visual: Vijolični blisk na zaslonu, Kai zamrzne za sekundo, nato nadaljuje.
**3. FLASHBACK SPOMINI** (trigger: pristopi k posebnim predmetom)
| Predmet | Spomin |
|---------|--------|
| Star foto na tleh | "Srečni Časi" — Ana z metuljem v roki |
| Testna epruveta | "Očetov Laboratorij" — "Nikoli ne dotikaj teh vzorčkov" |
| Stara sveča | "Zadnja Večerja" — Anin rojstni dan, torta, smeh |
Vsak flashback = blago screen blur → Polaroid naredi "klik" → glas → nazaj.
---
### 🌫️ AKT 2: FAZA 1 — ODPRTJE (Dan 200+)
**Trigger:** 200 dni preživetih → fog of war se začne odpirati
**Unlock:** Gronk (ali prvih 20 kupcev ga dobijo takoj)
```
[Fog se delno dvigne]
SISTEM: "Megla se dviga... Svet je večji kot si mislil."
KAI (misli): "Kaj je tam zunaj...?"
[Gronk se pojavi]
GRONK: "Gronk... pomaga šefu?"
KAI: "Ti si... Gronk. Moj pomočnik."
GRONK: "Gronk... dober zombi?"
KAI: "Najboljši zombi."
GRONK: *Srečno mrmra*
```
**Nove zgodbe:**
- Scout Zombi razkrije ruševine v fogu
- Garaža, Rastlinjak, Hlev — vsaka ruševina ima **izgubo/zgodovino** (napisi v ruinah)
- Garaža: "Tukaj je imel nekdo sanje... Popravi jih."
- Rudnik: "Globoko pod tlemi leži odgovor..."
**Gronk Dialogi** (sproži se med delom):
```
[Med kmetovanjem]
GRONK: "Gronk... sadi... rastline..."
KAI: "Dobro delo, Gronk!"
GRONK: "Gronk... je... ponosen!"
[Kai je žalosten]
GRONK: "Šef... žalosten?"
KAI: "Moja sestra... Pogrešam jo..."
GRONK: "Gronk... pomagal... najti sestro!"
KAI: "Hvala, Gronk. Res si dober prijatelj."
[Ko sreča Kai ob ognju zvečer]
GRONK: "Gronk... rad... ogenj. Toplo."
KAI: "Jaz pa rad... mislim na Ano."
GRONK: "Ana... dobra? Kot šef?"
KAI: "Boljša. Ona je genialna."
[Grozno vreme]
GRONK: "Gronk... ne mara... mokro."
KAI: "Nobeden ne mara dežja, Gronk."
GRONK: "Gronk... ljubi... Gronk lažje govori to."
KAI: ["..."smeh]
```
**Parental Ghost (Oltar event):**
Kai zgradi oltar (20 Kamen + 10 Les + 2 Sveči) in najde predmete:
- **Inženirska ura** (Markov predmet) → Marko se pojavi kot duh
- **Medaljon** (Elenin predmet) → Elena se pojavi kot duh
```
[Marko Duh — moder svit]
MARKO: "Kai... Sin..."
KAI: "Ata? Ali si... res ti?"
MARKO: "Del mene ostaja. Dokler se spomniš."
MARKO: "Utri ograjo, sin. Zombiji so šibki, ampak horda..."
BUFF AKTIVEN: "Očetova Modrost: +15% Craft hitrost"
[Elena Duh — zlat svit]
ELENA: "Moj Kai... kako si zrasel."
KAI: "Mama... Žal mi je. Nisem mogel—"
ELENA: "Molči. Nisi kriv. Nikoli nisi bil kriv."
ELENA: "Rastline potrebujejo vodo, Kai. Kot ti ljubezen."
BUFF AKTIVEN: "Mamina Ljubezen: +10% Energy regen na kmetiji"
```
---
### 💜 AKT 3: REUNIJA (Faza 2+ — prihodnost)
**Lokacija:** Černobil Reaktor (Level 6175)
Kai pride skozi radiation zones, premaga Giant Troll King (Phase 1)...
```
[Kai vstopi v celico]
[Ana se obrne]
[Njune oči se srečajo]
ANA: "...Kai?"
KAI: "Ana... I found you."
[Tečeta drug k drugemu, objameta se, jočeta]
ANA: "You... you came for me..."
KAI: "Always. Twin Bond, remember?"
ANA: "I felt you. Every day. Searching..."
KAI: "Every single day."
[Twin Bond FULLY AWAKENS — zlata svetloba]
```
**ANA SE PRIDRUŽI** — postane playable character. Unlock vseh Twin Bond abilityjev:
- Twin Telepathy (govorita brez distance)
- Twin Strike (skupni napad, 2× damage)
- Twin Shield (prenos škode med njima)
- Twin Sense (vidita skozi zidove)
- Shared Buffs (hrana/potions za oba)
- Resurrection Ultimate (vsak reši drugega 1×/dan)
---
### ⚖️ AKT 4: ZADNJA IZBIRA (Faza 3 — End Game)
Dr. Krnić: *"Pridi v Reactor Core. Ali spusti SUPER VIRUS. Imaš 7 dni."*
Po Boss Rush (Dr. Krnić + Giant Troll King Phase 2):
```
[Cure Machine]
Dr. Krnić (dying): "Edini način... Alpha Hybrid Sacrifice.
Eden od vaju mora umreti... da se svet pozdravili."
[Twin Bond: "Skupaj... Premagava?"]
```
**CHOICE SCREEN:**
```
[ A ] Kai se žrtvuje → Ending 1
[ B ] Ana se žrtvuje → Ending 2
[ C ] Zavrni → Ending 3 (Dark World)
[ D ] Alternativa* → Ending 4 PERFECT ✨
* Samo če si zbral VSEH 50 Aninih clues + 9 Key Fragmentov
```
---
## 🏁 4 KONCI
### Ending 1 — 💔 Kai se žrtvuje
Kai: *"Ana... ti si genialna. Svet te potrebuje bolj. Živi. Za naju oba."*
Ana: Vodi novi svet. Ima hčer po imenu **Kai**.
### Ending 2 — 💔 Ana se žrtvuje
Ana: *"Kai... ti si rešil mene. Pusti meni rešiti ostale."*
Kai: Vodi novi svet. Ima hčer po imenu **Ana**.
### Ending 3 — 💀 Dark World
Oba zavrneta. Zombiji ostanejo. Živita skupaj na kmetiji večno.
*"Ne žalim ničesar."* — Kai (68)
### Ending 4 — 🌟 Perfect ✨
Ana: *"Počakaj! Twin Bond energija + 50 clues + 9 Fragmentov = ALTERNATIVNA REŠITEV!"*
Oba preživita. Svet ozdravljen. Zombiji postanejo *prijazni delavci*.
Zadnja scena: Piknik na kmetiji. Kai, Ana, otroci, smeh.
---
## 🛠️ KAKO SE ZGODBA GRADI V IGRI
### Sistem 1: IntroScene.js
```
ASSETS: polaroid_01.png polaroid_20.png
CODE: IntroScene.js
TRIGGER: Samo 1x, ob prvem startu
TRACKING: localStorage.setItem('introSeen', true)
```
### Sistem 2: Journal (Dnevnik)
```
ASSETS: ui_dialog.png (már imamo!)
CODE: JournalSystem.js (ustvari)
TRIGGER: Vsak večer ob kliku Sleep
DATA: Array dialogov indexiran po dnevu
```
### Sistem 3: Twin Bond Signal
```
ASSETS: vijolično overlay (Graphics — brez asseta)
CODE: TwinBondSystem.js
TRIGGER: Vsak dan, 10% chance ob polnoči
SOUND: Ana glas (Edge-TTS proceduralan)
```
### Sistem 4: Flashback Memories
```
ASSETS: 3 predmeti: foto, epruveta, sveča
CODE: MemorySystem.js
TRIGGER: Kai stopi v 80px bližino predmeta
EFFECT: Screen blur → Polaroid click → glas → blur ven
```
### Sistem 5: Gronk Dialogi (Faza 1)
```
ASSETS: gronk_walk_sheet.png (že imamo!)
CODE: GronjSystem.js (dodamo k NPC logic)
TRIGGER: Triggeri: ob kmetovanju, po 20+ minutah, ob ognju, jokanju
```
### Sistem 6: Parental Ghosts (Oltar)
```
ASSETS: oltar_obj.png, inzenirska_ura.png, medaljon.png
CODE: AltarSystem.js
TRIGGER: Zgradi oltar + najdi oba predmeta
EFFECT: Duh se pojavi, dá buff, ostane permanent
```
---
## 📋 IMPLEMENTACIJSKI VRSTNI RED
### 🟢 SPRINT A — Prolog (Intro Sekvenca)
```
1. Generiramo 20 Polaroid slik (AI)
2. Napišemo IntroScene.js
- Fade in/out med slikami
- Typewriter tekst na vsaki
- ESC/Space skip za vsako
3. Integriramo v game.js:
scene: [IntroScene, GrassScene, UIScene]
4. localStorage tracking → ne ponavlja se
```
### 🟡 SPRINT B — Daily Journal
```
1. Napišemo dialogArray.js (vse Kai dnevniške vnose po dnevu)
2. JournalSystem.js → ob kliku "Sleep" prikaže dialog
3. UIScene: dialog box za prikaz (dialog_panel.png — že imamo!)
```
### 🟠 SPRINT C — Twin Bond
```
1. TwinBondSystem.js
- Vsak dan, 10% chance Signal
- Ana voice line (typewriter)
- Vijolični blisk efekt
- UI: Vijolična ikona v HUD blisne
2. HUD: Dodamo Twin Bond ikono (majhna vijolična srce ikona)
```
### 🔵 SPRINT D — Flashback Memories
```
1. Postavimo 3 predmete na otok (foto, epruveta, sveča)
2. MemorySystem.js — proximity trigger (80px)
3. Efekt: Phaser camera.setPostPipeline blur
4. Polaroid click zvok (proceduralen)
```
---
## 📊 ZGODBA STATUS (2026-03-03)
```
PROLOG (20 polaroidov): ██░░░░░░░░ 10% (napisi OK, slike manjkajo)
KAI DNEVNIK: ████░░░░░░ 40% (tekst pisan, kode ni)
TWIN BOND SIGNALI: █░░░░░░░░░ 5% (dizajn ok, implementacija 0)
FLASHBACK MEMORIES: █░░░░░░░░░ 5% (dizajn ok, predmeti manjkajo)
GRONK DIALOGI: ████░░░░░░ 40% (tekst pisan, Gronk ni v igri)
PARENTAL GHOSTS: █░░░░░░░░░ 5% (samo dizajn)
AKT 2-4 (Faza 1+): ░░░░░░░░░░ 0% (prihodnost)
```
---
*Posodobljeno: 2026-03-03 ob 13:19*
*Za dialoge: `DEMO_FAZA1_NAPISI_DIALOGI.md`*
*Za full lore: `ZGODBA_CELOTNA.md`*

View File

@@ -3,6 +3,7 @@ if (typeof process !== 'undefined' && process.env) {
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true';
}
import MenuScene from './scenes/MenuScene.js';
import GrassScene from './scenes/GrassScene_Clean.js';
import UIScene from './scenes/UIScene.js';
@@ -14,8 +15,8 @@ const config = {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH
},
pixelArt: true, // Zagotovi ostrino pri zumiranju (pixel art style)
backgroundColor: '#1a1a1a', // Temno siva, da slika izstopa
pixelArt: false, // Meni uporablja smooth slike
backgroundColor: '#000000',
parent: 'body',
physics: {
default: 'arcade',
@@ -24,7 +25,9 @@ const config = {
gravity: { y: 0 }
}
},
scene: [GrassScene, UIScene]
// Vrstni red: MenuScene je prva (avtomatski start), ostale so pasivne
scene: [MenuScene, GrassScene, UIScene]
};
const game = new Phaser.Game(config);

View File

@@ -1,4 +1,9 @@
import BuildingSystem from '../systems/BuildingSystem.js';
import FarmingSystem from '../systems/FarmingSystem.js';
import WaterSystem from '../systems/WaterSystem.js';
import DayNightSystem from '../systems/DayNightSystem.js';
import InventorySystem from '../systems/InventorySystem.js';
import ZombieSystem from '../systems/ZombieSystem.js';
export default class GrassSceneClean extends Phaser.Scene {
constructor() {
@@ -123,6 +128,15 @@ export default class GrassSceneClean extends Phaser.Scene {
// 8. Weather System
this.load.image('rain_drops', 'DEMO_FAZA1/Environment/rain_drops.png');
// 9. Farming System Assets
FarmingSystem.preload(this);
// 10. Water System Assets
WaterSystem.preload(this);
// 11. Zombie System Assets
ZombieSystem.preload(this);
}
// ===================================================================
@@ -1091,6 +1105,10 @@ export default class GrassSceneClean extends Phaser.Scene {
console.log('🏗️ Building System initialized — press [B] to open build mode');
// === ZAŽENIMO UIScene kot overlay (HUD sloj) ===
if (!this.scene.isActive('UIScene')) {
this.scene.launch('UIScene');
}
// --- ANIMATIONS ---
// 0-3: Down, 4-7: Left, 8-11: Right, 12-15: Up
@@ -1212,6 +1230,147 @@ export default class GrassSceneClean extends Phaser.Scene {
console.log('🌧️ Rain system initialized (procedural canvas drops)!');
// ─── FARMING SYSTEM ────────────────────────────────────────────
this.farmingSystem = new FarmingSystem(this, {
islandX: this.islandX,
islandY: this.islandY,
tileSize: this.TILE_SIZE,
});
// Harvest event → pokaži gold gain in posodobi inventory
this.events.on('harvest', (data) => {
console.log(`🌾 Harvest: ${data.item} ×${data.count} = ${data.value}g`);
// Posodobi inventory gold (ko bo InventorySystem)
if (this.playerGold !== undefined) this.playerGold += data.value;
else this.playerGold = data.value;
// Emit do UIScene
const uiScene = this.scene.get('UIScene');
if (uiScene && uiScene.events) {
uiScene.events.emit('goldChanged', this.playerGold);
}
});
console.log('✅ FarmingSystem ready — [E] ore, posadi, zalij, pozeni!');
// ─── WATER SYSTEM ────────────────────────────────────────
this.waterSystem = new WaterSystem(this, {
tileSize: this.TILE_SIZE,
});
// Postavi Rain Catcher blizu centra otoka (kamor Kai spawn-a)
const rcX = this.islandX + this.islandWidth / 2 + 200;
const rcY = this.islandY + this.islandHeight / 2;
this.waterSystem.placeRainCatcher(rcX, rcY);
// Poveži water system z farming sistemom (preverj vodo pred zalivanjem)
this.farmingSystem.waterSystem = this.waterSystem;
// WATER CHANGED EVENT → UI update
this.events.on('waterChanged', (data) => {
const uiScene = this.scene.get('UIScene');
if (uiScene && uiScene.events) {
uiScene.events.emit('waterChanged', data);
}
});
console.log('✅ WaterSystem ready — [F] napolni vedro pri Rain Catcher-ju!');
// ─── DAY/NIGHT SYSTEM ─────────────────────────────────
this.dayNightSystem = new DayNightSystem(this, {
speed: 1, // 1 sec real = 1 min igre (24 min = 1 dan)
});
// === EVENT WIRE-UP ===
// Dan se je spremenil → farma tick
this.events.on('dayChanged', ({ day }) => {
if (this.farmingSystem) this.farmingSystem.onNewDay(day);
console.log(`📅 Dan ${day} prišel!`);
});
// Dež → Rain Catcher zbira
this.events.on('rainDay', ({ day }) => {
if (this.waterSystem) this.waterSystem.onRain();
this.triggerRainfall(); // Vizualni dež
console.log(`🌧️ Dežen dan ${day}`);
});
// Jutro → Rast trave/dreves
this.events.on('morningArrived', () => {
this.morningGrowthCheck();
});
// Ur a sprememba → UIScene clock
this.events.on('timeChanged', (data) => {
const uiScene = this.scene.get('UIScene');
if (uiScene && uiScene.events) {
uiScene.events.emit('timeChanged', data);
}
});
// Registriraj spalno vrečo kot sleep objekt
// (spawn point je center otoka)
const sleepX = this.islandX + this.islandWidth / 2;
const sleepY = this.islandY + this.islandHeight / 2;
this.dayNightSystem.registerSleepObject(sleepX, sleepY, null, 'sleeping_bag');
console.log('✅ DayNightSystem ready — [Z] za spanje, 1 min/s!');
// ─── INVENTORY SYSTEM ─────────────────────────────────
this.inventorySystem = new InventorySystem(this);
// Pošlji začetni hotbar stanje v UIScene
this.scene.get('UIScene')?.events.emit('hotbarUpdate', {
slots: this.inventorySystem.hotbar.map((key, i) => {
const def = this.inventorySystem.ITEM_DEFS[key];
return key ? {
key, icon: def?.icon, name: def?.name,
count: this.inventorySystem.getCount(key),
active: i === 0, type: def?.type,
stackable: def?.stackable
} : null;
}),
activeSlot: 0,
gold: 0,
});
console.log('✅ InventorySystem ready — [1-7] hotbar, scroll wheel!');
// ─── ZOMBIE SYSTEM ─────────────────────────────────
this.zombieSystem = new ZombieSystem(this, {
islandX: this.islandX,
islandY: this.islandY,
islandW: this.islandWidth,
islandH: this.islandHeight,
});
this.zombieSystem.createAnimations();
// Noč → Začni spawnat
this.events.on('nightArrived', () => {
this.zombieSystem.startNightSpawning();
});
// Jutro → Ustavi spawn (zombiji ostanejo)
this.events.on('morningArrived', () => {
this.zombieSystem.stopNightSpawning();
});
// Zombi udomačen event
this.events.on('zombieTamed', ({ zombie }) => {
console.log('✨ Kai je udomačil zombija!');
});
console.log('✅ ZombieSystem ready — [SPACE] za Alfa Moč!');
// === TAKOJ SPAWNI ZOMBIJA (za testiranje, podnevi+ponoči) ===
this.time.delayedCall(3000, () => {
if (this.zombieSystem) {
this.zombieSystem.spawnZombie(); // 1x takoj ob startu
console.log('🧟 Test zombi spawnan!');
}
});
}
@@ -1703,6 +1862,26 @@ export default class GrassSceneClean extends Phaser.Scene {
}
}
// === WATER SYSTEM UPDATE ===
if (this.waterSystem) {
this.waterSystem.update(this.kai);
}
// === DAY/NIGHT SYSTEM UPDATE ===
if (this.dayNightSystem) {
this.dayNightSystem.update(delta, this.kai);
}
// === ZOMBIE SYSTEM UPDATE ===
if (this.zombieSystem) {
this.zombieSystem.update(this.kai, delta);
}
// === FARMING SYSTEM UPDATE ===
if (this.farmingSystem) {
this.farmingSystem.update(this.kai, time);
}
// === BUILDING SYSTEM: Ghost follows pointer, building Y-sorting ===
if (this.buildingSystem) {
this.buildingSystem.update(this.input.activePointer);

View File

@@ -0,0 +1,359 @@
/**
* ============================================================
* MenuScene.js — Nova Farma | Glavni Meni
* ============================================================
* Vsebuje:
* - Animirano ozadje (otok ponoči)
* - Naslov igre z glow efektom
* - Kai lik (lebdi + diha)
* - Gumbi: IGRAJ, NASTAVITVE, IZHOD
* - Trial Version badge + Purchase badge
* - Firefly particle efekt
* - Fog (megla) ki se premika
* ============================================================
*/
export default class MenuScene extends Phaser.Scene {
constructor() {
super({ key: 'MenuScene' });
}
preload() {
this.load.path = 'assets/';
// Ozadje
this.load.image('menu_bg', 'DEMO_FAZA1/UI/menu_background.png');
// Liki
this.load.image('kai_menu', 'DEMO_FAZA1/Characters/Kai_Dreads.png');
// UI Badges
this.load.image('badge_trial', 'DEMO_FAZA1/UI/badge_trial.png');
this.load.image('badge_buy', 'DEMO_FAZA1/UI/badge_purchase.png');
// Gumb ozadje (dialog panel za gumbe)
this.load.image('ui_dialog', 'DEMO_FAZA1/UI/dialog_panel.png');
}
create() {
const W = this.cameras.main.width; // 1920
const H = this.cameras.main.height; // 1080
// ─────────────────────────────────────────────────────────────────
// OZADJE
// ─────────────────────────────────────────────────────────────────
this.bg = this.add.image(W / 2, H / 2, 'menu_bg')
.setDisplaySize(W, H)
.setDepth(0);
// Temna vinjeta (robovi temnejši)
const vignette = this.add.graphics().setDepth(1);
const vigRad = this.add.graphics().setDepth(1);
// Gradientni overlay — temno rob, svetel center
for (let i = 0; i < 8; i++) {
const alpha = 0.04 * (8 - i);
vignette.fillStyle(0x000000, alpha);
vignette.fillRect(0, 0, W * (i * 0.06), H);
vignette.fillRect(W - W * (i * 0.06), 0, W * (i * 0.06), H);
vignette.fillRect(0, 0, W, H * (i * 0.04));
vignette.fillRect(0, H - H * (i * 0.04), W, H * (i * 0.04));
}
// ─────────────────────────────────────────────────────────────────
// FIREFLIES — lebdeče svetlobne pike
// ─────────────────────────────────────────────────────────────────
this.fireflies = [];
for (let i = 0; i < 35; i++) {
const ff = {
x: Phaser.Math.Between(60, W - 60),
y: Phaser.Math.Between(100, H - 150),
r: Phaser.Math.FloatBetween(2, 5),
alpha: Phaser.Math.FloatBetween(0.2, 0.9),
speed: Phaser.Math.FloatBetween(0.3, 0.9),
offset: Math.random() * Math.PI * 2,
color: Phaser.Math.RND.pick([0x44ffcc, 0xaaffaa, 0x88ffdd, 0xccff66]),
};
this.fireflies.push(ff);
}
this.fireflyGfx = this.add.graphics().setDepth(2);
// ─────────────────────────────────────────────────────────────────
// KAI — desno, lebdeč
// ─────────────────────────────────────────────────────────────────
this.kaiImg = this.add.image(W * 0.78, H * 0.62, 'kai_menu')
.setScale(0.95)
.setOrigin(0.5, 1.0)
.setDepth(10);
// Kai lebdi gor/dol
this.tweens.add({
targets: this.kaiImg,
y: H * 0.62 - 18,
duration: 2200,
yoyo: true,
repeat: -1,
ease: 'Sine.easeInOut',
});
// Subtilen glow pod Kai-em (shadow/aura)
this.kaiAura = this.add.ellipse(W * 0.78, H * 0.625, 180, 30, 0x44ffcc, 0.18)
.setDepth(9);
this.tweens.add({
targets: this.kaiAura,
scaleX: { from: 0.85, to: 1.15 },
alpha: { from: 0.18, to: 0.08 },
duration: 2200,
yoyo: true,
repeat: -1,
ease: 'Sine.easeInOut',
});
// ─────────────────────────────────────────────────────────────────
// NASLOV IGRE
// ─────────────────────────────────────────────────────────────────
// Subtitle zgoraj
this.add.text(W * 0.32, H * 0.12, 'KRVAVA ŽETEV', {
fontFamily: '"Georgia", serif',
fontSize: '28px',
color: '#cc6666',
letterSpacing: 12,
stroke: '#000000',
strokeThickness: 4,
}).setOrigin(0.5).setDepth(10).setAlpha(0.85);
// Glavni naslov
const title = this.add.text(W * 0.32, H * 0.22, 'NOVA\nFARMA', {
fontFamily: '"Georgia", serif',
fontSize: '130px',
fontStyle: 'bold',
color: '#e8d5a3',
stroke: '#1a0a00',
strokeThickness: 14,
lineSpacing: -10,
align: 'center',
}).setOrigin(0.5).setDepth(10);
// Glow pulse na naslovu
this.tweens.add({
targets: title,
alpha: { from: 1.0, to: 0.85 },
duration: 2800,
yoyo: true,
repeat: -1,
ease: 'Sine.easeInOut',
});
// Tagline pod naslovom
this.add.text(W * 0.32, H * 0.41, '"Preživi. Najdi jo. Ne pozabi."', {
fontFamily: '"Georgia", serif',
fontStyle: 'italic',
fontSize: '22px',
color: '#8899aa',
stroke: '#000000',
strokeThickness: 3,
}).setOrigin(0.5).setDepth(10);
// Horizontal divider
const div = this.add.graphics().setDepth(10);
div.lineStyle(1, 0x44ffcc, 0.35);
div.lineBetween(W * 0.12, H * 0.45, W * 0.52, H * 0.45);
// ─────────────────────────────────────────────────────────────────
// GUMBI
// ─────────────────────────────────────────────────────────────────
const BTN_X = W * 0.32;
const BTN_Y_START = H * 0.54;
const BTN_GAP = 95;
const buttons = [
{ label: '▶ IGRAJ', id: 'play', color: '#44ffcc', hColor: '#ffffff' },
{ label: '⚙ NASTAVITVE', id: 'settings', color: '#aabbcc', hColor: '#ffffff' },
{ label: '✕ IZHOD', id: 'quit', color: '#cc6666', hColor: '#ff8888' },
];
this._btns = [];
buttons.forEach((b, i) => {
const y = BTN_Y_START + i * BTN_GAP;
// Gumb ozadje (Graphics)
const bg = this.add.graphics().setDepth(11);
const drawBg = (hover) => {
bg.clear();
bg.fillStyle(hover ? 0x1a2a20 : 0x0d1510, hover ? 0.95 : 0.80);
bg.fillRoundedRect(BTN_X - 190, y - 28, 380, 56, 12);
bg.lineStyle(hover ? 2 : 1, hover ? 0x44ffcc : 0x224433, hover ? 1.0 : 0.5);
bg.strokeRoundedRect(BTN_X - 190, y - 28, 380, 56, 12);
};
drawBg(false);
// Gumb tekst
const txt = this.add.text(BTN_X, y, b.label, {
fontFamily: '"Arial Black", Arial',
fontSize: '26px',
color: b.color,
stroke: '#000000',
strokeThickness: 4,
letterSpacing: 3,
}).setOrigin(0.5).setDepth(12);
// Hit zone
const zone = this.add.zone(BTN_X, y, 380, 56)
.setDepth(13)
.setInteractive({ cursor: 'pointer' });
zone.on('pointerover', () => {
drawBg(true);
txt.setColor(b.hColor).setScale(1.05);
// Puščica
this.tweens.add({ targets: txt, x: BTN_X + 8, duration: 120, ease: 'Quad.out' });
});
zone.on('pointerout', () => {
drawBg(false);
txt.setColor(b.color).setScale(1.0);
this.tweens.add({ targets: txt, x: BTN_X, duration: 120, ease: 'Quad.out' });
});
zone.on('pointerdown', () => this._onButton(b.id));
this._btns.push({ bg, txt, zone });
});
// ─────────────────────────────────────────────────────────────────
// TRIAL BADGE (levo zgoraj)
// ─────────────────────────────────────────────────────────────────
const trialBadge = this.add.image(30, 22, 'badge_trial')
.setOrigin(0, 0)
.setScale(0.32)
.setDepth(20);
this.tweens.add({
targets: trialBadge,
y: 28,
duration: 1600,
yoyo: true,
repeat: -1,
ease: 'Sine.easeInOut',
});
// Purchase badge (desno spodaj, majhen)
this.add.image(W - 20, H - 20, 'badge_buy')
.setOrigin(1, 1)
.setScale(0.28)
.setDepth(20)
.setInteractive({ cursor: 'pointer' })
.on('pointerover', function () { this.setScale(0.31); })
.on('pointerout', function () { this.setScale(0.28); });
// ─────────────────────────────────────────────────────────────────
// VERZIJA + AVTOR (spodaj levo)
// ─────────────────────────────────────────────────────────────────
this.add.text(20, H - 20, 'v0.1.0-DEMO | © 2026 HipoDevil666', {
fontFamily: 'Arial',
fontSize: '14px',
color: '#445544',
}).setOrigin(0, 1).setDepth(20);
// ─────────────────────────────────────────────────────────────────
// CONTROLS HINT (spodaj desno)
// ─────────────────────────────────────────────────────────────────
this.add.text(W - 20, H - 20, 'WASD Gibanje | E Interakcija | SPACE Alfa Moč', {
fontFamily: 'Arial',
fontSize: '14px',
color: '#334433',
}).setOrigin(1, 1).setDepth(20);
// ─────────────────────────────────────────────────────────────────
// FADE IN ob vstopu
// ─────────────────────────────────────────────────────────────────
this.cameras.main.fadeIn(800, 0, 0, 0);
// Akumuliraj čas za animacije
this._t = 0;
}
// ─────────────────────────────────────────────────────────────────────
// GUMB LOGIKA
// ─────────────────────────────────────────────────────────────────────
_onButton(id) {
if (id === 'play') {
this.cameras.main.fadeOut(600, 0, 0, 0);
this.cameras.main.once('camerafadeoutcomplete', () => {
this.scene.start('GrassSceneClean');
});
}
else if (id === 'settings') {
// TODO: SettingsScene
this._showToast('Nastavitve kmalu! 🔧');
}
else if (id === 'quit') {
// Electron quit
if (typeof window !== 'undefined' && window.electronAPI) {
window.electronAPI.quit();
} else if (typeof process !== 'undefined') {
window.close();
}
}
}
// Kratko sporočilo na zaslonu
_showToast(msg) {
const W = this.cameras.main.width;
const toast = this.add.text(W / 2, 80, msg, {
fontFamily: 'Arial',
fontSize: '22px',
color: '#44ffcc',
stroke: '#000',
strokeThickness: 3,
backgroundColor: '#0a1a10cc',
padding: { x: 18, y: 8 },
}).setOrigin(0.5).setDepth(100).setAlpha(0);
this.tweens.add({
targets: toast,
alpha: { from: 0, to: 1 },
y: 70,
duration: 300,
ease: 'Quad.out',
onComplete: () => {
this.time.delayedCall(1400, () => {
this.tweens.add({
targets: toast,
alpha: 0,
duration: 400,
onComplete: () => toast.destroy(),
});
});
}
});
}
// ─────────────────────────────────────────────────────────────────────
// UPDATE
// ─────────────────────────────────────────────────────────────────────
update(time, delta) {
this._t += delta / 1000;
// Animiraj fireflye
this.fireflyGfx.clear();
for (const ff of this.fireflies) {
const pulse = 0.5 + 0.5 * Math.sin(this._t * ff.speed + ff.offset);
const a = 0.1 + pulse * 0.8;
const r = ff.r * (0.7 + 0.3 * pulse);
// Premik (leno lebdenje)
ff.x += Math.sin(this._t * 0.3 + ff.offset) * 0.4;
ff.y += Math.cos(this._t * 0.25 + ff.offset) * 0.3;
// Wrap
if (ff.x < 20) ff.x = this.cameras.main.width - 20;
if (ff.x > this.cameras.main.width - 20) ff.x = 20;
// Glow halo
this.fireflyGfx.fillStyle(ff.color, a * 0.15);
this.fireflyGfx.fillCircle(ff.x, ff.y, r * 3.5);
// Jedro
this.fireflyGfx.fillStyle(ff.color, a);
this.fireflyGfx.fillCircle(ff.x, ff.y, r);
}
}
}

View File

@@ -1,49 +1,646 @@
/**
* =============================================================
* UIScene.js — Nova Farma HUD Layer
* =============================================================
* Overlay scene (runs on top of GrassScene_Clean)
* Contains ALL UI elements:
*
* TOP LEFT: Trial Version badge + HP bar + energy bar
* TOP RIGHT: Weather widget (ura + vreme)
* BOT LEFT: Dan/Ura text
* BOT CENTER: Hotbar (7 slotov) + action button
* BOT RIGHT: Minimap compass ring
* POPUP: Inventory panel [I] toggle
* OVERLAY: Health Critical pulse (pri < 25% HP)
* =============================================================
*/
export default class UIScene extends Phaser.Scene {
constructor() {
super({ key: 'UIScene', active: false });
super({ key: 'UIScene', active: false }); // Zaženemo ročno iz GrassScene
// Stanje igre (posodablja se iz GrassScene prek eventos)
this.gameState = {
hp: 100,
maxHp: 100,
energy: 100,
maxEnergy: 100,
day: 1,
hour: 6,
minute: 0,
weather: 'rain', // 'sun', 'rain', 'storm', 'cloud'
inventoryOpen: false,
hotbarItems: [null, null, null, null, null, null, null], // 7 slotov
activeSlot: 0,
};
}
preload() {
this.load.path = 'assets/';
// Load UI Assets here (moved from GrassScene)
// ── Vse UI assete naložimo tukaj ──
this.load.image('ui_health_bar', 'DEMO_FAZA1/UI/health_bar.png');
this.load.image('ui_weather_widget', 'DEMO_FAZA1/UI/weather_widget.png');
this.load.image('ui_health_critical', 'DEMO_FAZA1/UI/health_critical.png');
this.load.image('ui_weather', 'DEMO_FAZA1/UI/weather_widget.png');
this.load.image('ui_minimap', 'DEMO_FAZA1/UI/minimap_frame.png');
this.load.image('ui_hotbar', 'DEMO_FAZA1/UI/hotbar_background.png');
this.load.image('ui_slot', 'DEMO_FAZA1/UI/inventory_slot.png');
this.load.image('ui_action_btn', 'DEMO_FAZA1/UI/action_btn.png');
this.load.image('ui_badge_trial', 'DEMO_FAZA1/UI/badge_trial.png');
this.load.image('ui_badge_buy', 'DEMO_FAZA1/UI/badge_purchase.png');
this.load.image('ui_inventory', 'DEMO_FAZA1/UI/inventory_panel.png');
this.load.image('ui_dialog', 'DEMO_FAZA1/UI/dialog_panel.png');
}
create() {
// Simple UI Layout
const width = this.cameras.main.width;
const height = this.cameras.main.height;
const PAD = 60; // Increased padding for better visibility
const W = this.cameras.main.width; // 1920
const H = this.cameras.main.height; // 1080
const PAD = 24;
// 0. TRIAL BADGE (Top Center - The "Hook")
this.add.image(width / 2, PAD, 'ui_badge_trial')
.setOrigin(0.5, 0)
.setScale(1.0) // Increased scale
.setDepth(100); // Ensure it's on top
// ─────────────────────────────────────────────────────────────────
// TOP LEFT — Trial badge + HP + Energy
// ─────────────────────────────────────────────────────────────────
// 1. Health (Top Left)
let health = this.add.image(PAD, PAD, 'ui_health_bar').setOrigin(0, 0).setScale(1.0);
// Trial Version Badge
this.trialBadge = this.add.image(PAD, PAD, 'ui_badge_trial')
.setOrigin(0, 0)
.setScale(0.38)
.setDepth(1000);
// 2. Weather (Top Right)
let weather = this.add.image(width - PAD, PAD, 'ui_weather_widget').setOrigin(1, 0).setScale(1.0);
// Badge lebdi animacija
this.tweens.add({
targets: this.trialBadge,
y: PAD + 6,
duration: 1800,
yoyo: true,
repeat: -1,
ease: 'Sine.easeInOut',
});
// 3. Minimap (Bottom Right)
let minimap = this.add.image(width - PAD, height - PAD, 'ui_minimap').setOrigin(1, 1).setScale(0.9);
// ── HP Bar (health_bar.png je vertikalni triptih: full/empty/empty) ─
// Spritesheet je 3 vrstice: top=HP full, mid=HP empty, bot=Energy
// Uporabljamo health_bar.png kot ozadje in rišemo fill čez njega
const HP_X = PAD + 10;
const HP_Y = PAD + 130;
const HP_W = 420;
const HP_H = 36;
// 4. Hotbar (Bottom Center)
let hotbar = this.add.image(width / 2, height - PAD, 'ui_hotbar').setOrigin(0.5, 1).setScale(1.0);
// Ozadje HP (temno rdeče)
this.hpBg = this.add.graphics().setDepth(1001);
this.hpBg.fillStyle(0x4a0000, 1.0);
this.hpBg.fillRoundedRect(HP_X, HP_Y, HP_W, HP_H, 8);
// 5. Action Button (Right of Hotbar)
let action = this.add.image(width / 2 + 400, height - PAD, 'ui_action_btn').setOrigin(0.5, 1).setScale(1.0);
// HP Fill (svetlo rdeča)
this.hpFill = this.add.graphics().setDepth(1002);
// Debug Text
this.add.text(width / 2, 50, 'HUD LAYER ACTIVE', {
fontSize: '24px', fill: '#00ff00', stroke: '#000', strokeThickness: 4
}).setOrigin(0.5);
// HP ikona (skull) kratko besedilo label
this.add.text(HP_X - 5, HP_Y + HP_H / 2, '❤️', { fontSize: '22px' })
.setOrigin(1, 0.5).setDepth(1003);
// HP vrednost text
this.hpText = this.add.text(HP_X + HP_W / 2, HP_Y + HP_H / 2, '100 / 100', {
fontFamily: 'Arial Black',
fontSize: '16px',
color: '#ffffff',
stroke: '#000000',
strokeThickness: 4,
}).setOrigin(0.5, 0.5).setDepth(1004);
// ── Energy Bar ─────────────────────────────────────────────────────
const EN_X = HP_X;
const EN_Y = HP_Y + HP_H + 8;
const EN_W = HP_W;
const EN_H = 24;
this.energyBg = this.add.graphics().setDepth(1001);
this.energyBg.fillStyle(0x1a1a00, 1.0);
this.energyBg.fillRoundedRect(EN_X, EN_Y, EN_W, EN_H, 6);
this.energyFill = this.add.graphics().setDepth(1002);
this.add.text(EN_X - 5, EN_Y + EN_H / 2, '⚡', { fontSize: '16px' })
.setOrigin(1, 0.5).setDepth(1003);
this.energyText = this.add.text(EN_X + EN_W / 2, EN_Y + EN_H / 2, '100%', {
fontFamily: 'Arial Black',
fontSize: '13px',
color: '#ffee44',
stroke: '#000000',
strokeThickness: 3,
}).setOrigin(0.5, 0.5).setDepth(1004);
// Shrani pozicije za redraw
this._hpBar = { x: HP_X, y: HP_Y, w: HP_W, h: HP_H };
this._enBar = { x: EN_X, y: EN_Y, w: EN_W, h: EN_H };
// Prvič nariši
this._redrawBars();
// ─────────────────────────────────────────────────────────────────
// TOP RIGHT — Weather Widget (ura + vreme)
// ─────────────────────────────────────────────────────────────────
this.weatherWidget = this.add.image(W - PAD, PAD, 'ui_weather')
.setOrigin(1, 0)
.setScale(0.55)
.setDepth(1000);
// Ura text (čez weather widget)
this.timeText = this.add.text(W - PAD - 62, PAD + 68, '06:00', {
fontFamily: 'Arial Black',
fontSize: '28px',
color: '#ff3333',
stroke: '#000000',
strokeThickness: 4,
}).setOrigin(0.5, 0.5).setDepth(1005);
// Dan label (pod uro)
this.dayText = this.add.text(W - PAD - 135, PAD + 10, 'DAN 1', {
fontFamily: 'Arial Black',
fontSize: '18px',
color: '#ffdd44',
stroke: '#000000',
strokeThickness: 4,
}).setOrigin(0.5, 0).setDepth(1005);
// ─────────────────────────────────────────────────────────────────
// BOTTOM CENTER — Hotbar (7 slotov)
// ─────────────────────────────────────────────────────────────────
const HOTBAR_SCALE = 0.72;
this.hotbarBg = this.add.image(W / 2, H - PAD, 'ui_hotbar')
.setOrigin(0.5, 1)
.setScale(HOTBAR_SCALE)
.setDepth(1000);
// Hotbar dimenzije (hotbar_background.png = ~1120×160px)
const HOTBAR_W = 1120 * HOTBAR_SCALE;
const HOTBAR_H = 160 * HOTBAR_SCALE;
const HOT_START_X = W / 2 - HOTBAR_W / 2 + 56;
const HOT_Y = H - PAD - HOTBAR_H / 2;
const SLOT_W = (HOTBAR_W - 112) / 7;
// Shrani za event listener
this._hotbarW = HOTBAR_W;
this._hotbarH = HOTBAR_H;
this._slotW = SLOT_W;
this._hotY = HOT_Y;
this.hotbarSlots = [];
for (let i = 0; i < 7; i++) {
const sx = HOT_START_X + i * SLOT_W + SLOT_W / 2;
// Slot number label
const numLabel = this.add.text(sx - SLOT_W * 0.35, HOT_Y - HOTBAR_H * 0.32, `${i + 1}`, {
fontFamily: 'Arial',
fontSize: '12px',
color: '#888888',
}).setOrigin(0, 0).setDepth(1002);
// Active highlight
const highlight = this.add.graphics().setDepth(1001);
if (i === this.gameState.activeSlot) {
highlight.lineStyle(3, 0xffdd00, 1.0);
highlight.strokeRoundedRect(
sx - SLOT_W / 2 + 4, HOT_Y - HOTBAR_H / 2 + 4,
SLOT_W - 8, HOTBAR_H - 8, 6
);
}
this.hotbarSlots.push({ x: sx, y: HOT_Y, highlight, numLabel });
}
// ── Action Button (desno od hotbara) ────────────────────────────
this.actionBtn = this.add.image(W / 2 + HOTBAR_W / 2 + 50, H - PAD - 40, 'ui_action_btn')
.setOrigin(0.5, 0.5)
.setScale(0.9)
.setDepth(1000)
.setInteractive({ cursor: 'pointer' });
this.actionBtn.on('pointerover', () => this.actionBtn.setTint(0xffdd88));
this.actionBtn.on('pointerout', () => this.actionBtn.clearTint());
// Crafting label
this.add.text(this.actionBtn.x, H - PAD + 2, 'CRAFTING', {
fontFamily: 'Arial',
fontSize: '13px',
color: '#aaaaaa',
stroke: '#000',
strokeThickness: 2,
}).setOrigin(0.5, 0).setDepth(1002);
// ─────────────────────────────────────────────────────────────────
// BOTTOM RIGHT — Minimap Compass
// ─────────────────────────────────────────────────────────────────
const MM_SCALE = 0.55;
const MM_SIZE = 256 * MM_SCALE; // compass je 256×256
this.minimapFrame = this.add.image(W - PAD, H - PAD, 'ui_minimap')
.setOrigin(1, 1)
.setScale(MM_SCALE)
.setDepth(1000);
// Minimap inner black fill (bg)
this.minimapBg = this.add.graphics().setDepth(999);
const mmCx = W - PAD - MM_SIZE / 2;
const mmCy = H - PAD - MM_SIZE / 2;
this.minimapBg.fillStyle(0x001a33, 0.85);
this.minimapBg.fillCircle(mmCx, mmCy, MM_SIZE / 2 - 12);
// Kai dot on minimap
this.minimapKaiDot = this.add.graphics().setDepth(1001);
this.minimapKaiDot.fillStyle(0x44ffaa, 1.0);
this.minimapKaiDot.fillCircle(mmCx, mmCy, 5);
// Store for update
this._mmCx = mmCx;
this._mmCy = mmCy;
this._mmR = MM_SIZE / 2 - 16;
// "MAP" label
this.add.text(mmCx, mmCy + MM_SIZE / 2 - 8, 'MAPA', {
fontFamily: 'Arial',
fontSize: '11px',
color: '#556655',
}).setOrigin(0.5, 1).setDepth(1002);
// ─────────────────────────────────────────────────────────────────
// INVENTORY POPUP (I tipka toggle)
// ─────────────────────────────────────────────────────────────────
const INV_SCALE = 0.75;
const INV_W = 600 * INV_SCALE;
const INV_H = 600 * INV_SCALE;
this.inventoryPanel = this.add.image(W / 2, H / 2, 'ui_inventory')
.setOrigin(0.5, 0.5)
.setScale(INV_SCALE)
.setDepth(5000)
.setVisible(false);
// Inventory title
this.inventoryTitle = this.add.text(W / 2, H / 2 - INV_H / 2 + 28, 'INVENTAR', {
fontFamily: 'Arial Black',
fontSize: '22px',
color: '#e8d5a3',
stroke: '#000000',
strokeThickness: 4,
}).setOrigin(0.5, 0.5).setDepth(5001).setVisible(false);
// Close hint
this.inventoryClose = this.add.text(W / 2, H / 2 + INV_H / 2 - 20, '[I] Zapri', {
fontFamily: 'Arial',
fontSize: '16px',
color: '#888888',
}).setOrigin(0.5, 1).setDepth(5001).setVisible(false);
// Dim overlay when inventory open
this.invDim = this.add.rectangle(W / 2, H / 2, W, H, 0x000000, 0.4)
.setDepth(4999)
.setVisible(false);
// [I] tipka toggle
this.input.keyboard.on('keydown-I', () => this.toggleInventory());
// ─────────────────────────────────────────────────────────────────
// HEALTH CRITICAL OVERLAY (pulse ko HP < 25%)
// ─────────────────────────────────────────────────────────────────
this.criticalOverlay = this.add.image(W / 2, H / 2, 'ui_health_critical')
.setOrigin(0.5, 0.5)
.setScale(Math.max(W / 512, H / 512)) // Pokrije ekran
.setAlpha(0)
.setDepth(4000);
// ─────────────────────────────────────────────────────────────────
// DIALOG BOX (spodaj, za govor NPCjev)
// ─────────────────────────────────────────────────────────────────
const DLG_W = 900;
const DLG_H = 160;
this.dialogPanel = this.add.image(W / 2, H - 20, 'ui_dialog')
.setOrigin(0.5, 1)
.setDisplaySize(DLG_W, DLG_H)
.setDepth(6000)
.setVisible(false);
this.dialogText = this.add.text(W / 2, H - 30, '', {
fontFamily: '"Courier New", monospace',
fontSize: '20px',
color: '#2a1a00',
wordWrap: { width: DLG_W - 80 },
align: 'left',
}).setOrigin(0.5, 1).setDepth(6001).setVisible(false);
this.dialogName = this.add.text(W / 2 - DLG_W / 2 + 40, H - DLG_H - 10, '', {
fontFamily: 'Arial Black',
fontSize: '18px',
color: '#ffdd88',
stroke: '#000000',
strokeThickness: 3,
}).setOrigin(0, 1).setDepth(6001).setVisible(false);
// ─────────────────────────────────────────────────────────────────
// KEYBOARD SHORTCUTS HINT (spodaj levo)
// ─────────────────────────────────────────────────────────────────
this.add.text(PAD, H - PAD, '[I] Inventar [B] Gradnja [ESC] Pavza', {
fontFamily: 'Arial',
fontSize: '14px',
color: '#444444',
stroke: '#000000',
strokeThickness: 2,
}).setOrigin(0, 1).setDepth(1000);
// ─────────────────────────────────────────────────────────────────
// VZPOSTAVI KOMUNIKACIJO Z GRASSSCENE
// ─────────────────────────────────────────────────────────────────
this.game.events.on('ui-update-hp', (hp) => {
this.gameState.hp = Phaser.Math.Clamp(hp, 0, this.gameState.maxHp);
this._redrawBars();
this._checkCritical();
});
this.game.events.on('ui-update-energy', (energy) => {
this.gameState.energy = Phaser.Math.Clamp(energy, 0, this.gameState.maxEnergy);
this._redrawBars();
});
this.game.events.on('ui-update-time', (day, hour, minute) => {
this.gameState.day = day;
this.gameState.hour = hour;
this.gameState.minute = minute;
this._updateTimeDisplay();
});
this.game.events.on('ui-show-dialog', (name, text) => {
this.showDialog(name, text);
});
this.game.events.on('ui-hide-dialog', () => {
this.hideDialog();
});
// ── Hotbar update iz InventorySystem ──────────────────────────────
// Hotbar item text labels (nad sloti)
this._hotbarItemTexts = [];
this._hotbarCountTexts = [];
for (let i = 0; i < 7; i++) {
const { x, y } = this.hotbarSlots[i];
const icon = this.add.text(x, y - 4, '', {
fontSize: '26px',
align: 'center',
}).setOrigin(0.5, 0.5).setDepth(1005);
const cnt = this.add.text(x + 16, y + 14, '', {
fontFamily: 'Arial Black',
fontSize: '11px',
color: '#ffdd44',
stroke: '#000000',
strokeThickness: 3,
}).setOrigin(0.5, 0.5).setDepth(1006);
this._hotbarItemTexts.push(icon);
this._hotbarCountTexts.push(cnt);
}
// Gold counter (spodaj levo od hotbara)
this.goldText = this.add.text(W / 2 - HOTBAR_W / 2, H - PAD - 85, '💰 0g', {
fontFamily: 'Arial Black',
fontSize: '16px',
color: '#ffd700',
stroke: '#000000',
strokeThickness: 4,
}).setOrigin(0, 0.5).setDepth(1005);
// Poslušaj hotbar updete iz InventorySystem
this.events.on('hotbarUpdate', (data) => {
const { slots, activeSlot, gold } = data;
// Posodobi slote
for (let i = 0; i < 7; i++) {
const slot = slots[i];
const hl = this.hotbarSlots[i].highlight;
hl.clear();
// Active slot highlight
const { x, y } = this.hotbarSlots[i];
const SW = this._slotW;
const SH = this._hotbarH;
if (i === activeSlot) {
hl.lineStyle(3, 0xffdd00, 1.0);
hl.strokeRoundedRect(
x - SW / 2 + 4, y - SH / 2 + 4,
SW - 8, SH - 8, 6
);
}
// Icon in count
if (slot) {
this._hotbarItemTexts[i].setText(slot.icon || '');
this._hotbarCountTexts[i].setText(
slot.stackable && slot.count > 0 ? `×${slot.count}` : ''
);
} else {
this._hotbarItemTexts[i].setText('');
this._hotbarCountTexts[i].setText('');
}
}
// Gold
if (this.goldText) this.goldText.setText(`💰 ${gold}g`);
});
// ── Time update iz DayNightSystem ─────────────────────────────────
this.events.on('timeChanged', (data) => {
if (this.timeText) {
this.timeText.setText(data.timeStr || '06:00');
}
if (this.dayText) {
this.dayText.setText(`DAN ${data.day}`);
}
});
// ── Water update iz WaterSystem ───────────────────────────────────
// Vedro indikator (blizu hotbara)
this.waterHUD = this.add.text(W / 2 + HOTBAR_W / 2 - 60, H - PAD - 85, '💧 3/5L', {
fontFamily: 'Arial Black',
fontSize: '14px',
color: '#44aaff',
stroke: '#000000',
strokeThickness: 3,
}).setOrigin(1, 0.5).setDepth(1005);
this.events.on('waterChanged', (data) => {
if (this.waterHUD) {
const pct = data.can / data.canMax;
const col = pct > 0.5 ? '#44aaff' : pct > 0.2 ? '#ffaa00' : '#ff4444';
this.waterHUD
.setText(`💧 ${data.can}/${data.canMax}L`)
.setColor(col);
}
});
// UIScene teče vzporedno z GrassScene, prinese se na vrh
this.scene.bringToTop();
console.log('✅ UIScene initialized with all elements!');
}
// =========================================================
// HELPERS
// =========================================================
/** Nariši HP in Energy bar fills */
_redrawBars() {
const { hp, maxHp, energy, maxEnergy } = this.gameState;
// HP bar
const hpPct = hp / maxHp;
const { x, y, w, h } = this._hpBar;
this.hpFill.clear();
// Barva glede na HP %
const hpColor = hpPct > 0.6 ? 0xcc2222 :
hpPct > 0.3 ? 0xdd7700 : 0xff0000;
this.hpFill.fillStyle(hpColor, 1.0);
this.hpFill.fillRoundedRect(x, y, Math.max(4, w * hpPct), h, 8);
// HP outer border
this.hpFill.lineStyle(2, 0x660000, 1.0);
this.hpFill.strokeRoundedRect(x, y, w, h, 8);
this.hpText.setText(`${Math.round(hp)} / ${maxHp}`);
// Energy bar
const enPct = energy / maxEnergy;
const e = this._enBar;
this.energyFill.clear();
const enColor = enPct > 0.5 ? 0xddcc00 :
enPct > 0.2 ? 0xdd8800 : 0xff4400;
this.energyFill.fillStyle(enColor, 1.0);
this.energyFill.fillRoundedRect(e.x, e.y, Math.max(4, e.w * enPct), e.h, 6);
this.energyFill.lineStyle(2, 0x444400, 1.0);
this.energyFill.strokeRoundedRect(e.x, e.y, e.w, e.h, 6);
this.energyText.setText(`${Math.round(energy)}%`);
}
/** Preveri kritičen HP in prikaži overlay */
_checkCritical() {
const pct = this.gameState.hp / this.gameState.maxHp;
if (pct < 0.25) {
// Pulse animacija
if (!this._criticalTween || !this._criticalTween.isPlaying()) {
this._criticalTween = this.tweens.add({
targets: this.criticalOverlay,
alpha: { from: 0, to: 0.55 },
duration: 600,
yoyo: true,
repeat: -1,
ease: 'Sine.easeInOut',
});
}
} else {
// Ugasni
if (this._criticalTween) {
this._criticalTween.stop();
this._criticalTween = null;
}
this.criticalOverlay.setAlpha(0);
}
}
/** Posodobi uro + dan display */
_updateTimeDisplay() {
const { day, hour, minute } = this.gameState;
const hStr = String(hour).padStart(2, '0');
const mStr = String(minute).padStart(2, '0');
this.timeText.setText(`${hStr}:${mStr}`);
this.dayText.setText(`DAN ${day}`);
// Ura barva: rdeča ponoči, rumena podnevi
const isNight = hour >= 21 || hour < 5;
this.timeText.setColor(isNight ? '#8899ff' : '#ff3333');
}
/** Toggle Inventory popup */
toggleInventory() {
this.gameState.inventoryOpen = !this.gameState.inventoryOpen;
const open = this.gameState.inventoryOpen;
this.inventoryPanel.setVisible(open);
this.inventoryTitle.setVisible(open);
this.inventoryClose.setVisible(open);
this.invDim.setVisible(open);
if (open) {
// Slide in animacija
this.inventoryPanel.setAlpha(0).setScale(0.65);
this.tweens.add({
targets: this.inventoryPanel,
alpha: 1,
scale: 0.75,
duration: 200,
ease: 'Back.out',
});
}
}
/** Prikaži dialog box (typewriter) */
showDialog(name, text) {
this.dialogPanel.setVisible(true);
this.dialogName.setVisible(true).setText(name || '');
this.dialogText.setVisible(true).setText('');
// Typewriter efekt
let idx = 0;
if (this._typewriterTimer) this._typewriterTimer.remove();
this._typewriterTimer = this.time.addEvent({
delay: 35,
repeat: text.length - 1,
callback: () => {
this.dialogText.setText(text.substring(0, ++idx));
}
});
}
/** Skrij dialog box */
hideDialog() {
if (this._typewriterTimer) this._typewriterTimer.remove();
this.dialogPanel.setVisible(false);
this.dialogText.setVisible(false);
this.dialogName.setVisible(false);
}
// =========================================================
// UPDATE — Minimap Kai dot
// =========================================================
update() {
// Posodaibaj Kai dot na minimapu
const grassScene = this.scene.get('GrassSceneClean');
if (!grassScene || !grassScene.kai) return;
const kai = grassScene.kai;
// Svet bounds (iz GrassScene)
const worldW = grassScene.worldWidth || 10400;
const worldH = grassScene.worldHeight || 10400;
// Normaliziraj Kai pozicijo [0..1]
const nx = kai.x / worldW;
const ny = kai.y / worldH;
// Mapiraj na minimap krog
const angle = Math.atan2(ny - 0.5, nx - 0.5);
const dist = Math.min(Math.sqrt((nx - 0.5) ** 2 + (ny - 0.5) ** 2) * 2, 1);
const dotX = this._mmCx + Math.cos(angle) * dist * this._mmR;
const dotY = this._mmCy + Math.sin(angle) * dist * this._mmR;
this.minimapKaiDot.clear();
this.minimapKaiDot.fillStyle(0x44ffaa, 1.0);
this.minimapKaiDot.fillCircle(dotX, dotY, 5);
// Pulsirajoč rob na dotiku
this.minimapKaiDot.lineStyle(2, 0xffffff, 0.6);
this.minimapKaiDot.strokeCircle(dotX, dotY, 7);
}
}

View File

@@ -2,43 +2,38 @@
* ============================================================
* BUILDING SYSTEM — Nova Farma
* ============================================================
* Handles:
* - Build Mode (toggle with [B] key or UI button)
* - Ghost preview image that follows the cursor
* - Click to place building at cursor position
* - Y-sort depth so Kai walks behind / in front of buildings
* - Physics static body so Kai cannot walk through walls
* - Side-panel Build UI with selectable building icons
* [B] = vklopi / izklopi build mode
* [Esc] = izhod
* Klik = postavi stavbo
*
* UI meni je ODSTRANJEN — stavbe se dodajajo programsko
* prek: buildingSystem.selectBuilding('sotor')
* ============================================================
*/
export default class BuildingSystem {
/**
* @param {Phaser.Scene} scene - The scene this system runs in
* @param {Phaser.Physics.Arcade.Sprite} kai - Player sprite reference
* @param {Phaser.Scene} scene
* @param {Phaser.Physics.Arcade.Sprite} kai
*/
constructor(scene, kai) {
this.scene = scene;
this.kai = kai;
// Build mode state
this.active = false; // Is build mode ON?
this.selectedKey = null; // Currently selected building texture key
// Ghost preview sprite
this.active = false;
this.selectedKey = null;
this.ghost = null;
this.placed = [];
// Group of all placed buildings (static physics bodies)
// Static physics group za vse postavljene stavbe
this.buildingsGroup = scene.physics.add.staticGroup();
// Catalogue of buildable items
// { key, label, colliderW, colliderH, colliderOffsetX, colliderOffsetY, scale }
// Scale is % of Kai height (64px).
// Katalog stavb — tukaj dodajamo nove
this.catalogue = [
{
key: 'sotor',
label: 'Šotor',
scale: 2.5, // 2.5× Kai 160px tall
scale: 2.5, // 2.5× Kai (64px) = 160px
colliderW: 160,
colliderH: 40,
colliderOffsetX: -80,
@@ -46,8 +41,8 @@ export default class BuildingSystem {
},
{
key: 'campfire',
label: 'Ogenj',
scale: 1.0, // Same height as Kai
label: 'Taborni ogenj',
scale: 1.0,
colliderW: 50,
colliderH: 20,
colliderOffsetX: -25,
@@ -64,7 +59,7 @@ export default class BuildingSystem {
},
{
key: 'foundation_concrete',
label: 'Temelj',
label: 'Betonski temelj',
scale: 2.0,
colliderW: 120,
colliderH: 20,
@@ -73,160 +68,37 @@ export default class BuildingSystem {
},
];
// Build UI panel (DOM container)
this.panel = null;
// Track placed buildings for saving/loading later
this.placed = [];
this._setupUI();
this._setupInput();
}
// ==========================================
// UI PANEL (Fixed to screen, top-right)
// ==========================================
_setupUI() {
const scene = this.scene;
const CAM_W = scene.cameras.main.width;
const PANEL_W = 80;
const PANEL_PADDING = 8;
const BTN_SIZE = 60;
const BTN_GAP = 12;
// ── Background Panel ─────────────────────────────────────────────
this.panel = scene.add.graphics()
.setScrollFactor(0)
.setDepth(20000);
const panelH = this.catalogue.length * (BTN_SIZE + BTN_GAP) + PANEL_PADDING * 2 + 40;
const panelX = CAM_W - PANEL_W - 16;
const panelY = 120;
// Dark translucent panel
this.panel.fillStyle(0x0a0a0f, 0.85);
this.panel.fillRoundedRect(panelX, panelY, PANEL_W, panelH, 10);
this.panel.lineStyle(2, 0x44ffaa, 0.6);
this.panel.strokeRoundedRect(panelX, panelY, PANEL_W, panelH, 10);
// "Build" label
scene.add.text(
panelX + PANEL_W / 2,
panelY + 10,
'[B]',
{ fontSize: '13px', color: '#44ffaa', fontFamily: 'Arial', align: 'center' }
).setOrigin(0.5, 0).setScrollFactor(0).setDepth(20001);
// ── Building Buttons ──────────────────────────────────────────────
this.catalogue.forEach((item, i) => {
const btnX = panelX + PANEL_PADDING;
const btnY = panelY + 40 + i * (BTN_SIZE + BTN_GAP);
const btnW = PANEL_W - PANEL_PADDING * 2;
const btnH = BTN_SIZE;
// Button background
const btnBg = scene.add.graphics()
.setScrollFactor(0)
.setDepth(20001);
btnBg.fillStyle(0x1a2a1a, 0.9);
btnBg.fillRoundedRect(btnX, btnY, btnW, btnH, 6);
btnBg.lineStyle(1, 0x224422, 1.0);
btnBg.strokeRoundedRect(btnX, btnY, btnW, btnH, 6);
// Icon (preview of the building texture)
const icon = scene.add.image(
btnX + btnW / 2,
btnY + btnH / 2 - 8,
item.key
)
.setScrollFactor(0)
.setDepth(20002)
.setDisplaySize(btnW - 8, btnH - 20);
// Label text
const label = scene.add.text(
btnX + btnW / 2,
btnY + btnH - 10,
item.label,
{ fontSize: '9px', color: '#aaffcc', fontFamily: 'Arial', align: 'center' }
).setOrigin(0.5, 0.5).setScrollFactor(0).setDepth(20002);
// Invisible hit zone for click detection
const hitZone = scene.add.zone(btnX, btnY, btnW, btnH)
.setOrigin(0, 0)
.setScrollFactor(0)
.setDepth(20003)
.setInteractive({ cursor: 'pointer' });
hitZone.on('pointerover', () => {
btnBg.clear();
btnBg.fillStyle(0x2a4a2a, 1.0);
btnBg.fillRoundedRect(btnX, btnY, btnW, btnH, 6);
btnBg.lineStyle(2, 0x44ffaa, 1.0);
btnBg.strokeRoundedRect(btnX, btnY, btnW, btnH, 6);
});
hitZone.on('pointerout', () => {
const isSelected = this.selectedKey === item.key;
btnBg.clear();
btnBg.fillStyle(isSelected ? 0x1a4a1a : 0x1a2a1a, 0.9);
btnBg.fillRoundedRect(btnX, btnY, btnW, btnH, 6);
btnBg.lineStyle(isSelected ? 2 : 1, isSelected ? 0x44ffaa : 0x224422, 1.0);
btnBg.strokeRoundedRect(btnX, btnY, btnW, btnH, 6);
});
hitZone.on('pointerdown', () => {
this.selectBuilding(item.key);
});
// Store references to update highlight state
item._btnBg = btnBg;
item._btnX = btnX;
item._btnY = btnY;
item._btnW = btnW;
item._btnH = btnH;
});
}
// ==========================================
// KEYBOARD: [B] toggles build mode
// KEYBOARD: [B] toggle, [Esc] izhod
// ==========================================
_setupInput() {
const scene = this.scene;
// Toggle build mode with [B]
scene.input.keyboard.on('keydown-B', () => {
if (this.active) {
this.deactivate();
} else {
this.activate();
}
this.scene.input.keyboard.on('keydown-B', () => {
this.active ? this.deactivate() : this.activate();
});
// Cancel / exit build mode with [Escape]
scene.input.keyboard.on('keydown-ESC', () => {
this.scene.input.keyboard.on('keydown-ESC', () => {
if (this.active) this.deactivate();
});
}
// ==========================================
// ACTIVATE BUILD MODE
// ACTIVATE
// ==========================================
activate() {
this.active = true;
// If nothing selected yet, default to first item
if (!this.selectedKey) {
this.selectBuilding(this.catalogue[0].key);
} else {
this._createGhost(this.selectedKey);
}
// Show tip text
if (!this._modeTip) {
this._modeTip = this.scene.add.text(
this.scene.cameras.main.width / 2,
20,
this.scene.cameras.main.width / 2, 20,
'🏗️ NAČIN GRADNJE | Klik = postavi | B / Esc = izhod',
{
fontSize: '16px',
@@ -235,32 +107,22 @@ export default class BuildingSystem {
strokeThickness: 3,
fontFamily: 'Arial',
backgroundColor: '#0a0a0f99',
padding: { x: 10, y: 4 }
padding: { x: 10, y: 4 },
}
)
.setOrigin(0.5, 0)
.setScrollFactor(0)
.setDepth(20010);
).setOrigin(0.5, 0).setScrollFactor(0).setDepth(20010);
}
this._modeTip.setVisible(true);
}
// ==========================================
// DEACTIVATE BUILD MODE
// DEACTIVATE
// ==========================================
deactivate() {
this.active = false;
this.selectedKey = null;
if (this.ghost) {
this.ghost.destroy();
this.ghost = null;
}
if (this.ghost) { this.ghost.destroy(); this.ghost = null; }
if (this._modeTip) this._modeTip.setVisible(false);
// Reset all button highlights
this.catalogue.forEach(item => this._drawBtn(item, false));
}
// ==========================================
@@ -270,77 +132,49 @@ export default class BuildingSystem {
this.selectedKey = key;
if (!this.active) this.activate();
// Destroy old ghost
if (this.ghost) {
this.ghost.destroy();
this.ghost = null;
}
if (this.ghost) { this.ghost.destroy(); this.ghost = null; }
this._createGhost(key);
// Update button highlights
this.catalogue.forEach(item => {
this._drawBtn(item, item.key === key);
});
}
_drawBtn(item, selected) {
if (!item._btnBg) return;
item._btnBg.clear();
item._btnBg.fillStyle(selected ? 0x1a4a1a : 0x1a2a1a, 0.9);
item._btnBg.fillRoundedRect(item._btnX, item._btnY, item._btnW, item._btnH, 6);
item._btnBg.lineStyle(selected ? 2 : 1, selected ? 0x44ffaa : 0x224422, 1.0);
item._btnBg.strokeRoundedRect(item._btnX, item._btnY, item._btnW, item._btnH, 6);
}
// ==========================================
// GHOST PREVIEW SPRITE
// GHOST PREVIEW
// ==========================================
_createGhost(key) {
const def = this.catalogue.find(c => c.key === key);
if (!def) return;
const KAI_H = 64; // px
const KAI_H = 64;
const targetH = KAI_H * def.scale;
const texture = this.scene.textures.get(key);
const srcH = texture.getSourceImage().height;
const srcH = this.scene.textures.get(key).getSourceImage().height;
const scale = targetH / srcH;
this.ghost = this.scene.add.image(0, 0, key)
.setScale(scale)
.setOrigin(0.5, 1.0) // Feet at cursor
.setOrigin(0.5, 1.0)
.setAlpha(0.6)
.setTint(0x44ffaa) // Green tint = valid placement
.setTint(0x44ffaa)
.setDepth(99999);
// Make ghost follow pointer in update()
}
// ==========================================
// UPDATE (call from scene's update())
// UPDATE — klic iz scene.update()
// ==========================================
update(pointer) {
if (!this.active || !this.ghost) return;
// Ghost follows world-space cursor
const worldX = pointer.worldX;
const worldY = pointer.worldY;
this.ghost.setPosition(worldX, worldY);
const wx = pointer.worldX;
const wy = pointer.worldY;
this.ghost.setPosition(wx, wy);
// Check if placement is on the island (within physics bounds)
const bounds = this.scene.physics.world.bounds;
const onIsland = (
worldX > bounds.x &&
worldX < bounds.x + bounds.width &&
worldY > bounds.y &&
worldY < bounds.y + bounds.height
);
// Green = valid, Red = invalid
this.ghost.setTint(onIsland ? 0x44ffaa : 0xff4444);
// Zelena = na otoku, Rdeča = zunaj
const b = this.scene.physics.world.bounds;
const ok = wx > b.x && wx < b.x + b.width &&
wy > b.y && wy < b.y + b.height;
this.ghost.setTint(ok ? 0x44ffaa : 0xff4444);
}
// ==========================================
// PLACE BUILDING (called on click)
// PLACE BUILDING (ob kliku)
// ==========================================
placeBuilding(worldX, worldY) {
if (!this.active || !this.selectedKey) return;
@@ -348,87 +182,73 @@ export default class BuildingSystem {
const def = this.catalogue.find(c => c.key === this.selectedKey);
if (!def) return;
// Validate placement is on island
const bounds = this.scene.physics.world.bounds;
if (
worldX <= bounds.x || worldX >= bounds.x + bounds.width ||
worldY <= bounds.y || worldY >= bounds.y + bounds.height
) {
// Flash ghost red
this.scene.tweens.add({
targets: this.ghost,
alpha: { from: 0.9, to: 0.3 },
duration: 100,
yoyo: true,
repeat: 2,
});
// Validacija — mora biti na otoku
const b = this.scene.physics.world.bounds;
if (worldX <= b.x || worldX >= b.x + b.width ||
worldY <= b.y || worldY >= b.y + b.height) {
// Flash rdeče
if (this.ghost) {
this.scene.tweens.add({
targets: this.ghost,
alpha: { from: 0.9, to: 0.2 },
duration: 80,
yoyo: true,
repeat: 2,
});
}
return;
}
// ── Calculate final scale ─────────────────────────────────────────
// Izračun scale
const KAI_H = 64;
const targetH = KAI_H * def.scale;
const texture = this.scene.textures.get(this.selectedKey);
const srcH = texture.getSourceImage().height;
const scale = targetH / srcH;
// ── Create placed sprite ──────────────────────────────────────────
// Ustvari static physics image
const building = this.scene.physics.add.staticImage(worldX, worldY, this.selectedKey)
.setScale(scale)
.setOrigin(0.5, 1.0);
.setOrigin(0.5, 1.0)
.setDepth(worldY);
// Y-sort: depth is based on the foot Y position (worldY) so Kai sorts properly
building.setDepth(worldY);
// ── Physics collider body ─────────────────────────────────────────
// Adjust physics body to cover just the base of the building
const bodyW = def.colliderW;
const bodyH = def.colliderH;
const offsetX = srcH * scale * 0 + def.colliderOffsetX; // center-ish
const offsetY = def.colliderOffsetY; // above feet
building.body.setSize(bodyW, bodyH);
// Physics collider body (samo baza stavbe)
building.body.setSize(def.colliderW, def.colliderH);
building.body.setOffset(
(texture.getSourceImage().width * scale) / 2 + def.colliderOffsetX,
srcH * scale + def.colliderOffsetY
);
building.body.reset(worldX, worldY);
// ── Add to static group & collider ───────────────────────────────
// Collider med Kai in stavbo
this.buildingsGroup.add(building, true);
this.scene.physics.add.collider(this.kai, building);
// ── Spawn animation ───────────────────────────────────────────────
// Spawn animacija
building.setAlpha(0);
this.scene.tweens.add({
targets: building,
alpha: 1,
scaleX: scale,
scaleY: scale,
duration: 300,
duration: 280,
ease: 'Back.out',
});
// Save for later
this.placed.push({ key: this.selectedKey, x: worldX, y: worldY });
// ── Update depth every frame (for Y-sort) ────────────────────────
// Store ref so update() can sort depths
building._isBuilding = true;
this.scene._buildingSprites = this.scene._buildingSprites || [];
this.scene._buildingSprites.push(building);
// Small flash feedback
const flash = this.scene.add.rectangle(worldX, worldY - targetH * 0.5, 80, 80, 0x44ffaa, 0.7)
// Flash feedback
const flash = this.scene.add.rectangle(worldX, worldY - targetH * 0.5, 80, 80, 0x44ffaa, 0.6)
.setDepth(99998);
this.scene.tweens.add({
targets: flash,
alpha: 0,
scaleX: 2,
scaleY: 2,
duration: 400,
alpha: 0, scaleX: 2, scaleY: 2,
duration: 350,
onComplete: () => flash.destroy(),
});
console.log(`🏗️ Placed: ${this.selectedKey} at (${Math.round(worldX)}, ${Math.round(worldY)})`);
// Shrani referenco za Y-sort v update()
this.scene._buildingSprites = this.scene._buildingSprites || [];
this.scene._buildingSprites.push(building);
this.placed.push({ key: this.selectedKey, x: worldX, y: worldY });
console.log(`🏗️ Placed: ${this.selectedKey} @ (${Math.round(worldX)}, ${Math.round(worldY)})`);
}
}

View File

@@ -0,0 +1,441 @@
/**
* ============================================================
* DayNightSystem.js — Nova Farma
* ============================================================
* Realen dan/noč cikel z vsemi hook-i:
*
* - Vsaka sekunda realna = 1 minuta v igri (24 min real = 1 dan)
* - 06:00 = Jutro (svetlo, ptice)
* - 12:00 = Poldne (svetlo max)
* - 20:00 = Večer (tema pada, oranžna)
* - 22:00 = Noč (modra tema, zvezde)
* - 00:00 = Polnoč → nov dan trigger
*
* Events emittirani:
* - 'dayChanged' { day } → FarmingSystem.onNewDay(day)
* - 'morningArrived' { day } → Budnica / ptice / energija reset
* - 'nightArrived' { day } → Zombiji aktiv
* - 'hourChanged' { hour, minute } → UIScene clock update
*
* Spanje:
* - [Z] blizu spalne vreče/šotora → Sleep → skok na 06:00 naslednjega dne
* ============================================================
*/
export default class DayNightSystem {
/**
* @param {Phaser.Scene} scene
* @param {object} options
*/
constructor(scene, options = {}) {
this.scene = scene;
// Čas
this.day = 1;
this.hour = 6; // Začne ob 6:00
this.minute = 0;
// Hitrost: 1 real sekunda = N minut v igri
// Default: 24 real minut = 1 dan → 1 real sec = 1 igra minuta
this.gameMinutesPerRealSecond = options.speed || 1;
// Interaktivni objekti za spanje
this.sleepObjects = []; // { x, y, sprite, type }
this.isSleeping = false;
this.interactRange = 90;
// Input [Z] za spanje
this.keyZ = scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Z);
// Ambient Graphics (overlay za noč)
this.ambientOverlay = scene.add.graphics()
.setScrollFactor(0)
.setDepth(4500); // Pod UIScene (depth 5000+), nad svetom
// Star particles za noč
this.stars = [];
this._initStars();
// Čas tracking
this._accumMs = 0;
// Callback cache
this._lastHour = -1;
this._lastDay = -1;
// Sleep prompt text
this._sleepPrompt = null;
this._highlightGfx = scene.add.graphics().setDepth(9002);
console.log(`🌅 DayNightSystem initialized — Dan ${this.day}, ${this._timeStr()}`);
}
// ─────────────────────────────────────────────────────────────────────────
// UPDATE — Kliči iz scene.update() z delta
// ─────────────────────────────────────────────────────────────────────────
update(delta, kai) {
if (this.isSleeping) return;
// ── Tick čas ──
this._accumMs += delta;
const msPerGameMin = 1000 / this.gameMinutesPerRealSecond;
while (this._accumMs >= msPerGameMin) {
this._accumMs -= msPerGameMin;
this._tickMinute();
}
// ── Ambient svetloba ──
this._updateAmbient();
// ── Zvezde ──
this._updateStars();
// ── Sleep interakcija ──
this._highlightGfx.clear();
const nearSleep = this._getSleepNear(kai);
if (nearSleep) {
this._highlightGfx.lineStyle(2, 0xaaaaff, 0.8);
this._highlightGfx.strokeCircle(nearSleep.x, nearSleep.y, 50);
this._showSleepPrompt(nearSleep);
} else {
this._hideSleepPrompt();
}
if (Phaser.Input.Keyboard.JustDown(this.keyZ)) {
if (nearSleep) {
this.sleep();
}
}
}
// ─────────────────────────────────────────────────────────────────────────
// TICK minuta
// ─────────────────────────────────────────────────────────────────────────
_tickMinute() {
this.minute++;
if (this.minute >= 60) {
this.minute = 0;
this.hour++;
this._onHourChange();
}
if (this.hour >= 24) {
this.hour = 0;
this._onMidnight();
}
// Emit UI update vsako minuto
this.scene.events.emit('timeChanged', {
day: this.day,
hour: this.hour,
minute: this.minute,
timeStr: this._timeStr(),
});
}
// ─────────────────────────────────────────────────────────────────────────
// HOOK — Vsaka ura
// ─────────────────────────────────────────────────────────────────────────
_onHourChange() {
this.scene.events.emit('hourChanged', {
day: this.day,
hour: this.hour,
minute: this.minute,
});
// Posebni uri:
if (this.hour === 6) {
// Jutro!
this.scene.events.emit('morningArrived', { day: this.day });
console.log(`🌅 Jutro! Dan ${this.day}, 06:00`);
}
if (this.hour === 20) {
// Večer
this.scene.events.emit('eveningArrived', { day: this.day });
console.log(`🌆 Večer — zombiji se bujajo...`);
}
if (this.hour === 22) {
// Noč
this.scene.events.emit('nightArrived', { day: this.day });
console.log(`🌙 Noč! Be careful, Kai...`);
}
}
// ─────────────────────────────────────────────────────────────────────────
// HOOK — Polnoč → Nov dan
// ─────────────────────────────────────────────────────────────────────────
_onMidnight() {
this.day++;
console.log(`🌙 Polnoč → Dan ${this.day}!`);
// ── Farming tick ──
this.scene.events.emit('dayChanged', { day: this.day });
// ── Rain check (30% chance) ──
if (Math.random() < 0.3) {
this.scene.events.emit('rainDay', { day: this.day });
}
}
// ─────────────────────────────────────────────────────────────────────────
// SPANJE — Skip na 06:00 naslednjega dne
// ─────────────────────────────────────────────────────────────────────────
sleep() {
if (this.isSleeping) return;
// Dovoli spanje samo med 20:00 in 24:00 ali 00:00-06:00
if (this.hour >= 6 && this.hour < 20) {
this._showFloatingText(
this.scene.cameras.main.width / 2,
this.scene.cameras.main.height / 2 - 60,
'😴 Preuranjeno za spati! (po 20:00)',
'#ffaa44', 20, true
);
return;
}
this.isSleeping = true;
console.log(`😴 Kai gre spat... (${this._timeStr()} → 06:00)`);
// Fade black screen
this.scene.cameras.main.fadeOut(800, 0, 0, 0);
this.scene.time.delayedCall(900, () => {
// Preskoči na jutro
const prevDay = this.day;
if (this.hour >= 20 || this.hour < 6) {
this._onMidnight(); // Nov dan
}
this.hour = 6;
this.minute = 0;
// Emit morning events
this.scene.events.emit('morningArrived', { day: this.day });
this.scene.events.emit('timeChanged', {
day: this.day, hour: 6, minute: 0, timeStr: '06:00'
});
// Fade back in
this.scene.cameras.main.fadeIn(1200, 0, 0, 0);
// Prikaži "Dan X" overlay
this._showDayOverlay(this.day);
this.scene.time.delayedCall(1300, () => {
this.isSleeping = false;
});
});
}
// ─────────────────────────────────────────────────────────────────────────
// AMBIENT OSVETLITEV
// ─────────────────────────────────────────────────────────────────────────
_updateAmbient() {
const W = this.scene.cameras.main.width;
const H = this.scene.cameras.main.height;
const h = this.hour + this.minute / 60;
let alpha = 0;
let r = 0, g = 0, b = 0;
if (h >= 6 && h < 8) {
// Zora: oranžno-roza tema
const t = (h - 6) / 2;
alpha = 0.25 * (1 - t);
r = 0xff; g = 0x88; b = 0x00;
} else if (h >= 8 && h < 18) {
// Dan: brez overlaya
alpha = 0;
} else if (h >= 18 && h < 20) {
// Sončni zahod: oranžna
const t = (h - 18) / 2;
alpha = 0.15 + t * 0.2;
r = 0xff; g = 0x66; b = 0x00;
} else if (h >= 20 && h < 22) {
// Večer: temno modra
const t = (h - 20) / 2;
alpha = 0.35 + t * 0.25;
r = 0x00; g = 0x00; b = 0x44;
} else if (h >= 22 || h < 4) {
// Noč: globoka modra tema
alpha = 0.60;
r = 0x00; g = 0x00; b = 0x22;
} else if (h >= 4 && h < 6) {
// Pred zoro
const t = (h - 4) / 2;
alpha = 0.60 * (1 - t * 0.5);
r = 0x00; g = 0x00; b = 0x22;
}
this.ambientOverlay.clear();
if (alpha > 0.01) {
this.ambientOverlay.fillStyle(
(r << 16) | (g << 8) | b,
alpha
);
this.ambientOverlay.fillRect(0, 0, W, H);
}
}
// ─────────────────────────────────────────────────────────────────────────
// ZVEZDE (Noč)
// ─────────────────────────────────────────────────────────────────────────
_initStars() {
const W = this.scene.cameras.main.width;
const H = this.scene.cameras.main.height;
this.starGfx = this.scene.add.graphics()
.setScrollFactor(0)
.setDepth(4499);
for (let i = 0; i < 80; i++) {
this.stars.push({
x: Math.random() * W,
y: Math.random() * H * 0.6, // Samo zgoraj
size: 0.5 + Math.random() * 1.5,
twinkle: Math.random() * Math.PI * 2,
speed: 0.5 + Math.random() * 1.5,
});
}
}
_updateStars() {
const h = this.hour + this.minute / 60;
// Stars visible 21:00 - 05:00
let starAlpha = 0;
if (h >= 21 || h < 5) {
starAlpha = (h >= 21) ? Math.min(1, (h - 21) / 0.5) : Math.min(1, (5 - h) / 0.5);
}
this.starGfx.clear();
if (starAlpha < 0.01) return;
const t = Date.now() * 0.001;
for (const star of this.stars) {
const twinkle = 0.4 + 0.6 * Math.sin(t * star.speed + star.twinkle);
this.starGfx.fillStyle(0xffffff, starAlpha * twinkle);
this.starGfx.fillCircle(star.x, star.y, star.size);
}
}
// ─────────────────────────────────────────────────────────────────────────
// SLEEP OBJECTS MANAGEMENT
// ─────────────────────────────────────────────────────────────────────────
registerSleepObject(x, y, sprite, type = 'sleeping_bag') {
this.sleepObjects.push({ x, y, sprite, type });
console.log(`😴 Sleep object registered: ${type} @ ${x},${y}`);
}
_getSleepNear(kai) {
let closest = null;
let minDist = this.interactRange;
for (const obj of this.sleepObjects) {
const d = Phaser.Math.Distance.Between(kai.x, kai.y, obj.x, obj.y);
if (d < minDist) { minDist = d; closest = obj; }
}
return closest;
}
// ─────────────────────────────────────────────────────────────────────────
// UI HELPERS
// ─────────────────────────────────────────────────────────────────────────
_timeStr() {
return `${String(this.hour).padStart(2, '0')}:${String(this.minute).padStart(2, '0')}`;
}
_showSleepPrompt(obj) {
if (!this._sleepPrompt) {
this._sleepPrompt = this.scene.add.text(0, 0, '', {
fontFamily: 'Arial Black',
fontSize: '14px',
color: '#aaaaff',
stroke: '#000000',
strokeThickness: 4,
backgroundColor: '#00000099',
padding: { x: 8, y: 4 },
}).setDepth(9999).setScrollFactor(1);
}
const label = (this.hour >= 20 || this.hour < 6)
? `😴 [Z] Pojdi spat`
: `🌅 [Z] Spanje (po 20:00)`;
this._sleepPrompt.setText(label).setAlpha(1).setPosition(obj.x - 60, obj.y - 80);
}
_hideSleepPrompt() {
if (this._sleepPrompt) this._sleepPrompt.setAlpha(0);
}
_showDayOverlay(day) {
const W = this.scene.cameras.main.width;
const H = this.scene.cameras.main.height;
const txt = this.scene.add.text(W / 2, H / 2, `☀️ DAN ${day}`, {
fontFamily: 'Arial Black',
fontSize: '64px',
color: '#ffdd44',
stroke: '#000000',
strokeThickness: 8,
}).setOrigin(0.5).setScrollFactor(0).setDepth(6000).setAlpha(0);
this.scene.tweens.add({
targets: txt,
alpha: { from: 0, to: 1 },
y: H / 2 - 20,
duration: 600,
ease: 'Back.out',
onComplete: () => {
this.scene.time.delayedCall(1200, () => {
this.scene.tweens.add({
targets: txt,
alpha: 0,
y: H / 2 - 60,
duration: 500,
onComplete: () => txt.destroy(),
});
});
}
});
}
_showFloatingText(x, y, msg, color, size = 20, fixedToCamera = false) {
const txt = this.scene.add.text(x, y, msg, {
fontFamily: 'Arial Black',
fontSize: `${size}px`,
color,
stroke: '#000000',
strokeThickness: 4,
}).setOrigin(0.5).setDepth(9998);
if (fixedToCamera) txt.setScrollFactor(0);
this.scene.tweens.add({
targets: txt,
y: y - 50,
alpha: { from: 1, to: 0 },
duration: 2000,
onComplete: () => txt.destroy(),
});
}
// ─────────────────────────────────────────────────────────────────────────
// PUBLIC API
// ─────────────────────────────────────────────────────────────────────────
getTimeStr() { return this._timeStr(); }
getDay() { return this.day; }
getHour() { return this.hour; }
isNight() { return this.hour >= 22 || this.hour < 6; }
isDay() { return this.hour >= 6 && this.hour < 20; }
/** Nastavi čas (za debug/testing) */
setTime(hour, minute = 0) {
this.hour = hour;
this.minute = minute;
console.log(`⏰ Čas nastavljen: ${this._timeStr()}`);
}
destroy() {
this.ambientOverlay?.destroy();
this.starGfx?.destroy();
this.starGfx = null;
this._sleepPrompt?.destroy();
this._highlightGfx?.destroy();
}
}

View File

@@ -0,0 +1,660 @@
/**
* ============================================================
* FarmingSystem.js — Nova Farma
* ============================================================
* Farming logika:
* - [E] blizu tal → Ore (Hoe) → tilled_soil tile
* - [E] blizu tilled tile (z semenom v roki) → Posadi
* - [E] blizu rastline (z zalivalko v roki) → Zalij
* - Rast po N dnevih (tick ob sleep/new day)
* - [E] blizu zrele rastline → Harvest → item v inventar
* - Brez zalivanja 1 dan → rastlina uvene → izguba
* ============================================================
*
* CROP DEFINITIONS:
* wheat: 4 stopnje, 3 dni, 30g semena, 3×30g harvest
* carrot: 5 stopenj, 4 dni, 15g semena, 2×40g harvest
* cannabis: 6 stopenj, 7 dni, 50g semena, 4×200g harvest (Faza 1)
*/
export default class FarmingSystem {
/**
* @param {Phaser.Scene} scene — GrassSceneClean instanca
* @param {object} options — { islandX, islandY, tileSize }
*/
constructor(scene, options = {}) {
this.scene = scene;
this.islandX = options.islandX || 2000;
this.islandY = options.islandY || 2000;
this.tileSize = options.tileSize || 128;
// Crop definicije
this.CROPS = {
wheat: {
key: 'wheat',
stages: 4, // 0=sprout,1=small,2=medium,3=ripe
daysToGrow: 3, // Dan od saditve do žetve
seedCost: 10,
harvestItem: 'wheat',
harvestCount: [1, 3], // min,max
harvestValue: 30,
stageKeys: [
'crop_wheat_stage0',
'crop_wheat_stage1',
'crop_wheat_stage2',
'crop_wheat_stage3',
],
},
carrot: {
key: 'carrot',
stages: 5,
daysToGrow: 4,
seedCost: 15,
harvestItem: 'carrot',
harvestCount: [1, 2],
harvestValue: 40,
stageKeys: [
'crop_carrot_stage0',
'crop_carrot_stage1',
'crop_carrot_stage2',
'crop_carrot_stage3',
'crop_carrot_stage4',
],
},
cannabis: {
key: 'cannabis',
stages: 6,
daysToGrow: 7,
seedCost: 50,
harvestItem: 'cannabis_bud',
harvestCount: [3, 5],
harvestValue: 200,
stageKeys: [
'crop_cannabis_stage0',
'crop_cannabis_stage1',
'crop_cannabis_stage2',
'crop_cannabis_stage3',
'crop_cannabis_stage4',
'crop_cannabis_stage5',
],
},
};
// Stanje
this.tilledPlots = []; // { x, y, tileSprite, crop, dayPlanted, dayWatered, stage, sprite, wilted }
this.currentDay = 1;
this.interactRange = 60; // px — razdalja za interakcijo
this.TILE_DISPLAY = 40; // Vizualna velikost tilled tile — majhna kocka!
// Aktiven tool v roki (set from InventorySystem)
this.activeTool = 'hoe'; // 'hoe' | 'watering_can' | null
this.activeSeed = 'wheat'; // 'wheat' | 'carrot' | 'cannabis'
// Physics group za tilled tile colliderje
this.plotGroup = scene.physics.add.staticGroup();
// Graphics za highlight (Kai blizu)
this.highlightGfx = scene.add.graphics().setDepth(9000);
// Input — E tipka
this.keyE = scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.E);
// Debounce
this._lastInteract = 0;
console.log('🌱 FarmingSystem initialized');
}
// ─────────────────────────────────────────────────────────────────────────
// PRELOAD — Assets (kliči iz scene.preload PRED new FarmingSystem)
// ─────────────────────────────────────────────────────────────────────────
static preload(scene) {
// Tilled soil
scene.load.image('tilled_soil', 'DEMO_FAZA1/Crops/tilled_soil.png');
// Wheat (4 stopnje)
for (let i = 0; i < 4; i++) {
scene.load.image(`crop_wheat_stage${i}`, `DEMO_FAZA1/Crops/crop_wheat_stage${i}.png`);
}
// Carrot (5 stopenj)
for (let i = 0; i < 5; i++) {
scene.load.image(`crop_carrot_stage${i}`, `DEMO_FAZA1/Crops/crop_carrot_stage${i}.png`);
}
// Cannabis (6 stopenj)
for (let i = 0; i < 6; i++) {
scene.load.image(`crop_cannabis_stage${i}`, `DEMO_FAZA1/Crops/crop_cannabis_stage${i}.png`);
}
}
// ─────────────────────────────────────────────────────────────────────────
// UPDATE — Kliči iz scene.update()
// ─────────────────────────────────────────────────────────────────────────
update(kai, time) {
this.highlightGfx.clear();
const nearPlot = this._getPlotNear(kai);
const nearEmpty = this._getEmptyGroundNear(kai);
// ── Highlight najbližji plot ──
const W = 48, H = 24; // Velikost tilled plot kocke
if (nearPlot) {
this.highlightGfx.lineStyle(2, 0x44ffcc, 0.9);
this.highlightGfx.strokeRect(
nearPlot.x - W / 2,
nearPlot.y - H / 2,
W, H
);
// Tooltip nad plotom
this._drawTooltip(nearPlot);
} else if (nearEmpty && this.activeTool === 'hoe') {
// Pregled oranja
this.highlightGfx.lineStyle(2, 0xaabb88, 0.5);
this.highlightGfx.strokeRect(
nearEmpty.x - W / 2,
nearEmpty.y - H / 2,
W, H
);
}
// ── E tipka — interakcija ──
if (Phaser.Input.Keyboard.JustDown(this.keyE)) {
if (nearPlot) {
this._interactWithPlot(nearPlot, kai);
} else if (nearEmpty && this.activeTool === 'hoe') {
this._tillGround(nearEmpty.x, nearEmpty.y);
}
}
}
// ─────────────────────────────────────────────────────────────────────────
// ORE (TILL) — Ustvari nov tilled plot
// ─────────────────────────────────────────────────────────────────────────
_tillGround(x, y) {
// Snap na grid — center tile-a
const ts = this.tileSize;
const snappedX = Math.floor(x / ts) * ts + ts / 2;
const snappedY = Math.floor(y / ts) * ts + ts / 2;
// Ali plot že obstaja?
if (this.tilledPlots.some(p => Math.abs(p.x - snappedX) < 4 && Math.abs(p.y - snappedY) < 4)) return;
// === Nariši tilled soil z Graphics — točna velikost, brez skaliranja slik ===
const W = 48; // širina kocke (world px)
const H = 24; // višina kocke (world px)
const gfx = this.scene.add.graphics();
gfx.setDepth(snappedY - 50);
// Ozadje — temna rjava zemlja
gfx.fillStyle(0x3a2010, 1);
gfx.fillRect(snappedX - W / 2, snappedY - H / 2, W, H);
// Brazde (bela linija × 3, horizontalne)
gfx.lineStyle(1, 0x6b4020, 0.7);
const lines = 3;
for (let i = 1; i <= lines; i++) {
const ly = snappedY - H / 2 + (H / (lines + 1)) * i;
gfx.beginPath();
gfx.moveTo(snappedX - W / 2 + 3, ly);
gfx.lineTo(snappedX + W / 2 - 3, ly);
gfx.strokePath();
}
// Tanka okvirna linija
gfx.lineStyle(1, 0x8b5a2b, 0.5);
gfx.strokeRect(snappedX - W / 2, snappedY - H / 2, W, H);
// Pop-in animacija — skali iz 0
gfx.setScale(0.1);
this.scene.tweens.add({
targets: gfx,
scaleX: 1.0,
scaleY: 1.0,
duration: 250,
ease: 'Back.out',
});
// Shrani referenčni objekt (tile = gfx)
const tile = gfx;
// Physics static body za collider
const body = this.plotGroup.create(snappedX, snappedY);
body.setDisplaySize(this.tileSize - 4, this.tileSize - 4);
body.setAlpha(0);
body.refreshBody();
// Shrani plot
const plot = {
x: snappedX,
y: snappedY,
tileSprite: tile,
bodyRef: body,
crop: null,
dayPlanted: null,
dayWatered: null,
stage: 0,
sprite: null,
wilted: false,
};
this.tilledPlots.push(plot);
// Animacija: "Pop" ob oranju
this.scene.tweens.add({
targets: tile,
scaleX: { from: 1.1, to: 1.0 },
scaleY: { from: 0.8, to: 1.0 },
duration: 200,
ease: 'Bounce.out',
});
this._showFloatingText(snappedX, snappedY, '🪱 Poorano!', '#aabb88');
console.log(`🌱 Poorano: ${snappedX}, ${snappedY}`);
}
// ─────────────────────────────────────────────────────────────────────────
// INTERAKCIJA Z PLOTOM (posadi / zalij / poženi)
// ─────────────────────────────────────────────────────────────────────────
_interactWithPlot(plot, kai) {
if (plot.wilted) {
// Uvela rastlina — odstrani in reciklira plot
this._removeCrop(plot);
this._showFloatingText(plot.x, plot.y, '🍂 Odstranjeno', '#cc6666');
return;
}
if (!plot.crop) {
// 1. Prazno poorano → Posadi
if (this.activeTool === 'hoe' || this.activeSeed) {
this._plantCrop(plot, this.activeSeed);
}
} else if (plot.crop && plot.stage < this._getCropDef(plot.crop).stages - 1) {
// 2. Rastlina raste → Zalij (samo z zalivalko)
if (this.activeTool === 'watering_can') {
this._waterCrop(plot);
} else {
this._showFloatingText(plot.x, plot.y - 30, '💧 Vzemi zalivalko!', '#aaaaff');
}
} else if (plot.stage >= this._getCropDef(plot.crop).stages - 1 && !plot.wilted) {
// 3. Zrela → Poženi
this._harvestCrop(plot);
}
}
// ─────────────────────────────────────────────────────────────────────────
// POSADI
// ─────────────────────────────────────────────────────────────────────────
_plantCrop(plot, cropKey) {
const def = this._getCropDef(cropKey);
if (!def) {
console.warn('Unknown crop:', cropKey);
return;
}
plot.crop = cropKey;
plot.dayPlanted = this.currentDay;
plot.dayWatered = this.currentDay; // Zaliješ takoj ob sajenju
plot.stage = 0;
plot.wilted = false;
// Ustvari sprite za rastlino (stage 0)
// Stage 0 = 20px visoka (klica), max stage = 70px (zrela rastlina)
const stageKey = def.stageKeys[0];
const cropH = 20;
const texture = this.scene.textures.get(stageKey);
const srcH = texture?.getSourceImage()?.height || 100;
const scale = cropH / srcH;
// Posadi na center tilled tile-a z offset za vizualni izgled
plot.sprite = this.scene.add.image(plot.x, plot.y, stageKey)
.setScale(scale)
.setOrigin(0.5, 1.0)
.setDepth(plot.y + 1)
.setAlpha(0);
// Pop-in animacija
this.scene.tweens.add({
targets: plot.sprite,
alpha: 1,
scaleX: { from: scale * 1.4, to: scale },
scaleY: { from: scale * 0.5, to: scale },
duration: 350,
ease: 'Back.out',
});
this._showFloatingText(plot.x, plot.y - 30, `🌱 ${cropKey}!`, '#88ff88', 18);
console.log(`🌱 Posajeno: ${cropKey} @ day ${this.currentDay}`);
}
// ─────────────────────────────────────────────────────────────────────────
// ZALIJ
// ─────────────────────────────────────────────────────────────────────────
_waterCrop(plot) {
if (!plot.crop) return;
if (plot.dayWatered === this.currentDay) {
this._showFloatingText(plot.x, plot.y - 30, '✅ Že zalivano!', '#44ffcc');
return;
}
// === PREVERI VODO v vedru ===
if (this.waterSystem) {
if (!this.waterSystem.hasWater(1)) {
this._showFloatingText(plot.x, plot.y - 30,
'❌ Vedro prazno! [F] pri RC', '#ff6666', 16);
return;
}
this.waterSystem.useWater(1); // Porabi 1L
}
plot.dayWatered = this.currentDay;
plot.wilted = false;
// Ripple efekt — modri krogci
this._waterRipple(plot.x, plot.y);
this._showFloatingText(plot.x, plot.y - 20, '💧 Zalivano!', '#44aaff');
// Tla (Graphics) - prebarva temnejše ko so mokra
if (plot.tileSprite) {
plot.tileSprite.clear();
const W = 48, H = 24;
const snappedX = plot.x, snappedY = plot.y;
// Mokra zemlja = temnejša
plot.tileSprite.fillStyle(0x25140a, 1);
plot.tileSprite.fillRect(snappedX - W / 2, snappedY - H / 2, W, H);
plot.tileSprite.lineStyle(1, 0x4a2c10, 0.7);
for (let i = 1; i <= 3; i++) {
const ly = snappedY - H / 2 + (H / 4) * i;
plot.tileSprite.beginPath();
plot.tileSprite.moveTo(snappedX - W / 2 + 3, ly);
plot.tileSprite.lineTo(snappedX + W / 2 - 3, ly);
plot.tileSprite.strokePath();
}
}
}
// ─────────────────────────────────────────────────────────────────────────
// ŽETEV
// ─────────────────────────────────────────────────────────────────────────
_harvestCrop(plot) {
const def = this._getCropDef(plot.crop);
const count = Phaser.Math.Between(def.harvestCount[0], def.harvestCount[1]);
const value = count * def.harvestValue;
// Notify scene (UIScene bo prikazal)
this.scene.events.emit('harvest', {
item: def.harvestItem,
count,
value,
x: plot.x,
y: plot.y,
});
// Floating nagrade tekst
this._showFloatingText(plot.x, plot.y - 20,
`🌾 ×${count} (+${value}g)`, '#ffd700', 28);
// Bounce animacija preden izgine
if (plot.sprite) {
this.scene.tweens.add({
targets: plot.sprite,
scaleX: plot.sprite.scaleX * 1.3,
scaleY: plot.sprite.scaleY * 1.3,
alpha: 0,
duration: 400,
ease: 'Back.in',
onComplete: () => { if (plot.sprite) plot.sprite.destroy(); }
});
}
// Reset plota (poorano ostane, crop zbrisano)
plot.crop = null;
plot.sprite = null;
plot.dayPlanted = null;
plot.dayWatered = null;
plot.stage = 0;
plot.wilted = false;
// Tla postanejo svetlejša (prazna)
this.scene.tweens.add({
targets: plot.tileSprite,
alpha: 0.9,
duration: 500,
});
console.log(`🌾 Harvest: ${def.harvestItem} ×${count} = ${value}g`);
}
// ─────────────────────────────────────────────────────────────────────────
// DNEVNI TICK — Kliči enkrat na dan (ob sleep/new day)
// ─────────────────────────────────────────────────────────────────────────
onNewDay(day) {
this.currentDay = day;
for (const plot of this.tilledPlots) {
if (!plot.crop || plot.wilted) continue;
const def = this._getCropDef(plot.crop);
// ── Preveri zalivanje ──
if (plot.dayWatered < day - 1) {
// Ni bilo zalivano včeraj → Uvene!
this._wiltCrop(plot);
continue;
}
// ── Rast ──
const daysGrown = day - plot.dayPlanted;
// Enakomerno razporedimo stopnje čez daysToGrow
const newStage = Math.min(
Math.floor((daysGrown / def.daysToGrow) * def.stages),
def.stages - 1
);
if (newStage > plot.stage) {
plot.stage = newStage;
this._updateCropSprite(plot);
if (newStage === def.stages - 1) {
// ZRELA!
this._showFloatingText(plot.x, plot.y - 40, '🌟 Zrelo!', '#ffd700', 24);
this._glowPulse(plot.sprite);
}
}
}
console.log(`🌞 Dan ${day} tick — ${this.tilledPlots.filter(p => p.crop).length} crop-ov aktiv`);
}
// ─────────────────────────────────────────────────────────────────────────
// UVENJANJE
// ─────────────────────────────────────────────────────────────────────────
_wiltCrop(plot) {
plot.wilted = true;
// Sprite potemni in se trese
if (plot.sprite) {
this.scene.tweens.add({
targets: plot.sprite,
tint: 0x554433,
alpha: 0.5,
duration: 800,
ease: 'Sine.in',
onComplete: () => {
if (plot.sprite) plot.sprite.setTint(0x554433).setAlpha(0.45);
}
});
}
this._showFloatingText(plot.x, plot.y - 30, '🍂 Uvelo!', '#cc6633');
console.warn(`🍂 Rastlina uvela @ ${plot.x},${plot.y}`);
}
// ─────────────────────────────────────────────────────────────────────────
// POMOŽNE METODE
// ─────────────────────────────────────────────────────────────────────────
_getCropDef(key) {
return this.CROPS[key] || null;
}
_updateCropSprite(plot) {
const def = this._getCropDef(plot.crop);
const stageKey = def.stageKeys[plot.stage];
// Ciljana višina: 20px (stage0) → 70px (zrela)
const minH = 20, maxH = 70;
const cropH = minH + (plot.stage / (def.stages - 1)) * (maxH - minH);
const texture = this.scene.textures.get(stageKey);
const srcH = texture?.getSourceImage()?.height || 150;
const scale = cropH / srcH;
if (plot.sprite) {
// Animiran prehod na novo stopnjo
this.scene.tweens.add({
targets: plot.sprite,
scaleX: { from: plot.sprite.scaleX, to: scale },
scaleY: { from: plot.sprite.scaleY, to: scale },
duration: 600,
ease: 'Back.out',
onStart: () => { if (plot.sprite) plot.sprite.setTexture(stageKey); }
});
}
}
_removeCrop(plot) {
if (plot.sprite) { plot.sprite.destroy(); plot.sprite = null; }
plot.crop = null;
plot.dayPlanted = null;
plot.dayWatered = null;
plot.stage = 0;
plot.wilted = false;
if (plot.tileSprite) plot.tileSprite.setAlpha(0.9).clearTint();
}
_getPlotNear(kai) {
let closest = null;
let minDist = this.interactRange;
for (const plot of this.tilledPlots) {
const d = Phaser.Math.Distance.Between(kai.x, kai.y, plot.x, plot.y);
if (d < minDist) { minDist = d; closest = plot; }
}
return closest;
}
_getEmptyGroundNear(kai) {
// Snap na tile center — Kai mora biti v bližini centra tile-a
const ts = this.tileSize;
const snappedX = Math.floor(kai.x / ts) * ts + ts / 2;
const snappedY = Math.floor(kai.y / ts) * ts + ts / 2;
const dist = Phaser.Math.Distance.Between(kai.x, kai.y, snappedX, snappedY);
if (dist > this.interactRange) return null;
if (this.tilledPlots.some(p => Math.abs(p.x - snappedX) < 8 && Math.abs(p.y - snappedY) < 8)) return null;
return { x: snappedX, y: snappedY };
}
_drawTooltip(plot) {
const def = plot.crop ? this._getCropDef(plot.crop) : null;
let label = '';
if (!plot.crop) label = '🌱 [E] Posadi';
else if (plot.wilted) label = '🍂 [E] Odstrani';
else if (plot.stage >= (def?.stages - 1)) label = '🌾 [E] Poženi!';
else if (this.activeTool === 'watering_can') label = '💧 [E] Zalij';
else label = `🌿 ${plot.crop} (${plot.stage + 1}/${def?.stages})`;
// Tooltip — svetla, jasna, pri vrhu tilled tile-a
if (!this._tooltipText) {
this._tooltipText = this.scene.add.text(0, 0, '', {
fontFamily: 'Arial Black',
fontSize: '13px',
color: '#ffffff',
stroke: '#000000',
strokeThickness: 4,
backgroundColor: '#00000099',
padding: { x: 8, y: 4 },
}).setDepth(9999).setScrollFactor(1);
}
const W = 48, H = 24;
this._tooltipText
.setText(label)
.setPosition(plot.x - this._tooltipText.width / 2, plot.y - H / 2 - 20);
}
_showFloatingText(x, y, msg, color = '#ffffff', fontSize = 20) {
const txt = this.scene.add.text(x, y, msg, {
fontFamily: 'Arial Black',
fontSize: `${fontSize}px`,
color,
stroke: '#000000',
strokeThickness: 4,
}).setOrigin(0.5).setDepth(9998);
this.scene.tweens.add({
targets: txt,
y: y - 60,
alpha: { from: 1, to: 0 },
duration: 1400,
ease: 'Cubic.out',
onComplete: () => txt.destroy(),
});
}
_waterRipple(x, y) {
const gfx = this.scene.add.graphics().setDepth(9997);
let r = 4, alpha = 0.7;
const expand = this.scene.time.addEvent({
delay: 40,
repeat: 8,
callback: () => {
gfx.clear();
gfx.lineStyle(2, 0x4499ff, alpha);
gfx.strokeCircle(x, y, r);
r += 6; alpha -= 0.08;
if (r > 55) { gfx.destroy(); expand.remove(); }
}
});
}
_glowPulse(sprite) {
if (!sprite) return;
this.scene.tweens.add({
targets: sprite,
alpha: { from: 1, to: 0.5 },
duration: 400,
yoyo: true,
repeat: 3,
ease: 'Sine.inOut',
});
}
// ─────────────────────────────────────────────────────────────────────────
// PUBLIC API — Za GrassScene
// ─────────────────────────────────────────────────────────────────────────
/** Nastavi aktiven tool (kliče InventorySystem) */
setTool(tool) { this.activeTool = tool; }
/** Nastavi aktivno seme (kliče InventorySystem) */
setSeed(seed) { this.activeSeed = seed; }
/** Vrni vse activate plots (za save) */
getState() {
return this.tilledPlots.map(p => ({
x: p.x, y: p.y,
crop: p.crop,
dayPlanted: p.dayPlanted,
dayWatered: p.dayWatered,
stage: p.stage,
wilted: p.wilted,
}));
}
/** Destroy */
destroy() {
this.highlightGfx?.destroy();
this._tooltipText?.destroy();
this.tilledPlots.forEach(p => {
p.tileSprite?.destroy();
p.sprite?.destroy();
});
this.tilledPlots = [];
}
}

View File

@@ -0,0 +1,326 @@
/**
* ============================================================
* InventorySystem.js — Nova Farma
* ============================================================
* Hotbar (7 slotov) + Items logika
*
* DEMO starter items:
* Slot 1: 🪓 Motika (Hoe) → ore tla
* Slot 2: 💧 Zalivalka → zalij crop
* Slot 3: 🌾 Pšenica semena ×5
* Slot 4: 🥕 Korenje semena ×5
* Slot 5: — prazen —
* Slot 6: — prazen —
* Slot 7: — prazen —
*
* Keys: [1]-[7] za slot, scroll za rotate
* Emit 'hotbarChanged' { slot, item } → GrassScene → FarmingSystem
* ============================================================
*/
export default class InventorySystem {
/**
* @param {Phaser.Scene} scene — GrassSceneClean
*/
constructor(scene) {
this.scene = scene;
// Vsi predmeti (full inventory)
this.items = {};
// Format: { 'wheat_seed': { name, icon, count, type, tool/seed } }
// Hotbar = 7 slotov, vsak kaže na item key ali null
this.hotbar = [
'hoe',
'watering_can',
'wheat_seed',
'carrot_seed',
null,
null,
null,
];
this.activeSlot = 0; // Slot 1 = Motika
// Item definicije
this.ITEM_DEFS = {
hoe: {
name: 'Motika',
icon: '🪓',
type: 'tool',
tool: 'hoe',
stackable: false,
count: 1,
desc: '[E] Oraj tla',
},
watering_can: {
name: 'Zalivalka',
icon: '💧',
type: 'tool',
tool: 'watering_can',
stackable: false,
count: 1,
desc: '[E] Zalij rastlino',
},
wheat_seed: {
name: 'Pšenica',
icon: '🌾',
type: 'seed',
seed: 'wheat',
stackable: true,
count: 5,
desc: '3 dni • 30g/harvest',
color: '#ffdd88',
},
carrot_seed: {
name: 'Korenje',
icon: '🥕',
type: 'seed',
seed: 'carrot',
stackable: true,
count: 5,
desc: '4 dni • 40g/harvest',
color: '#ff8844',
},
cannabis_seed: {
name: 'Konoplja',
icon: '🌿',
type: 'seed',
seed: 'cannabis',
stackable: true,
count: 0,
desc: '7 dni • 200g/harvest',
color: '#88cc44',
},
// Harvested items
wheat: {
name: 'Pšenica',
icon: '🌾',
type: 'resource',
stackable: true,
count: 0,
value: 30,
},
carrot: {
name: 'Korenje',
icon: '🥕',
type: 'resource',
stackable: true,
count: 0,
value: 40,
},
cannabis_bud: {
name: 'Konoplja',
icon: '🌿',
type: 'resource',
stackable: true,
count: 0,
value: 200,
},
};
// Inicializiraj items iz ITEM_DEFS (samo tiste s count > 0)
for (const [key, def] of Object.entries(this.ITEM_DEFS)) {
this.items[key] = { ...def };
}
// Denar (Gold)
this.gold = 0;
// Number keys [1]-[7]
for (let i = 1; i <= 7; i++) {
scene.input.keyboard.on(`keydown-${i}`, () => this.selectSlot(i - 1));
}
// Scroll wheel za rotation
scene.input.on('wheel', (pointer, gObj, dx, dy) => {
if (dy > 0) this.selectSlot((this.activeSlot + 1) % 7);
else if (dy < 0) this.selectSlot((this.activeSlot + 6) % 7);
});
// Poslušaj harvest evente → dodaj v inventar
scene.events.on('harvest', (data) => {
this.addItem(data.item, data.count);
this.addGold(data.value);
});
// Prvič sinhronizira z FarmingSystem
this._syncWithFarming();
console.log('🎒 InventorySystem initialized');
}
// ─────────────────────────────────────────────────────────────────────────
// IZBERI SLOT
// ─────────────────────────────────────────────────────────────────────────
selectSlot(index) {
if (index < 0 || index >= 7) return;
this.activeSlot = index;
this._syncWithFarming();
this._notifyUI();
console.log(`🎒 Slot ${index + 1}: ${this.hotbar[index] || 'prazen'}`);
}
// ─────────────────────────────────────────────────────────────────────────
// DODAJ ITEM
// ─────────────────────────────────────────────────────────────────────────
addItem(key, count = 1) {
if (!this.items[key]) {
this.items[key] = { ...(this.ITEM_DEFS[key] || { name: key, icon: '📦', count: 0 }) };
}
this.items[key].count += count;
// Dodaj v hotbar če je prazen slot + seme/resource
const def = this.ITEM_DEFS[key];
if (def && def.stackable && !this.hotbar.includes(key)) {
const emptySlot = this.hotbar.findIndex(s => s === null);
if (emptySlot !== -1) this.hotbar[emptySlot] = key;
}
this._notifyUI();
console.log(`🎒 +${count}× ${key} (skupaj: ${this.items[key].count})`);
}
addGold(amount) {
this.gold += amount;
this._notifyUI();
console.log(`💰 +${amount}g (skupaj: ${this.gold}g)`);
}
// ─────────────────────────────────────────────────────────────────────────
// PORABI ITEM (semena pri sajenju)
// ─────────────────────────────────────────────────────────────────────────
useActiveItem() {
const key = this.hotbar[this.activeSlot];
if (!key) return false;
const item = this.items[key];
if (!item) return false;
if (item.type === 'seed') {
if (item.count <= 0) {
this._showMsg(`❌ Ni ${item.name} semen!`, '#ff6666');
return false;
}
item.count--;
if (item.count <= 0 && item.stackable) {
// Odstrani iz hotbara
this.hotbar[this.activeSlot] = null;
}
this._notifyUI();
return true;
}
if (item.type === 'tool') return true; // Orodja se ne porabijo
return false;
}
// ─────────────────────────────────────────────────────────────────────────
// SYNC z FarmingSystem
// ─────────────────────────────────────────────────────────────────────────
_syncWithFarming() {
const farming = this.scene.farmingSystem;
if (!farming) return;
const key = this.hotbar[this.activeSlot];
if (!key) {
farming.setTool(null);
return;
}
const item = this.items[key];
if (!item) return;
if (item.type === 'tool') {
farming.setTool(item.tool);
farming.setSeed(null);
} else if (item.type === 'seed') {
farming.setTool('hoe'); // Semena → avtomatsko orodje = motika za saditev
farming.setSeed(item.seed);
} else {
farming.setTool(null);
farming.setSeed(null);
}
}
// ─────────────────────────────────────────────────────────────────────────
// NOTIFY UIScene
// ─────────────────────────────────────────────────────────────────────────
_notifyUI() {
const uiScene = this.scene.scene.get('UIScene');
if (!uiScene) return;
// Pošlji hotbar stanje
const hotbarData = this.hotbar.map((key, i) => {
if (!key) return null;
const item = this.items[key];
const def = this.ITEM_DEFS[key];
return {
key,
icon: def?.icon || '📦',
name: def?.name || key,
count: item?.count ?? 1,
active: i === this.activeSlot,
type: def?.type || 'unknown',
stackable: def?.stackable || false,
};
});
uiScene.events.emit('hotbarUpdate', {
slots: hotbarData,
activeSlot: this.activeSlot,
gold: this.gold,
});
}
_showMsg(msg, color = '#ffffff') {
const cam = this.scene.cameras.main;
const txt = this.scene.add.text(
cam.worldView.centerX,
cam.worldView.top + 80,
msg, {
fontFamily: 'Arial Black',
fontSize: '20px',
color,
stroke: '#000000',
strokeThickness: 4,
}
).setOrigin(0.5).setDepth(9999);
this.scene.tweens.add({
targets: txt,
y: txt.y - 40,
alpha: { from: 1, to: 0 },
duration: 1500,
onComplete: () => txt.destroy(),
});
}
// ─────────────────────────────────────────────────────────────────────────
// PUBLIC API
// ─────────────────────────────────────────────────────────────────────────
getActiveItem() {
const key = this.hotbar[this.activeSlot];
return key ? this.items[key] : null;
}
getActiveItemKey() {
return this.hotbar[this.activeSlot];
}
getCount(key) {
return this.items[key]?.count || 0;
}
getGold() {
return this.gold;
}
getState() {
return {
items: this.items,
hotbar: this.hotbar,
activeSlot: this.activeSlot,
gold: this.gold,
};
}
}

View File

@@ -0,0 +1,329 @@
/**
* ============================================================
* WaterSystem.js — Nova Farma (DEMO)
* ============================================================
* Voda je omejena v Demu — edini vir = Rain Catcher.
*
* Logika:
* - Rain Catcher zbira vodo med dežjem (triggerRainfall)
* - Max kapaciteta = 20 L na Rain Catcher
* - Zalivanje porabi 1 L na crop
* - Kai dobi "vedro" (watering_can) s 5L kapaciteto
* - [E] blizu Rain Catcher → Napolni vedro iz Rain Catcher-ja
* - UIScene prikaže: 💧 vedro: 3/5 | 🪣 RC: 12/20
* ============================================================
*/
export default class WaterSystem {
/**
* @param {Phaser.Scene} scene
* @param {object} options — { islandX, islandY, tileSize }
*/
constructor(scene, options = {}) {
this.scene = scene;
this.tileSize = options.tileSize || 128;
// === Vedro (Watering Can) ===
this.canCurrent = 3; // Začne s 3L
this.canMax = 5; // Max 5L v vedru
this.canPerCrop = 1; // 1L na zalivanje
// === Rain Catcher objekti ===
this.rainCatchers = []; // { x, y, sprite, current, max, label }
// === Highlights ===
this.highlightGfx = scene.add.graphics().setDepth(9001);
// === Input [F] za napolnit vedro ===
this.keyF = scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.F);
// === Interact razdalja ===
this.interactRange = 100;
// === Dežjni multiplier ===
this.rainCollectRate = 4; // L na dežen dan na RC
console.log('💧 WaterSystem initialized');
}
// ─────────────────────────────────────────────────────────────────────────
// PRELOAD
// ─────────────────────────────────────────────────────────────────────────
static preload(scene) {
scene.load.image('rain_catcher', 'DEMO_FAZA1/Structures/rain_catcher.png');
}
// ─────────────────────────────────────────────────────────────────────────
// PLACE RAIN CATCHER (kliče se ob postavitvi zgradbe ali ob init)
// ─────────────────────────────────────────────────────────────────────────
placeRainCatcher(x, y) {
const targetH = 100; // px višina v svetu
const texture = this.scene.textures.get('rain_catcher');
const srcH = texture?.getSourceImage()?.height || 512;
const scale = targetH / srcH;
const sprite = this.scene.add.image(x, y, 'rain_catcher')
.setScale(scale)
.setOrigin(0.5, 1.0)
.setDepth(y)
.setInteractive();
// Label nad RC
const label = this.scene.add.text(x, y - targetH - 8, '🪣 0/20L', {
fontFamily: 'Arial Black',
fontSize: '12px',
color: '#44aaff',
stroke: '#000000',
strokeThickness: 3,
}).setOrigin(0.5, 1.0).setDepth(y + 1);
const rc = {
x, y,
sprite,
label,
current: 0,
max: 20,
};
this.rainCatchers.push(rc);
this._updateLabel(rc);
// Pop-in animacija
sprite.setAlpha(0).setScale(scale * 0.1);
this.scene.tweens.add({
targets: sprite,
alpha: 1,
scaleX: scale,
scaleY: scale,
duration: 500,
ease: 'Back.out',
});
console.log(`🪣 Rain Catcher postavljen @ ${x}, ${y}`);
return rc;
}
// ─────────────────────────────────────────────────────────────────────────
// UPDATE — kliči iz scene.update()
// ─────────────────────────────────────────────────────────────────────────
update(kai) {
this.highlightGfx.clear();
const nearRC = this._getRCNear(kai);
if (nearRC) {
// Highlight RC
this.highlightGfx.lineStyle(2, 0x44aaff, 0.9);
this.highlightGfx.strokeCircle(nearRC.x, nearRC.y - 50, 60);
// Tooltip
this._showRCTooltip(nearRC);
} else {
this._hideRCTooltip();
}
// [F] — Napolni vedro
if (Phaser.Input.Keyboard.JustDown(this.keyF)) {
if (nearRC) {
this._fillCan(nearRC);
} else {
this._showFloatingTextAt(kai.x, kai.y - 40, '🪣 Bliže Rain Catcher!', '#aaaaff', 14);
}
}
}
// ─────────────────────────────────────────────────────────────────────────
// NAPOLNI VEDRO iz Rain Catcher-ja
// ─────────────────────────────────────────────────────────────────────────
_fillCan(rc) {
if (rc.current <= 0) {
this._showFloatingTextAt(rc.x, rc.y - 110, '❌ RC je prazen!', '#ff6666');
return;
}
if (this.canCurrent >= this.canMax) {
this._showFloatingTextAt(rc.x, rc.y - 110, '✅ Vedro je polno!', '#44ffcc');
return;
}
const needed = this.canMax - this.canCurrent;
const take = Math.min(needed, rc.current);
rc.current -= take;
this.canCurrent += take;
this._updateLabel(rc);
this.scene.events.emit('waterChanged', {
can: this.canCurrent,
canMax: this.canMax,
});
// Animacija — vodni curek (modri krogci padajo v vedro)
this._fillAnimation(rc.x, rc.y - 30);
this._showFloatingTextAt(rc.x, rc.y - 110, `💧 +${take}L (vedro: ${this.canCurrent}/${this.canMax}L)`, '#44aaff', 16);
console.log(`💧 Napolnjeno: +${take}L. Vedro: ${this.canCurrent}/${this.canMax}L. RC: ${rc.current}/${rc.max}L`);
}
// ─────────────────────────────────────────────────────────────────────────
// PORABI VODO ZA ZALIVANJE (kliče FarmingSystem)
// ─────────────────────────────────────────────────────────────────────────
useWater(amount = 1) {
if (this.canCurrent < amount) return false; // Ni dovolj vode!
this.canCurrent -= amount;
this.scene.events.emit('waterChanged', {
can: this.canCurrent,
canMax: this.canMax,
});
return true;
}
hasWater(amount = 1) {
return this.canCurrent >= amount;
}
// ─────────────────────────────────────────────────────────────────────────
// DEŽNI TICK — kliči ob dežju (triggerRainfall v GrassScene)
// ─────────────────────────────────────────────────────────────────────────
onRain() {
let total = 0;
for (const rc of this.rainCatchers) {
const collected = Math.min(this.rainCollectRate, rc.max - rc.current);
rc.current += collected;
total += collected;
this._updateLabel(rc);
// Ripple animacija na RC
this._rainRipple(rc.x, rc.y - 40);
}
if (total > 0) {
console.log(`🌧️ Dež zbral ${total}L v Rain Catcher-jih`);
}
this.scene.events.emit('waterChanged', {
can: this.canCurrent,
canMax: this.canMax,
});
}
// ─────────────────────────────────────────────────────────────────────────
// POMOŽNE METODE
// ─────────────────────────────────────────────────────────────────────────
_getRCNear(kai) {
let closest = null;
let minDist = this.interactRange;
for (const rc of this.rainCatchers) {
const d = Phaser.Math.Distance.Between(kai.x, kai.y, rc.x, rc.y);
if (d < minDist) { minDist = d; closest = rc; }
}
return closest;
}
_updateLabel(rc) {
const pct = rc.current / rc.max;
const color = pct > 0.5 ? '#44aaff' : pct > 0.2 ? '#ffaa00' : '#ff4444';
rc.label
.setText(`🪣 ${rc.current}/${rc.max}L`)
.setColor(color);
}
_showRCTooltip(rc) {
if (!this._rcTooltip) {
this._rcTooltip = this.scene.add.text(0, 0, '', {
fontFamily: 'Arial Black',
fontSize: '13px',
color: '#ffffff',
stroke: '#000000',
strokeThickness: 4,
backgroundColor: '#00000099',
padding: { x: 8, y: 4 },
}).setDepth(9999).setScrollFactor(1);
}
const label = this.canCurrent >= this.canMax
? `✅ Vedro polno (${this.canCurrent}L)`
: `[F] Napolni vedro 💧${this.canCurrent}/${this.canMax}L`;
this._rcTooltip
.setText(label)
.setAlpha(1)
.setPosition(rc.x - this._rcTooltip.width / 2, rc.y - 115);
}
_hideRCTooltip() {
if (this._rcTooltip) this._rcTooltip.setAlpha(0);
}
_fillAnimation(x, y) {
// Padajoče kapljice
for (let i = 0; i < 5; i++) {
const drop = this.scene.add.graphics().setDepth(9998);
const ox = x + Phaser.Math.Between(-15, 15);
drop.fillStyle(0x44aaff, 0.8);
drop.fillEllipse(ox, y, 4, 8);
this.scene.tweens.add({
targets: drop,
y: y + 30 + Phaser.Math.Between(0, 20),
alpha: 0,
delay: i * 60,
duration: 300,
ease: 'Quad.in',
onComplete: () => drop.destroy(),
});
}
}
_rainRipple(x, y) {
const gfx = this.scene.add.graphics().setDepth(9997);
let r = 2, alpha = 0.6;
const ev = this.scene.time.addEvent({
delay: 50,
repeat: 5,
callback: () => {
gfx.clear();
gfx.lineStyle(1, 0x44aaff, alpha);
gfx.strokeEllipse(x, y, r * 2, r);
r += 4; alpha -= 0.1;
if (r > 25) { gfx.destroy(); ev.remove(); }
}
});
}
_showFloatingTextAt(x, y, msg, color = '#ffffff', fontSize = 18) {
const txt = this.scene.add.text(x, y, msg, {
fontFamily: 'Arial Black',
fontSize: `${fontSize}px`,
color,
stroke: '#000000',
strokeThickness: 4,
}).setOrigin(0.5).setDepth(9998);
this.scene.tweens.add({
targets: txt,
y: y - 50,
alpha: { from: 1, to: 0 },
duration: 1600,
ease: 'Cubic.out',
onComplete: () => txt.destroy(),
});
}
// ─────────────────────────────────────────────────────────────────────────
// PUBLIC API
// ─────────────────────────────────────────────────────────────────────────
/** Vrni stanje za save */
getState() {
return {
can: this.canCurrent,
catchers: this.rainCatchers.map(rc => ({ x: rc.x, y: rc.y, current: rc.current })),
};
}
destroy() {
this.highlightGfx?.destroy();
this._rcTooltip?.destroy();
this.rainCatchers.forEach(rc => {
rc.sprite?.destroy();
rc.label?.destroy();
});
this.rainCatchers = [];
}
}

View File

@@ -0,0 +1,466 @@
/**
* ============================================================
* ZombieSystem.js — Nova Farma Demo
* ============================================================
* Shambler zombi AI:
* - Spawn ob robu otoka (ponoči)
* - Počasi sledi Kai-u (shambling walk)
* - [SPACE] blizu → Alfa Moč → zombi udomačen
* - Udomačen zombi sledi Kai-u kot spremljevalec
* - Max 3 aktivni zombiji na otoku
*
* Sprite sheet: zombie_shambler_walk_sheet.png
* Format: 4 stolpci × 4 vrstice = 16 frameov
* Frame size: 160×160px (640px sheet / 4)
* Vrstice: 0=DOWN, 1=LEFT, 2=RIGHT, 3=UP
* ============================================================
*/
export default class ZombieSystem {
constructor(scene, options = {}) {
this.scene = scene;
// Otok meje (za spawn na robu)
this.islandX = options.islandX || 2000;
this.islandY = options.islandY || 2000;
this.islandW = options.islandW || 6400;
this.islandH = options.islandH || 6400;
// Zombi parametri
this.maxZombies = 3;
this.spawnAtNight = true;
this.zombieSpeed = 35; // px/s — počasi šambers
this.detectionRange = 600; // Kai zazna zombija → prikaže opozorilo
this.alfaRange = 120; // Razdalja za Alfa Moč [SPACE]
this.tamingTime = 2000; // ms za udomačitev (drži [SPACE])
// Aktivni zombiji
this.zombies = [];
// Alfa moč (SPACE)
this.keySpace = scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE);
this._alfaProgress = 0;
this._alfaGfx = scene.add.graphics().setDepth(9005);
this._alfaBar = null;
// Spawn timer
this.spawnTimer = null;
// Phaser groups
this.zombieGroup = scene.add.group();
console.log('🧟 ZombieSystem initialized');
}
// ─────────────────────────────────────────────────────────────────────────
// PRELOAD
// ─────────────────────────────────────────────────────────────────────────
static preload(scene) {
// Frame size: 640/4 = 160px
scene.load.spritesheet('zombie_shambler', 'DEMO_FAZA1/Characters/zombie_shambler_walk_sheet.png', {
frameWidth: 160,
frameHeight: 160,
});
}
// ─────────────────────────────────────────────────────────────────────────
// CREATE ANIMATIONS
// ─────────────────────────────────────────────────────────────────────────
createAnimations() {
const fps = 6;
const anims = [
{ key: 'zombie_walk_down', frames: [0, 1, 2, 3] },
{ key: 'zombie_walk_left', frames: [4, 5, 6, 7] },
{ key: 'zombie_walk_right', frames: [8, 9, 10, 11] },
{ key: 'zombie_walk_up', frames: [12, 13, 14, 15] },
];
for (const anim of anims) {
if (this.scene.anims.exists(anim.key)) continue;
this.scene.anims.create({
key: anim.key,
frames: this.scene.anims.generateFrameNumbers('zombie_shambler', {
frames: anim.frames,
}),
frameRate: fps,
repeat: -1,
});
}
console.log('🧟 Zombie animations created');
}
// ─────────────────────────────────────────────────────────────────────────
// SPAWN TIMER — Zaženi ob noč eventa
// ─────────────────────────────────────────────────────────────────────────
startNightSpawning() {
if (this.spawnTimer) return;
this.spawnTimer = this.scene.time.addEvent({
delay: 12000, // Vsakih 12s poskusi spawnat (ponoči)
loop: true,
callback: () => {
if (this.zombies.length < this.maxZombies) {
this.spawnZombie();
}
}
});
// Takoj spawni prvega
this.scene.time.delayedCall(2000, () => {
if (this.zombies.length < this.maxZombies) this.spawnZombie();
});
console.log('🌙 Zombiji se začnejo spawnat!');
}
stopNightSpawning() {
if (this.spawnTimer) {
this.spawnTimer.remove();
this.spawnTimer = null;
}
}
// ─────────────────────────────────────────────────────────────────────────
// SPAWN POSAMIČNEGA ZOMBIJA
// ─────────────────────────────────────────────────────────────────────────
spawnZombie(x = null, y = null) {
// Spawn na naključnem robu otoka
if (x === null || y === null) {
const edge = Phaser.Math.Between(0, 3);
const margin = 200;
switch (edge) {
case 0: // Zgornji rob
x = this.islandX + Phaser.Math.Between(margin, this.islandW - margin);
y = this.islandY + margin;
break;
case 1: // Desni rob
x = this.islandX + this.islandW - margin;
y = this.islandY + Phaser.Math.Between(margin, this.islandH - margin);
break;
case 2: // Spodnji rob
x = this.islandX + Phaser.Math.Between(margin, this.islandW - margin);
y = this.islandY + this.islandH - margin;
break;
case 3: // Levi rob
x = this.islandX + margin;
y = this.islandY + Phaser.Math.Between(margin, this.islandH - margin);
break;
}
}
const sprite = this.scene.add.sprite(x, y, 'zombie_shambler', 0)
.setScale(0.55) // 160px × 0.55 ≈ 88px — primerljivo s Kai-jem
.setOrigin(0.5, 1.0)
.setDepth(y);
// Pop-in z mgle
sprite.setAlpha(0);
this.scene.tweens.add({
targets: sprite,
alpha: 1,
duration: 800,
ease: 'Sine.in',
});
sprite.play('zombie_walk_down');
// Rdeč halo efekt
const halo = this.scene.add.graphics().setDepth(y - 1);
halo.fillStyle(0xff0000, 0.15);
halo.fillCircle(x, y - 20, 35);
const zombie = {
sprite,
halo,
x, y,
state: 'wandering', // 'wandering' | 'chasing' | 'tamed'
tamed: false,
hp: 30,
maxHp: 30,
dir: 'down',
wanderTimer: 0,
wanderDirX: 0,
wanderDirY: 1,
};
this.zombies.push(zombie);
console.log(`🧟 Zombi spawnan @ ${Math.round(x)},${Math.round(y)}. Skupaj: ${this.zombies.length}`);
return zombie;
}
// ─────────────────────────────────────────────────────────────────────────
// UPDATE — Kliči vsak frame iz scene.update()
// ─────────────────────────────────────────────────────────────────────────
update(kai, delta) {
this._alfaGfx.clear();
const nearestHostile = this._getNearestHostile(kai);
// === ALFA MOČ [SPACE] ===
if (nearestHostile && Phaser.Math.Distance.Between(
kai.x, kai.y, nearestHostile.sprite.x, nearestHostile.sprite.y
) < this.alfaRange) {
if (this.keySpace.isDown) {
this._alfaProgress += delta;
this._drawAlfaBar(kai, nearestHostile);
if (this._alfaProgress >= this.tamingTime) {
this._tameZombie(nearestHostile, kai);
this._alfaProgress = 0;
}
} else {
this._alfaProgress = Math.max(0, this.alfaProgress - delta * 2);
}
// Hint tekst
if (!this._spaceHint || !this._spaceHint.active) {
this._spaceHint = this._showHint(
nearestHostile.sprite.x,
nearestHostile.sprite.y - 80,
'[SPACE] Alfa Moč!', '#ff4444'
);
}
} else {
this._alfaProgress = 0;
if (this._spaceHint) { this._spaceHint.destroy(); this._spaceHint = null; }
}
// === POSODOBI VSE ZOMBIJE ===
for (let i = this.zombies.length - 1; i >= 0; i--) {
const z = this.zombies[i];
if (!z.sprite || !z.sprite.active) {
this.zombies.splice(i, 1);
continue;
}
this._updateZombie(z, kai, delta);
}
}
// ─────────────────────────────────────────────────────────────────────────
// POSODOBI POSAMIČNEGA ZOMBIJA
// ─────────────────────────────────────────────────────────────────────────
_updateZombie(z, kai, delta) {
const dt = delta / 1000;
const speed = z.tamed ? this.zombieSpeed * 1.3 : this.zombieSpeed;
const distToKai = Phaser.Math.Distance.Between(
z.sprite.x, z.sprite.y, kai.x, kai.y
);
let moveX = 0, moveY = 0;
if (z.tamed) {
// === UDOMAČEN: sledi Kai-u na razdalji 80px ===
if (distToKai > 80) {
const angle = Phaser.Math.Angle.Between(z.sprite.x, z.sprite.y, kai.x, kai.y);
moveX = Math.cos(angle) * speed * dt;
moveY = Math.sin(angle) * speed * dt;
}
// Zeleni halo za udomačene
z.halo.clear();
z.halo.fillStyle(0x44ff88, 0.2);
z.halo.fillCircle(z.sprite.x, z.sprite.y - 20, 35);
} else if (distToKai < 450) {
// === ZAZNALI KAI-A: Lovijo ===
z.state = 'chasing';
const angle = Phaser.Math.Angle.Between(z.sprite.x, z.sprite.y, kai.x, kai.y);
moveX = Math.cos(angle) * speed * dt;
moveY = Math.sin(angle) * speed * dt;
// Rdeči halo (nevarnost)
z.halo.clear();
z.halo.fillStyle(0xff0000, 0.18);
z.halo.fillCircle(z.sprite.x, z.sprite.y - 20, 40);
} else {
// === TAVANJE ===
z.state = 'wandering';
z.wanderTimer -= delta;
if (z.wanderTimer <= 0) {
const angle = Math.random() * Math.PI * 2;
z.wanderDirX = Math.cos(angle);
z.wanderDirY = Math.sin(angle);
z.wanderTimer = 2000 + Math.random() * 3000;
}
moveX = z.wanderDirX * speed * 0.5 * dt;
moveY = z.wanderDirY * speed * 0.5 * dt;
// Subtilen rdeči halo
z.halo.clear();
z.halo.fillStyle(0xff2200, 0.08);
z.halo.fillCircle(z.sprite.x, z.sprite.y - 20, 30);
}
// === PREMAKNI ===
z.sprite.x += moveX;
z.sprite.y += moveY;
z.halo.x = z.sprite.x - (z.halo.x || 0);
// Depth sorting
z.sprite.setDepth(z.sprite.y);
// === ANIMACIJA po smeri ===
const absX = Math.abs(moveX), absY = Math.abs(moveY);
let newDir = z.dir;
if (absX > 0.01 || absY > 0.01) {
if (absY > absX) {
newDir = moveY > 0 ? 'down' : 'up';
} else {
newDir = moveX > 0 ? 'right' : 'left';
}
}
if (newDir !== z.dir) {
z.dir = newDir;
const animKey = `zombie_walk_${newDir}`;
if (z.sprite.anims.currentAnim?.key !== animKey) {
z.sprite.play(animKey, true);
}
}
// Otok bounds
z.sprite.x = Phaser.Math.Clamp(z.sprite.x, this.islandX, this.islandX + this.islandW);
z.sprite.y = Phaser.Math.Clamp(z.sprite.y, this.islandY, this.islandY + this.islandH);
}
// ─────────────────────────────────────────────────────────────────────────
// ALFA MOČ — Udomačitev
// ─────────────────────────────────────────────────────────────────────────
_tameZombie(zombie, kai) {
zombie.tamed = true;
zombie.state = 'tamed';
// Efekt: zlata eksplozija
this._alfaExplosion(zombie.sprite.x, zombie.sprite.y);
// Obarvi v zlato (shimmer)
zombie.sprite.setTint(0xffd700);
this.scene.time.delayedCall(800, () => {
zombie.sprite.clearTint();
});
// Notify scene
this.scene.events.emit('zombieTamed', { zombie });
this._showFloating(zombie.sprite.x, zombie.sprite.y - 60,
'✨ Udomačen!', '#ffd700', 22);
console.log(`✨ Zombi udomačen!`);
}
_drawAlfaBar(kai, zombie) {
const pct = this._alfaProgress / this.tamingTime;
const bx = zombie.sprite.x - 40;
const by = zombie.sprite.y - 110;
// Ozadje
this._alfaGfx.fillStyle(0x000000, 0.6);
this._alfaGfx.fillRect(bx, by, 80, 12);
// Fill
this._alfaGfx.fillStyle(0xff4444, 1.0);
this._alfaGfx.fillRect(bx + 1, by + 1, (80 - 2) * pct, 10);
// Okvir
this._alfaGfx.lineStyle(1, 0xffffff, 0.8);
this._alfaGfx.strokeRect(bx, by, 80, 12);
}
_alfaExplosion(x, y) {
const gfx = this.scene.add.graphics().setDepth(9010);
let r = 5, alpha = 1.0;
const ev = this.scene.time.addEvent({
delay: 30,
repeat: 15,
callback: () => {
gfx.clear();
gfx.lineStyle(3, 0xffd700, alpha);
gfx.strokeCircle(x, y - 30, r);
gfx.strokeCircle(x, y - 30, r * 0.6);
r += 8; alpha -= 0.06;
if (r > 100) { gfx.destroy(); ev.remove(); }
}
});
// Zlatni prah
for (let i = 0; i < 8; i++) {
const angle = (i / 8) * Math.PI * 2;
const particle = this.scene.add.graphics().setDepth(9011);
particle.fillStyle(0xffd700, 1.0);
particle.fillCircle(x, y - 30, 4);
this.scene.tweens.add({
targets: particle,
x: particle.x + Math.cos(angle) * 60,
y: particle.y + Math.sin(angle) * 60,
alpha: 0,
duration: 600,
ease: 'Quad.out',
onComplete: () => particle.destroy(),
});
}
}
// ─────────────────────────────────────────────────────────────────────────
// POMOŽNE
// ─────────────────────────────────────────────────────────────────────────
_getNearestHostile(kai) {
let nearest = null, minDist = this.alfaRange * 1.5;
for (const z of this.zombies) {
if (z.tamed) continue;
const d = Phaser.Math.Distance.Between(kai.x, kai.y, z.sprite.x, z.sprite.y);
if (d < minDist) { minDist = d; nearest = z; }
}
return nearest;
}
_showHint(x, y, msg, color) {
return this.scene.add.text(x, y, msg, {
fontFamily: 'Arial Black',
fontSize: '14px',
color,
stroke: '#000000',
strokeThickness: 4,
backgroundColor: '#00000088',
padding: { x: 6, y: 3 },
}).setOrigin(0.5).setDepth(9000);
}
_showFloating(x, y, msg, color, size = 20) {
const txt = this.scene.add.text(x, y, msg, {
fontFamily: 'Arial Black',
fontSize: `${size}px`,
color,
stroke: '#000000',
strokeThickness: 4,
}).setOrigin(0.5).setDepth(9999);
this.scene.tweens.add({
targets: txt,
y: y - 60,
alpha: { from: 1, to: 0 },
duration: 1800,
onComplete: () => txt.destroy(),
});
}
// ─────────────────────────────────────────────────────────────────────────
// PUBLIC API
// ─────────────────────────────────────────────────────────────────────────
getZombies() { return this.zombies; }
getTamedZombies() { return this.zombies.filter(z => z.tamed); }
getHostiles() { return this.zombies.filter(z => !z.tamed); }
removeAll() {
this.zombies.forEach(z => { z.sprite?.destroy(); z.halo?.destroy(); });
this.zombies = [];
}
destroy() {
this.removeAll();
this.stopNightSpawning();
this._alfaGfx?.destroy();
this._spaceHint?.destroy();
}
}