diff --git a/FAZA_10_CHECKLIST.md b/FAZA_10_CHECKLIST.md
new file mode 100644
index 0000000..fbd5ea9
--- /dev/null
+++ b/FAZA_10_CHECKLIST.md
@@ -0,0 +1,64 @@
+# FAZA 10: Ekonomija in Trgovina - Checklist
+
+**Status:** ✅ ZAKLJUČENO
+
+**Datum:** 2025-12-06
+
+---
+
+## ✅ Opravila (Developer)
+
+- [x] Implementacija `InventorySystem` (Gold Tracking):
+ - [x] Shranjevanje zlata (Gold).
+ - [x] UI prikaz zlata (desno zgoraj).
+- [x] NPC Interakcija (`InteractionSystem.js`):
+ - [x] Detekcija klika na NPC-ja (povečan radij).
+ - [x] Identifikacija 'merchant' tipa.
+- [x] Trgovina Logika:
+ - [x] Prodaja: Wheat -> Gold (5g/item).
+ - [x] Nakup: Gold -> Seeds (10g/5 items).
+ - [x] Visual feedback (+Gold/-Gold text popup).
+- [x] Integracija v GameScene.
+
+---
+
+## 🧪 Ročno testiranje (Naročnik)
+
+### Test 1: Prodaja
+**Ukaz:** Imej pridelke (Wheat) in klikni na NPC-ja (Merchanta).
+
+**Pričakovani rezultat:**
+- [x] Zlato se poveča (+5 na item).
+- [x] Pridelki izginejo iz inventarja.
+
+### Test 2: Nakup
+**Ukaz:** Bodi brez pšenice, imej zlato (>10) in klikni na NPC-ja.
+
+**Pričakovani rezultat:**
+- [x] Zlato se zmanjša (-10).
+- [x] Število semen v inventarju se poveča (+5).
+
+---
+
+## 📋 Potrditev Naročnika
+
+```
+FAZA 10: [STATUS]
+- Testirano: [DA]
+- Datum testiranja: 2025-12-06
+- Opombe: Uporabnik potrdil: "dela"
+
+ODOBRENO ZA FAZO 11: [DA]
+
+Podpis naročnika: User
+```
+
+---
+
+## ➡️ Naslednji koraki (po odobritvi)
+
+Ko naročnik potrdi FAZO 10, se začne:
+**FAZA 11: Gradnja in Obnova (Building)**
+- Poraba materialov (Wood, Stone, Gold) za gradnjo.
+- Postavljanje objektov na mrežo (npr. Ograja, Hiša).
+- UI za izbiro gradnje.
diff --git a/FAZA_11_CHECKLIST.md b/FAZA_11_CHECKLIST.md
new file mode 100644
index 0000000..b97e9d9
--- /dev/null
+++ b/FAZA_11_CHECKLIST.md
@@ -0,0 +1,68 @@
+# FAZA 11: Gradnja in Obnova (Building) - Checklist
+
+**Status:** ✅ ZAKLJUČENO
+
+**Datum:** 2025-12-06
+
+---
+
+## ✅ Opravila (Developer)
+
+- [x] Implementacija `BuildingSystem.js`:
+ - [x] Build Mode (Toggle 'B').
+ - [x] Menu za izbiro (UI overlay).
+ - [x] Preverjanje materialov (Wood/Stone/Gold).
+ - [x] Logika postavitve objekta (Tile validacija).
+- [x] Novi Objekti Sprites (`TextureGenerator`):
+ - [x] Fence (Ograja).
+ - [x] Wall (Zid).
+ - [x] House (Hiša).
+- [x] Integracija s TerrainSystem:
+ - [x] `placeStructure` metoda za dodajanje dekoracij.
+- [x] Integracija s GameScene:
+ - [x] Input mapping (1, 2, 3 za izbiro v Build Mode).
+
+---
+
+## 🧪 Ročno testiranje (Naročnik)
+
+### Test 1: Meni za Gradnjo
+**Ukaz:** Pritisni 'B'.
+
+**Pričakovani rezultat:**
+- [x] Odpre se "BUILD MODE" meni.
+
+### Test 2: Postavitev Ograje
+**Ukaz:** Pritisni '1' (za Fence) in klikni na prazno travo (imej vsaj 2 Wood).
+
+**Pričakovani rezultat:**
+- [x] Ograja se pojavi.
+- [x] Les se odšteje.
+- [x] "Built Fence!" sporočilo.
+
+---
+
+## 📋 Potrditev Naročnika
+
+```
+FAZA 11: [STATUS]
+- Testirano: [DA]
+- Datum testiranja: 2025-12-06
+- Opombe: Uporabnik potrdil: "top dela"
+
+ODOBRENO ZA FAZO 12: [DA]
+
+Podpis naročnika: User
+```
+
+---
+
+## ➡️ Naslednji koraki (po odobritvi)
+
+Ko naročnik potrdi FAZO 11, se začne:
+**FAZA 12: Napredno Shranjevanje (Persistence)**
+- Nadgradnja `SaveSystem.js`.
+- Shranjevanje Inventarja & Zlata.
+- Shranjevanje Kmetije (Pridelki).
+- Shranjevanje Zgrajenih objektov.
+- Testiranje Loadinga (da hiša ostane tam, kjer je bila).
diff --git a/FAZA_12_CHECKLIST.md b/FAZA_12_CHECKLIST.md
new file mode 100644
index 0000000..dfaa010
--- /dev/null
+++ b/FAZA_12_CHECKLIST.md
@@ -0,0 +1,54 @@
+# FAZA 12: Napredno Shranjevanje (Persistence) - Checklist
+
+**Status:** ✅ ZAKLJUČENO
+
+**Datum:** 2025-12-06
+
+---
+
+## ✅ Opravila (Developer)
+
+- [x] Nadgradnja `SaveSystem.js`:
+ - [x] **Inventar:** Shranjujejo se sloti in gold.
+ - [x] **Teren:** Shranjujejo se dinamični objekti (ograje, hiše, rože).
+ - [x] **Kmetija:** Shranjujejo se pridelki (faza rasti).
+ - [x] **Statistika & Čas:** Shranjeno.
+- [x] Loading Logic:
+ - [x] Čiščenje scene pred nalaganjem (preprečevanje duplikatov).
+ - [x] Ponovna obnova sveta iz Seeda + Naloženih sprememb.
+
+---
+
+## 🧪 Ročno testiranje (Naročnik)
+
+### Test: Save & Reload
+**Ukaz:** Zgradi, Zasluži, Shrani (F5), Osveži, Naloži (F9).
+
+**Pričakovani rezultat:**
+- [x] Vse strukture in pridelke so na svojem mestu.
+- [x] Zlato je povrnjeno.
+
+---
+
+## 📋 Potrditev Naročnika
+
+```
+FAZA 12: [STATUS]
+- Testirano: [DA]
+- Datum testiranja: 2025-12-06
+- Opombe: Uporabnik potrdil: "vse je ok"
+
+ODOBRENO ZA FAZO 13: [DA]
+
+Podpis naročnika: User
+```
+
+---
+
+## ➡️ Naslednji koraki (po odobritvi)
+
+Ko naročnik potrdi FAZO 12, se začne:
+**FAZA 13: Zombi Delavec (The Alpha System)**
+- Implementacija AI za zombija.
+- Krotenje (Follow/Stay komande).
+- Prva avtomatizacija (npr. Zombi sledi in napada ali pa samo stoji).
diff --git a/FAZA_13_CHECKLIST.md b/FAZA_13_CHECKLIST.md
new file mode 100644
index 0000000..de08632
--- /dev/null
+++ b/FAZA_13_CHECKLIST.md
@@ -0,0 +1,58 @@
+# FAZA 13: Zombi Delavec (The Alpha System) - Checklist
+
+**Status:** ✅ ZAKLJUČENO
+
+**Datum:** 2025-12-06
+
+---
+
+## ✅ Opravila (Developer)
+
+- [x] Zombi AI:
+ - [x] Spremeni obnašanje NPC Zombija.
+ - [x] Stanja: `Idle` (tava), `Follow` (sledi Alfi), `Work` (kopanje/sekanje).
+- [x] Interakcija (Krotenje):
+ - [x] Klik na zombija preklopi med "Sledi mi" (Follow), "Delaj" (Work) in "Straži" (Stay).
+ - [x] **NOVO:** Work način samodejno išče in uničuje vire (grme, drevesa).
+- [x] Vizualni feedback:
+ - [x] Ikona nad glavo zombija (! ali ?), ko dobi ukaz.
+
+---
+
+## 🧪 Ročno testiranje (Naročnik)
+
+### Test 1: Sledenje
+**Ukaz:** Klikni 1x na zombija.
+**Rezultat:** Oko 👁️. Zombi sledi.
+
+### Test 2: Delo (Novo!)
+**Ukaz:** Klikni 2x na zombija.
+**Rezultat:** Kramp ⛏️. Zombi gre do grma in ga uniči.
+
+### Test 3: Straža
+**Ukaz:** Klikni 3x na zombija.
+**Rezultat:** Ščit 🛡️. Zombi stoji pri miru.
+
+---
+
+## 📋 Potrditev Naročnika
+
+```
+FAZA 13: [STATUS]
+- Testirano: [DA]
+- Datum testiranja: 2025-12-06
+- Opombe: Uporabnik potrdil: "dela"
+
+ODOBRENO ZA FAZO 14: [DA]
+
+Podpis naročnika: User
+```
+
+---
+
+## ➡️ Naslednji koraki
+
+**FAZA 14: Obnova Mesta (Town Restoration)**
+- Implementacija sistema "Projektov" (gradbišča).
+- Prvi NPC Quest: Popravilo Kovačeve hiše.
+- UI za oddajo materiala (Les/Kamen).
diff --git a/FAZA_14_CHECKLIST.md b/FAZA_14_CHECKLIST.md
new file mode 100644
index 0000000..9585859
--- /dev/null
+++ b/FAZA_14_CHECKLIST.md
@@ -0,0 +1,62 @@
+# FAZA 14: Obnova Mesta (Town Restoration) - Checklist
+
+**Status:** ✅ ZAKLJUČENO
+
+**Datum:** 2025-12-06
+
+---
+
+## 🎯 Cilj
+Implementirati sistem "Projektov" za obnovo ruševin. Igralec mora zbrati materiale in jih dostaviti na gradbišče, da popravi hišo in odklene NPC-ja/Trgovino.
+
+## ✅ Opravila (Developer)
+
+- [x] **Sistem Ruševin (Ruins System):**
+ - [x] Dodati nov tip strukture: `Ruin` (Ruševina).
+ - [x] Interakcija z ruševino odpre meni "Projekt Obnove".
+- [x] **UI Projekta:**
+ - [x] Prikaz zahtevanih materialov (npr. 50 Lesa, 20 Kamna).
+ - [x] Gumb "Prispevaj" (Contribute).
+- [x] **Transformacija:**
+ - [x] Ko je projekt končan -> Ruševina se spremeni v `House` (ali `Smithy`).
+ - [x] Odklene se NPC (Trgovec se pojavi).
+- [x] **Prvi Quest: Kovačeva Delavnica:**
+ - [x] Postaviti ruševino na mapo (x:55, y:55).
+ - [x] Zahteva: 20 Lesa, 10 Kamna (za testiranje smo dali inventar).
+
+---
+
+## 🧪 Ročno testiranje (Naročnik)
+
+### Test 1: Donacija
+**Ukaz:** Zberi les/kamen (dobljen v inventar), klikni na ruševino, klikni "Prispevaj".
+**Rezultat:** Material se odšteje.
+
+### Test 2: Dokončanje
+**Ukaz:** Klikni Contribute.
+**Rezultat:** Ruševina postane lepa hiša. Pojavi se Trgovec.
+
+---
+
+## 📋 Potrditev Naročnika
+
+```
+FAZA 14: [STATUS]
+- Testirano: [DA]
+- Datum testiranja: 2025-12-06
+- Opombe: Uporabnik potrdil: "da dela"
+
+ODOBRENO ZA FAZO 15: [DA]
+
+Podpis naročnika: User
+```
+
+---
+
+## ➡️ Naslednji koraki
+
+**FAZA 15: Ekonomija (Economy System)**
+- Prodaja pridelkov (Wheat -> Gold).
+- Nakup semen (Gold -> Seeds).
+- Trgovina UI (Buy/Sell menu).
+- Nadgradnja orodij (Gold + Resources).
diff --git a/FAZA_15_CHECKLIST.md b/FAZA_15_CHECKLIST.md
new file mode 100644
index 0000000..e84bd05
--- /dev/null
+++ b/FAZA_15_CHECKLIST.md
@@ -0,0 +1,379 @@
+# FAZA 15-17 + Custom Assets + 2.5D Terrain: Advanced Systems - Checklist
+
+**Status:** ✅ ZAKLJUČENO
+
+**Datum:** 2025-12-06/07
+
+---
+
+## 🎯 Cilji
+- **FAZA 15:** Ekonomija (Trading, Gold, Materials)
+- **FAZA 16:** Weather & Open World (Rain, Fog, Hills)
+- **FAZA 17:** Sound, Parallax, Friendship
+- **BONUS:** Custom Sprite Integration
+- **BONUS:** 2.5D Minecraft-Style Terrain
+
+## ✅ Opravila (Developer)
+
+### FAZA 15: Economy
+- [x] **Trgovanje (Trading System):**
+ - [x] Interakcija s Trgovcem odpre `TradeMenu`
+ - [x] Seznam predmetov za nakup/prodajo
+ - [x] Gold/Currency sistem
+- [x] **Town Restoration:**
+ - [x] Multiple material requirements (Wood, Stone, Gold)
+ - [x] Different ruin types with different costs
+ - [x] Friendship/Hearts system (❤️)
+ - [x] User feedback messages
+
+### FAZA 16: Weather & World
+- [x] **Weather System:**
+ - [x] Rain effect (100-150 droplets)
+ - [x] Fog effect (gray overlay)
+ - [x] Storm (heavy rain)
+ - [x] Automatic weather cycling (30s)
+- [x] **Terrain Enhancement:**
+ - [x] Elevation/Hills sistem (Perlin Noise)
+ - [x] Height-based grass tinting (light = hills, dark = valleys)
+ - [x] Y-offset based on elevation (-25px max)
+ - [x] More rocks/bushes on hills
+
+### FAZA 17: Polish
+- [x] **Sound System:**
+ - [x] SoundManager class
+ - [x] Placeholder beep sounds (chop, pickup, plant, harvest, build)
+ - [x] Rain ambient sounds
+ - [x] Mute toggle (M key)
+- [x] **Parallax Layers:**
+ - [x] Layer 1: Sky + Distant Hills (0.2x scroll)
+ - [x] Layer 2: Far Trees (0.7x scroll)
+ - [x] Layer 3: Game objects (1.0x normal)
+ - [x] Layer 4: Foreground grass (1.05x scroll)
+ - [x] Smart fading (grass becomes transparent near player)
+- [x] **Camera:**
+ - [x] Viewport: 640x360 (pixel-perfect)
+ - [x] Instant follow (1.0 speed)
+ - [x] 100px deadzone
+ - [x] Round pixels enabled
+- [x] **Day/Night Cycle:**
+ - [x] Dynamic lighting overlays
+ - [x] Dawn, Day, Dusk, Night phases
+ - [x] Color tinting based on time
+
+### BONUS: Custom Sprite Integration
+- [x] **Character Sprites:**
+ - [x] Player custom sprite (protagonist with dreadlocks)
+ - [x] Zombie custom sprite (green skin, red eyes)
+ - [x] Merchant custom sprite (wizard with gold coin)
+ - [x] All characters scaled to 0.2 (20% size)
+- [x] **Environment Assets:**
+ - [x] Custom tree sprite (blue tree)
+ - [x] Custom stone/rock sprite
+ - [x] Custom grass tile texture
+ - [x] Wheat sprite
+ - [x] Leaf sprite
+- [x] **Asset Packs Loaded:**
+ - [x] objects_pack.png (furniture, barrels, gravestones)
+ - [x] walls_pack.png (walls, arches)
+ - [x] ground_tiles.png (terrain textures)
+ - [x] objects_pack2.png (additional objects)
+ - [x] trees_vegetation.png (trees, bushes)
+- [x] **Gravestone System:**
+ - [x] Extract gravestone from objects_pack atlas
+ - [x] Random spawning (0.5% chance on grass)
+ - [x] 10 HP (harder to destroy)
+ - [x] Zombie post-apocalyptic atmosphere
+- [x] **Transparency Processing:**
+ - [x] Auto-remove white/gray backgrounds
+ - [x] Auto-remove brown backgrounds (merchant)
+ - [x] Canvas willReadFrequently optimization
+- [x] **Performance Fixes:**
+ - [x] Fixed Canvas2D warnings (willReadFrequently: true)
+ - [x] Electron CSP already configured
+
+### BONUS: 2.5D Minecraft-Style Terrain ⛏️
+- [x] **Volumetric Blocks:**
+ - [x] Block thickness: 25px (2.5x thicker than before)
+ - [x] Left side shading: 30% darker
+ - [x] Right side shading: 50% darker (strong shadow)
+ - [x] Crisp black outlines for definition
+- [x] **Grass Blocks:**
+ - [x] Green top surface
+ - [x] Brown (dirt) side faces
+ - [x] Authentic Minecraft aesthetic
+- [x] **Rendering:**
+ - [x] Canvas renderer for pixel-perfect sharpness
+ - [x] CSS image-rendering: crisp-edges / pixelated
+ - [x] No antialiasing
+ - [x] Round pixels enabled
+- [x] **Hybrid Style:**
+ - [x] Terrain = 2.5D volumetric (Minecraft-like)
+ - [x] Characters = 2D flat sprites (pixel art)
+ - [x] Objects = 2D flat sprites (pixel art)
+
+---
+
+## 🧪 Ročno testiranje (Naročnik)
+
+### Test 1: 2.5D Terrain **⛏️ NOVO**
+**Ukaz:** Pritisni F4, opazuj teren.
+**Rezultat:**
+- Grass bloki imajo zeleno površino + rjave stranice
+- Bloki so debeli (25px thickness)
+- Močno senčenje na straneh (Minecraft-like)
+- Vse ostalo (karakterji, drevesa) je 2D flat
+
+### Test 2: Pixel-Perfect Ostrino **⛏️ NOVO**
+**Ukaz:** Pritisni F4, zoom-aj v karakterje.
+**Rezultat:**
+- Vsak pixel je oster in jasen
+- Ni zamegljenih robov
+- Crisp pixel art aesthetic
+
+### Test 3: Custom Sprites
+**Ukaz:** Pritisni F4 (Soft Reset).
+**Rezultat:**
+- Player = Custom protagonist sprite (20% velikosti)
+- Zombie = Custom zombie sprite (20% velikosti)
+- Merchant = Custom merchant sprite (20% velikosti)
+- Drevesa = Modro drevo sprite
+- Kamenje = Custom rock sprite
+- Travniki = Custom grass texture
+
+### Test 4: Gravestone Spawning
+**Ukaz:** Premakni se po zemljevidu, išči nagrobike.
+**Rezultat:** Redki nagrobniki (💀) na travniku, težje uničiti (10 HP).
+
+### Test 5: Transparency
+**Ukaz:** Opazuj vse sprite.
+**Rezultat:** Brez belega/rjavega ozadja, popolna transparentnost.
+
+### Test 6: Performance
+**Ukaz:** Odpri F12 Console.
+**Rezultat:** Ni več Canvas2D warnings.
+
+### Test 7: Town Restoration
+**Ukaz:** Dodaj materials v inventory (F12 console):
+```js
+this.scene.inventorySystem.addItem('wood', 200);
+this.scene.inventorySystem.addItem('stone', 100);
+this.scene.inventorySystem.addItem('gold', 100);
+```
+Klikni na ruin, klikni "Contribute".
+**Rezultat:** Ruin postane House, spawne NPC, +10 Hearts ❤️
+
+### Test 8: Weather
+**Ukaz:** Počakaj 30s v igri.
+**Rezultat:** Vreme se spremeni (dež/megla/jasno)
+
+### Test 9: Sound
+**Ukaz:** Sekaj drevo, poberi loot.
+**Rezultat:** Placeholder beep zvoki. M = mute/unmute.
+
+---
+
+## 📋 Potrditev Naročnika
+
+```
+FAZA 15-17 + Custom Assets + 2.5D: [STATUS]
+- Testirano: [DA/NE]
+- Datum testiranja: ___________
+- Opombe:
+
+ODOBRENO ZA NASLEDNJO FAZO: [DA/NE]
+```
+
+---
+
+## ➡️ Naslednji koraki
+
+**FAZA 18:** Combat System (Attack, Damage, Zombie AI)
+**FAZA 19:** Quest System (Tasks, Objectives, Rewards)
+**FAZA 20:** Building System (Use asset packs for construction)
+**FAZA 21:** Final Polish & Optimization
+
+---
+
+## 📊 Tehnični Pregled
+
+**Rendering:**
+- Canvas renderer (pixel-perfect)
+- 640x360 viewport
+- CSS crisp-edges
+- No antialiasing
+
+**Terrain:**
+- 2.5D isometric blocks
+- 25px thickness (Minecraft-style)
+- Procedural generation (Perlin Noise)
+- Custom grass tiles support
+
+**Characters:**
+- 2D flat sprites
+- 0.2 scale (20% size)
+- Custom sprite support
+- Auto-transparency processing
+
+**Assets:**
+- 11 custom sprites loaded
+- 5 asset packs (objects, walls, tiles, vegetation)
+- Gravestone extraction system
+- Sprite atlas support
+
+**Performance:**
+- Canvas willReadFrequently optimization
+- Object pooling (tiles, decorations, crops)
+- Frustum culling
+- No memory leaks
+
+## ✅ Opravila (Developer)
+
+### FAZA 15: Economy
+- [x] **Trgovanje (Trading System):**
+ - [x] Interakcija s Trgovcem odpre `TradeMenu`
+ - [x] Seznam predmetov za nakup/prodajo
+ - [x] Gold/Currency sistem
+- [x] **Town Restoration:**
+ - [x] Multiple material requirements (Wood, Stone, Gold)
+ - [x] Different ruin types with different costs
+ - [x] Friendship/Hearts system (❤️)
+ - [x] User feedback messages
+
+### FAZA 16: Weather & World
+- [x] **Weather System:**
+ - [x] Rain effect (100-150 droplets)
+ - [x] Fog effect (gray overlay)
+ - [x] Storm (heavy rain)
+ - [x] Automatic weather cycling (30s)
+- [x] **Terrain Enhancement:**
+ - [x] Elevation/Hills sistem (Perlin Noise)
+ - [x] Height-based grass tinting (light = hills, dark = valleys)
+ - [x] Y-offset based on elevation (-25px max)
+ - [x] More rocks/bushes on hills
+
+### FAZA 17: Polish
+- [x] **Sound System:**
+ - [x] SoundManager class
+ - [x] Placeholder beep sounds (chop, pickup, plant, harvest, build)
+ - [x] Rain ambient sounds
+ - [x] Mute toggle (M key)
+- [x] **Parallax Layers:**
+ - [x] Layer 1: Sky + Distant Hills (0.2x scroll)
+ - [x] Layer 2: Far Trees (0.7x scroll)
+ - [x] Layer 3: Game objects (1.0x normal)
+ - [x] Layer 4: Foreground grass (1.05x scroll)
+ - [x] Smart fading (grass becomes transparent near player)
+- [x] **Camera:**
+ - [x] Viewport: 640x360 (pixel-perfect)
+ - [x] Instant follow (1.0 speed)
+ - [x] 100px deadzone
+ - [x] Round pixels enabled
+- [x] **Day/Night Cycle:**
+ - [x] Dynamic lighting overlays
+ - [x] Dawn, Day, Dusk, Night phases
+ - [x] Color tinting based on time
+
+### BONUS: Custom Sprite Integration
+- [x] **Character Sprites:**
+ - [x] Player custom sprite (protagonist with dreadlocks)
+ - [x] Zombie custom sprite (green skin, red eyes)
+ - [x] Merchant custom sprite (wizard with gold coin)
+ - [x] All characters scaled to 0.2 (20% size)
+- [x] **Environment Assets:**
+ - [x] Custom tree sprite (blue tree)
+ - [x] Custom stone/rock sprite
+ - [x] Custom grass tile texture
+ - [x] Wheat sprite
+ - [x] Leaf sprite
+- [x] **Asset Packs Loaded:**
+ - [x] objects_pack.png (furniture, barrels, gravestones)
+ - [x] walls_pack.png (walls, arches)
+ - [x] ground_tiles.png (terrain textures)
+ - [x] objects_pack2.png (additional objects)
+ - [x] trees_vegetation.png (trees, bushes)
+- [x] **Gravestone System:**
+ - [x] Extract gravestone from objects_pack atlas
+ - [x] Random spawning (0.5% chance on grass)
+ - [x] 10 HP (harder to destroy)
+ - [x] Zombie post-apocalyptic atmosphere
+- [x] **Transparency Processing:**
+ - [x] Auto-remove white/gray backgrounds
+ - [x] Auto-remove brown backgrounds (merchant)
+ - [x] Canvas willReadFrequently optimization
+- [x] **Performance Fixes:**
+ - [x] Fixed Canvas2D warnings (willReadFrequently: true)
+ - [x] Electron CSP already configured
+
+---
+
+## 🧪 Ročno testiranje (Naročnik)
+
+### Test 1: Custom Sprites
+**Ukaz:** Pritisni F4 (Soft Reset).
+**Rezultat:**
+- Player = Custom protagonist sprite (20% velikosti)
+- Zombie = Custom zombie sprite (20% velikosti)
+- Merchant = Custom merchant sprite (20% velikosti)
+- Drevesa = Modro drevo sprite
+- Kamenje = Custom rock sprite
+- Travn iki = Custom grass texture
+
+### Test 2: Gravestone Spawning
+**Ukaz:** Premakni se po zemljevidu, išči nagrobike.
+**Rezultat:** Redki nagrobniki (💀) na trav niku, težje uničiti (10 HP).
+
+### Test 3: Transparency
+**Ukaz:** Opazuj vse sprite.
+**Rezultat:** Brez belega/rjavega ozadja, popolna transparentnost.
+
+### Test 4: Performance
+**Ukaz:** Odpri F12 Console.
+**Rezultat:** Ni več Canvas2D warnings.
+
+### Test 5: Town Restoration
+**Ukaz:** Dodaj materials v inventory (F12 console):
+```js
+this.scene.inventorySystem.addItem('wood', 200);
+this.scene.inventorySystem.addItem('stone', 100);
+this.scene.inventorySystem.addItem('gold', 100);
+```
+Klikni na ruin, klikni "Contribute".
+**Rezultat:** Ruin postane House, spawne NPC, +10 Hearts ❤️
+
+### Test 6: Weather
+**Ukaz:** Počakaj 30s v igri.
+**Rezultat:** Vreme se spremeni (dež/megla/jasno)
+
+### Test 7: Sound
+**Ukaz:** Sekaj drevo, poberi loot.
+**Rezultat:** Placeholder beep zvoki. M = mute/unmute.
+
+### Test 8: Parallax
+**Ukaz:** Premakni igralca okoli.
+**Rezultat:** Hribi v ozadju se premikajo počasneje, trava v ospredju hitreje.
+
+### Test 9: Hills
+**Ukaz:** Opazuj zemljevid.
+**Rezultat:** Svetlejša trava = hribi, temnejša = doline. Vizualno dvignjeno.
+
+---
+
+## 📋 Potrditev Naročnika
+
+```
+FAZA 15-17 + Custom Assets: [STATUS]
+- Testirano: [DA/NE]
+- Datum testiranja: ___________
+- Opombe:
+
+ODOBRENO ZA NASLEDNJO FAZO: [DA/NE]
+```
+
+---
+
+## ➡️ Naslednji koraki
+
+**FAZA 18:** Combat System (Attack, Damage, Zombie AI)
+**FAZA 19:** Quest System (Tasks, Objectives, Rewards)
+**FAZA 20:** Building System (Use asset packs for construction)
+**FAZA 21:** Final Polish & Optimization
diff --git a/FAZA_16_CHECKLIST.md b/FAZA_16_CHECKLIST.md
new file mode 100644
index 0000000..b53ee83
--- /dev/null
+++ b/FAZA_16_CHECKLIST.md
@@ -0,0 +1,69 @@
+# FAZA 16: Weather System & Open World - Checklist
+
+**Status:** ✅ ZAKLJUČENO
+
+**Datum:** 2025-12-06
+
+---
+
+## 🎯 Cilj
+Implementirati dinamični vremenski sistem in izboljšati občutek odprtega sveta. To vključuje:
+- Dež, meglo in nevihte
+- Vizualne efekte (dežne kapljice, zatemnitev)
+- Naključne vremenske spremembe
+- Večja, bolj živa mapa
+
+## ✅ Opravila (Developer)
+
+- [x] **Weather System:**
+ - [x] Ustvariti `WeatherSystem.js`
+ - [x] Tipi vremena: `'clear'`, `'rain'`, `'fog'`, `'storm'`
+ - [x] Periodične spremembe (vsakih 30s)
+- [x] **Rain Effect:**
+ - [x] Particle sistem za dež (100-150 kapljic)
+ - [x] Animacija padanja
+ - [x] Zatemnitev zaslona (overlay)
+- [x] **Fog Effect:**
+ - [x] Siv overlay z alpha kanalom
+- [x] **Integration:**
+ - [x] Dodano v `GameScene.js`
+ - [x] Update loop kliče `weatherSystem.update(delta)`
+ - [x] Dodano v `index.html`
+
+---
+
+## 🧪 Ročno testiranje (Naročnik)
+
+### Test 1: Dež
+**Ukaz:** Počakajte v igri ~30s.
+**Rezultat:** Začne deževati (modre črte padajo navzdol). Zaslon se zatemni.
+
+### Test 2: Megla
+**Ukaz:** Počakajte, da vreme se spremeni.
+**Rezultat:** Zaslon postane siv/mističen.
+
+### Test 3: Jasno vreme
+**Ukaz:** Počakajte.
+**Rezultat:** Vse efekte prenehajo.
+
+---
+
+## 📋 Potrditev Naročnika
+
+```
+FAZA 16: [STATUS]
+- Testirano: [DA/NE]
+- Datum testiranja: ___________
+- Opombe:
+
+ODOBRENO ZA NASLEDNJO FAZO: [DA/NE]
+```
+
+---
+
+## ➡️ Naslednji koraki
+
+**FAZA 17:** Sound & Music (Ambient zvoki, glasba za dan/noč)
+**FAZA 18:** Quest System (Naloge, cilji, nagrade)
+**FAZA 19:** NPC Dialog (Pogovor z NPC-ji)
+**FAZA 20:** Polish & Optimization (Optimizacija, zadnji detajli)
diff --git a/FAZA_3_CHECKLIST.md b/FAZA_3_CHECKLIST.md
new file mode 100644
index 0000000..5f3516e
--- /dev/null
+++ b/FAZA_3_CHECKLIST.md
@@ -0,0 +1,121 @@
+# FAZA 3: NPC-ji in Dekoracije - Checklist
+
+**Status:** ✅ PRIPRAVLJEN ZA TESTIRANJE
+
+**Datum:** 2025-12-06
+
+---
+
+## ✅ Opravila (Developer)
+
+- [x] Kreacija NPC entitete (NPC.js)
+- [x] Dodajanje 3 NPC-jev v sceno
+- [x] Random Walk AI za NPC-je
+- [x] Kreacija sprite-ov za dekoracije (rože, grmičevje)
+- [x] Generacija dekoracij na terenu (TerrainSystem.js)
+- [x] Parallax oblaki (GameScene.js)
+- [x] Depth sorting za dekoracije (igralec gre ZA grmom, ČEZ rožo)
+- [x] Posodobitev UI (naslov)
+
+**VSE OPRAVILA ZAKLJUČENA** ✅
+
+---
+
+## 🧪 Ročno testiranje (Naročnik)
+
+### Test 1: NPC-ji
+**Ukaz:** `npm start` -> Opazuj mapo
+
+**Pričakovani rezultat:**
+- [ ] Na mapi so vidni 3 NPC-ji (Zombie, Villager, Merchant)
+- [ ] NPC-ji so različnih barv
+- [ ] NPC-ji se premikajo samostojno (Random Walk)
+- [ ] NPC-ji se ustavijo za trenutek, nato zamenjajo smer
+- [ ] NPC-ji ne gredo skozi robove mape
+
+**Status:** ⏳ ČAKA NA TESTIRANJE
+
+---
+
+### Test 2: Dekoracije (Rože in Grmi)
+**Ukaz:** Razišči mapo
+
+**Pričakovani rezultat:**
+- [ ] Na travi so vidne rdeče rože (majhen sprite 16x16)
+- [ ] Na travi in zemlji so vidni zeleni grmi (večji sprite 32x32)
+- [ ] Dekoracije se ne pojavijo na vodi ali kamnu (ali zelo redko)
+- [ ] Dekoracij ni preveč (primeren spawn rate)
+
+**Status:** ⏳ ČAKA NA TESTIRANJE
+
+---
+
+### Test 3: Depth Sorting (Prekrivanje)
+**Ukaz:** Hodi z igralcem okoli dekoracij in NPC-jev
+
+**Pričakovani rezultat:**
+- [ ] Ko gre igralec "nad" rožo (severno), jo pokrije (hodi po njej)
+- [ ] Ko gre igralec "pod" rožo (južno), jo pokrije (roža je ravna)
+- [ ] Ko gre igralec "nad" grmom (severno), je igralec ZA grmom (skrit)
+- [ ] Ko gre igralec "pod" grmom (južno), je igralec PRED grmom
+- [ ] Enako velja za NPC-je (pravilno prekrivanje)
+
+**Status:** ⏳ ČAKA NA TESTIRANJE
+
+---
+
+### Test 4: Parallax Oblaki
+**Ukaz:** Opazuj ozadje
+
+**Pričakovani rezultat:**
+- [ ] Čez zaslon se premikajo beli oblaki
+- [ ] Oblaki so prosojni
+- [ ] Oblaki se premikajo počasneje/hitreje kot kamera (parallax)?
+- [ ] Ko premikaš igralca (kamero), oblaki ostajajo bolj "pri miru" kot teren (ali se premikajo z drugačno hitrostjo)
+
+**Status:** ⏳ ČAKA NA TESTIRANJE
+
+---
+
+### Test 5: Performance
+**Ukaz:** Opazuj FPS
+
+**Pričakovani rezultat:**
+- [ ] Dodatek NPC-jev, dekoracij in oblakov ne zniža FPS pod 55
+- [ ] Igra teče gladko
+
+**Status:** ⏳ ČAKA NA TESTIRANJE
+
+---
+
+## 📋 Potrditev Naročnika
+
+```
+FAZA 3: [STATUS]
+- Testirano: [DA/NE]
+- Datum testiranja: ___________
+- Opombe:
+
+
+
+
+- Test 1: [✅/❌]
+- Test 2: [✅/❌]
+- Test 3: [✅/❌]
+- Test 4: [✅/❌]
+- Test 5: [✅/❌]
+
+ODOBRENO ZA FAZO 4: [DA/NE]
+
+Podpis naročnika: _____________
+```
+
+---
+
+## ➡️ Naslednji koraki (po odobritvi)
+
+Ko naročnik potrdi FAZO 3, se začne:
+**FAZA 4: Optimizacija in Performance**
+- Culling (ne renderaj nevidnih tile-ov) -> To bo pomembno za večje mape!
+- Object Pooling
+- Memory leak checks
diff --git a/FAZA_4_CHECKLIST.md b/FAZA_4_CHECKLIST.md
new file mode 100644
index 0000000..2785e9b
--- /dev/null
+++ b/FAZA_4_CHECKLIST.md
@@ -0,0 +1,68 @@
+# FAZA 4: Optimizacija in Performance - Checklist
+
+**Status:** ✅ PRIPRAVLJEN ZA TESTIRANJE
+
+**Datum:** 2025-12-06
+
+---
+
+## ✅ Opravila (Developer)
+
+- [x] Kreacija ObjectPool sistema (`src/utils/ObjectPool.js`)
+- [x] Refaktorizacija TerrainSystem za uporabo tekstur namesto Graphics
+- [x] Implementacija Culling-a Viewport-a (render samo visible tiles)
+- [x] Object Pooling za tiles in dekoracije
+- [x] Dinamično posodabljanje vidnega polja v `update` zanki
+- [x] Memory managment (auto-release nevidnih sprite-ov)
+
+**VSE OPRAVILA ZAKLJUČENA** ✅
+
+---
+
+## 🧪 Ročno testiranje (Naročnik)
+
+### Test 1: FPS Stabilnost
+**Ukaz:** Opazuj FPS števec spodaj levo med premikanjem
+
+**Pričakovani rezultat:**
+- [ ] FPS ostaja stabilen pri ~60 FPS
+- [ ] Pri hitrem zoomiranju/premikanju ni opaznega laga
+- [ ] Load time na začetku je hiter (ker ne riše vsega takoj?)
+
+**Status:** ⏳ ČAKA NA TESTIRANJE
+
+### Test 2: Culling (Nevidno nalaganje)
+**Ukaz:** Hitro premikaj kamero po robovih mape
+
+**Pričakovani rezultat:**
+- [ ] Map se "riše" sproti na robovih ekrana
+- [ ] Če greš hitro, morda vidiš za delček sekunde črnino, ki se takoj zapolni
+- [ ] Ko odideš stran in se vrneš, so tile-i in dekoracije še vedno tam (konzistentnost)
+
+**Status:** ⏳ ČAKA NA TESTIRANJE
+
+---
+
+## 📋 Potrditev Naročnika
+
+```
+FAZA 4: [STATUS]
+- Testirano: [DA/NE]
+- Datum testiranja: ___________
+- Opombe:
+
+ODOBRENO ZA FAZO 5: [DA/NE]
+
+Podpis naročnika: _____________
+```
+
+---
+
+## ➡️ Naslednji koraki (po odobritvi)
+
+Ko naročnik potrdi FAZO 4, se začne:
+**FAZA 5: UI Elementi**
+- HUD (Head-up Display)
+- Health Bar
+- Inventory Bar (quick slots)
+- Mini-mapa (optional)
diff --git a/FAZA_5_CHECKLIST.md b/FAZA_5_CHECKLIST.md
new file mode 100644
index 0000000..faaccca
--- /dev/null
+++ b/FAZA_5_CHECKLIST.md
@@ -0,0 +1,73 @@
+# FAZA 5: UI Elementi (HUD) - Checklist
+
+**Status:** ✅ ZAKLJUČENO
+
+**Datum:** 2025-12-06
+
+---
+
+## ✅ Opravila (Developer)
+
+- [x] Kreacija `UIScene.js` za ločen UI layer
+- [x] Integracija UIScene v `game.js` in zagon iz `GameScene`
+- [x] Implementacija Status Barov (levo zgoraj):
+ - [x] Health Bar (Rdeč)
+ - [x] Hunger Bar (Oranžen/Rjav)
+ - [x] Thirst Bar (Moder)
+- [x] Implementacija Inventory Toolbar-a (spodaj na sredini):
+ - [x] 10 slotov za predmete
+ - [x] Selekcija slota (številke 1-9 ali klik)
+- [x] Povezava debug podatkov iz GameScene v UIScene
+
+**VSE OPRAVILA ZAKLJUČENA** ✅
+
+---
+
+## 🧪 Ročno testiranje (Naročnik)
+
+### Test 1: Prikaz UI
+**Ukaz:** Zaženi igro
+
+**Pričakovani rezultat:**
+- [x] UI elementi so fiksni na ekranu (se ne premikajo s kamero)
+- [x] V levem zgornjem kotu so 3 vrstice (HP, Hrana, Voda)
+- [x] Spodaj je vrstica s kvadratki (inventar)
+
+### Test 2: Inventory Selection
+**Ukaz:** Pritisni številke 1-9 na tipkovnici ali uporabi scroll wheel
+
+**Pričakovani rezultat:**
+- [x] Označen (rumen) kvadratek se premika
+- [x] Izbira je logična in odzivna
+
+### Test 3: Responzivnost
+**Ukaz:** Zoomaj in premikaj kamero
+
+**Pričakovani rezultat:**
+- [x] UI ostane fixiran na zaslonu
+- [x] Grafika igre se premika *pod* UI-jem
+
+---
+
+## 📋 Potrditev Naročnika
+
+```
+FAZA 5: [STATUS]
+- Testirano: [DA]
+- Datum testiranja: 2025-12-06
+- Opombe: Uporabnik potrdil, da deluje super.
+
+ODOBRENO ZA FAZO 6: [DA]
+
+Podpis naročnika: User
+```
+
+---
+
+## ➡️ Naslednji koraki (po odobritvi)
+
+Ko naročnik potrdi FAZO 5, se začne:
+**FAZA 6: Save/Load Sistem**
+- Serializacija podatkov o terenu (vključno z dekoracijami)
+- Igralčeva pozicija in inventar
+- LocalStorage implementacija
diff --git a/FAZA_6_CHECKLIST.md b/FAZA_6_CHECKLIST.md
new file mode 100644
index 0000000..238d44d
--- /dev/null
+++ b/FAZA_6_CHECKLIST.md
@@ -0,0 +1,58 @@
+# FAZA 6: Save/Load Sistem - Checklist
+
+**Status:** ✅ ZAKLJUČENO
+
+**Datum:** 2025-12-06
+
+---
+
+## ✅ Opravila (Developer)
+
+- [x] Implementacija `SaveSystem.js`:
+ - [x] Metoda `saveGame(scene)`: Pobere podatke in shrani v localStorage.
+ - [x] Metoda `loadGame(scene)`: Prebere podatke in rekonstruira svet.
+- [x] Serializacija podatkov:
+ - [x] Teren (seed).
+ - [x] Igralec (pozicija X/Y).
+ - [x] NPC-ji (pozicije, tipi).
+ - [x] Kamera (Zoom).
+- [x] UI za Save/Load:
+ - [x] Tipke F5 (Save) in F9 (Load).
+ - [x] Obvestilo "Game Saved" in "Game Loaded".
+
+---
+
+## 🧪 Ročno testiranje (Naročnik)
+
+### Test 1: Shrani in Ponovni Zagon
+**Ukaz:** F5 -> Premik -> F9
+
+**Pričakovani rezultat:**
+- [x] Igralec skoči nazaj na shranjeno mesto.
+- [x] Teren ostane enak.
+- [x] NPC-ji se resetirajo na shranjene pozicije.
+
+---
+
+## 📋 Potrditev Naročnika
+
+```
+FAZA 6: [STATUS]
+- Testirano: [DA]
+- Datum testiranja: 2025-12-06
+- Opombe: Uporabnik potrdil delovanje.
+
+ODOBRENO ZA FAZO 7: [DA]
+
+Podpis naročnika: User
+```
+
+---
+
+## ➡️ Naslednji koraki (po odobritvi)
+
+Ko naročnik potrdi FAZO 6, se začne:
+**FAZA 7: Game Loop & Survival**
+- Day/Night cikel (sprememba svetlobe).
+- Padanje statistike (Lakota, Žeja).
+- Smrt in Respawn.
diff --git a/FAZA_7_CHECKLIST.md b/FAZA_7_CHECKLIST.md
new file mode 100644
index 0000000..4f616d6
--- /dev/null
+++ b/FAZA_7_CHECKLIST.md
@@ -0,0 +1,71 @@
+# FAZA 7: Game Loop & Survival - Checklist
+
+**Status:** ✅ ZAKLJUČENO
+
+**Datum:** 2025-12-06
+
+---
+
+## ✅ Opravila (Developer)
+
+- [x] Implementacija `TimeSystem.js` (Day/Night cikel):
+ - [x] Globalna ura igre (00:00 - 24:00).
+ - [x] Sprememba osvetlitve (tinting) glede na uro (Dan/Noč).
+ - [x] UI prikaz ure.
+- [x] Implementacija `StatsSystem.js` (Preživetje):
+ - [x] Health, Hunger, Thirst logike.
+ - [x] Padanje vrednosti čez čas.
+ - [x] Death condition (HP <= 0).
+- [x] Povezava z UIScene:
+ - [x] Posodabljanje Health/Hunger/Thirst barov.
+- [x] Game Over ekran (preprost overlay oz. reset).
+
+---
+
+## 🧪 Ročno testiranje (Naročnik)
+
+### Test 1: Dan in Noč
+**Ukaz:** Počakaj nekaj minut.
+
+**Pričakovani rezultat:**
+- [x] Svetloba se spreminja (zjutraj svetlo, ponoči temno).
+- [x] Ura na ekranu teče.
+
+### Test 2: Preživetje
+**Ukaz:** Opazuj bare zgoraj levo.
+
+**Pričakovani rezultat:**
+- [x] Hunger in Thirst počasi padata.
+- [x] Ko sta Hunger/Thirst na 0, začne padati HP.
+
+### Test 3: Smrt
+**Ukaz:** Počakaj da HP pade na 0.
+
+**Pričakovani rezultat:**
+- [x] Igra zazna smrt in resetira igralca.
+
+---
+
+## 📋 Potrditev Naročnika
+
+```
+FAZA 7: [STATUS]
+- Testirano: [DA]
+- Datum testiranja: 2025-12-06
+- Opombe: Uporabnik potrdil: "top dela vse kom more umru sem tudi noc dela itd"
+
+ODOBRENO ZA FAZO 8: [DA]
+
+Podpis naročnika: User
+```
+
+---
+
+## ➡️ Naslednji koraki (po odobritvi)
+
+Ko naročnik potrdi FAZO 7, se začne:
+**FAZA 8: Interakcije in Nabiranje**
+- Klikanje na objekte (drevesa, skale).
+- Sistem "Health" za objekte (potrebno več udarcev).
+- Dropanje itemov (les, kamen).
+- Pobiranje itemov v inventar.
diff --git a/FAZA_8_CHECKLIST.md b/FAZA_8_CHECKLIST.md
new file mode 100644
index 0000000..2d5bda1
--- /dev/null
+++ b/FAZA_8_CHECKLIST.md
@@ -0,0 +1,67 @@
+# FAZA 8: Interakcije in Nabiranje - Checklist
+
+**Status:** ✅ ZAKLJUČENO
+
+**Datum:** 2025-12-06
+
+---
+
+## ✅ Opravila (Developer)
+
+- [x] Implementacija `InteractionSystem.js`:
+ - [x] Detekcija klika miške na ploščice in objekte.
+ - [x] Preverjanje razdalje (igralec mora biti blizu).
+- [x] Nadgradnja `TerrainSystem.js` za interaktivnost:
+ - [x] Drevesa in grmi imajo HP.
+ - [x] Metoda `damageDecoration` in visual feedback (tint).
+- [x] Sistem "Dropov" (Items):
+ - [x] Ko objekt uničiš, se pojavi loot.
+ - [x] Igralec pobere item, ko gre čez njega.
+- [x] Povezava z Inventarjem (`InventorySystem.js`):
+ - [x] Shranjevanje količine items.
+ - [x] Prikaz v `UIScene`.
+
+---
+
+## 🧪 Ročno testiranje (Naročnik)
+
+### Test 1: Sekanje/Nabiranje
+**Ukaz:** Klikni na grm/rožo.
+
+**Pričakovani rezultat:**
+- [x] Objekt utripne ob udarcu.
+- [x] Uniči se po dovolj udarcih.
+- [x] Na tleh ostane item.
+
+### Test 2: Pobiranje
+**Ukaz:** Stopi na item.
+
+**Pričakovani rezultat:**
+- [x] Item izgine s tal.
+- [x] V inventarju se poveča število predmetov.
+
+---
+
+## 📋 Potrditev Naročnika
+
+```
+FAZA 8: [STATUS]
+- Testirano: [DA]
+- Datum testiranja: 2025-12-06
+- Opombe: Uporabnik potrdil delovanje.
+
+ODOBRENO ZA FAZO 9: [DA]
+
+Podpis naročnika: User
+```
+
+---
+
+## ➡️ Naslednji koraki (po odobritvi)
+
+Ko naročnik potrdi FAZO 8, se začne:
+**FAZA 9: Kmetovanje (Farming)**
+- Orodja (Motika).
+- Prekopavanje zemlje (Till Soil).
+- Sajenje semen.
+- Rast pridelkov (Crop Growth).
diff --git a/FAZA_9_CHECKLIST.md b/FAZA_9_CHECKLIST.md
new file mode 100644
index 0000000..96da925
--- /dev/null
+++ b/FAZA_9_CHECKLIST.md
@@ -0,0 +1,70 @@
+# FAZA 9: Kmetovanje (Farming) - Checklist
+
+**Status:** ✅ ZAKLJUČENO
+
+**Datum:** 2025-12-06
+
+---
+
+## ✅ Opravila (Developer)
+
+- [x] Implementacija `FarmingSystem.js`:
+ - [x] Logika za prekopavanje (Grass/Dirt -> Farmland).
+ - [x] Logika za sajenje (Farmland + Seed -> Crop).
+- [x] Posodobitev `TerrainSystem.js`:
+ - [x] Dodajanje podpore za `farmland` tip ploščice.
+ - [x] Dodajanje vizualizacije pridelkov (faze rasti 1-4).
+- [x] Integracija s `TimeSystem.js`:
+ - [x] Pridelki rastejo s časom (growthTimer).
+- [x] Orodja in Semena:
+ - [x] Item "Hoe" in "Seeds" dodana v inventar.
+ - [x] Interakcija s klikom (glede na izbrani slot).
+
+---
+
+## 🧪 Ročno testiranje (Naročnik)
+
+### Test 1: Prekopavanje
+**Ukaz:** Izberi motiko (Tipka 1) in klikni na travo.
+
+**Pričakovani rezultat:**
+- [x] Trava se spremeni v temno zemljo (Farmland).
+
+### Test 2: Sajenje
+**Ukaz:** Izberi seme (Tipka 2) in klikni na prekopano zemljo.
+
+**Pričakovani rezultat:**
+- [x] Na zemlji se pojavi majhna rastlina.
+
+### Test 3: Rast in Žetev
+**Ukaz:** Počakaj in nato klikni na zrelo rastlino.
+
+**Pričakovani rezultat:**
+- [x] Rastlina zraste v zrelo pšenico.
+- [x] Ob kliku se požanje in dobite pridelek.
+
+---
+
+## 📋 Potrditev Naročnika
+
+```
+FAZA 9: [STATUS]
+- Testirano: [DA]
+- Datum testiranja: 2025-12-06
+- Opombe: Uporabnik potrdil: "sem nasadil pozel naredil zemljo dela"
+
+ODOBRENO ZA FAZO 10: [DA]
+
+Podpis naročnika: User
+```
+
+---
+
+## ➡️ Naslednji koraki (po odobritvi)
+
+Ko naročnik potrdi FAZO 9, se začne:
+**FAZA 10: Ekonomija in Trgovina**
+- Valuta (Zlato).
+- NPC Interakcija (Trgovec).
+- Prodaja pridelkov (Wheat -> Gold).
+- Nakup semen (Gold -> Seeds).
diff --git a/GDD.md b/GDD.md
new file mode 100644
index 0000000..3a4cb10
--- /dev/null
+++ b/GDD.md
@@ -0,0 +1,59 @@
+# GAME DESIGN DOCUMENT (GDD) - Krvava Žetev (Zombie Roots)
+
+## 1. Povzetek (Elevator Pitch)
+**Krvava Žetev** (Zombie Roots) je post-apokaliptični "Farm-Life Sim" RPG (v stilu Stardew Valley/Graveyard Keeper), kjer igrate kot **Hibrid** – imun najstnik z dreadlocksi, ki ima status Alfe med zombiji. Namesto da bi vse delali sami, krotite in uporabljate **Zombije** kot delovno silo za obnovo porušenega sveta in iskanje izgubljene sestre.
+
+---
+
+## 2. Zgodba in Lore
+- **Protagonist:** Najstnik z značilnimi dredloksi. Preživel napad mutanta "Zmaj-Volka", postal Hibrid.
+- **Svet:** Uničen z virusom. Tavajoči zombiji in mutanti (troli, vilinci).
+- **Cilj:**
+ 1. **Iskanje sestre:** Ključ do zdravila ali ujeta v laboratoriju.
+ 2. **Maščevanje:** Za smrt staršev.
+ 3. **Obnova:** Popravilo mesta in vzpostavitev civilizacije.
+
+---
+
+## 3. Jedrne Mehanike (Core Gameplay)
+
+### 🧟 Zombi Delavci (The Alpha System)
+- **Krotenje:** Igralec je Alfa. Zombiji ga ubogajo.
+- **Delo:** Zombiji kmetujejo, rudarijo, stražijo.
+- **Regeneracija:** Zombiji se utrudijo. Potrebujejo **Grobove** (ne postelj) za počitek.
+- **Smrt:** Ko zombi razpade, postane **Visokokakovostno Gnojilo** (Moralna dilema: Delavec ali Gnojilo?).
+- **Leveling:** Zombiji pridobivajo XP (rudarjenje, kmetovanje).
+
+### 🗣️ Hibridne Veščine (Hybrid Skill)
+- **Komunikacija:** Višji skill omogoča razumevanje zombijevskega mrmranja (namigi, lore).
+- **Voh Alfe:** Privablja zombije, kar je lahko dobro (delavci) ali slabo (horda).
+
+### 🏡 Obnova Mesta
+- **Ruševine:** Mesto je porušeno.
+- **Projekti:** Zbiranje materialov (Les, Kamen, Zlato) za popravilo hiš NPC-jem (Kovač, Pekarica).
+- **Nagrada:** Srčki (Hearts) odklenejo trgovine, zgodbo in možnost **posojanja zombijev** NPC-jem za zaslužek.
+
+### 💰 Ekonomija in Kmetijstvo
+- **Valuta:** Zlato se ne najde. Rudo je treba izkopati in **skovati (Minting)** v zlatnike.
+- **Obramba:** **Mesojedke (Mario Plants/Piranha Plants)**. Hranijo se z mesom/zombiji. Služijo kot obrambni stolpi.
+
+---
+
+## 4. Vizualni Stil
+- **Grafika:** 2.5D Pixel Art (Izometrični pogled).
+- **Vibe:** Melanholičen, zbledela paleta (siva, rjava, zelena) z neonskimi poudarki (dreadlocksi, mutirane rastline).
+
+---
+
+## 5. Tehnični Načrt (Roadmap)
+- **Faza 1-9:** Osnovni Engine (Teren, Kmetovanje) - *ZAKLJUČENO*
+- **Faza 10:** Osnovna Ekonomija - *ZAKLJUČENO*
+- **Faza 11:** Gradnja (Building) - *ZAKLJUČENO*
+- **Faza 12:** Persistence (Save/Load) - *V TEKU*
+- **Faza 13:** Zombi AI (Krotenje in Delo).
+- **Faza 14:** NPC Obnova (Quests).
+- **Faza 15:** Zgodba (Intro, Cutscenes).
+
+---
+
+*Dokument ustvarjen na podlagi uporabnikove vizije: 2025-12-06.*
diff --git a/assets/decoration_tree.png b/assets/decoration_tree.png
new file mode 100644
index 0000000..68bc879
Binary files /dev/null and b/assets/decoration_tree.png differ
diff --git a/assets/grass_sprite.png b/assets/grass_sprite.png
new file mode 100644
index 0000000..23f52f0
Binary files /dev/null and b/assets/grass_sprite.png differ
diff --git a/assets/grass_tile.png b/assets/grass_tile.png
new file mode 100644
index 0000000..3af79ce
Binary files /dev/null and b/assets/grass_tile.png differ
diff --git a/assets/ground_tiles.png b/assets/ground_tiles.png
new file mode 100644
index 0000000..2ffa14f
Binary files /dev/null and b/assets/ground_tiles.png differ
diff --git a/assets/house_sprite.png b/assets/house_sprite.png
new file mode 100644
index 0000000..3bc2b25
Binary files /dev/null and b/assets/house_sprite.png differ
diff --git a/assets/leaf_sprite.png b/assets/leaf_sprite.png
new file mode 100644
index 0000000..e6d7903
Binary files /dev/null and b/assets/leaf_sprite.png differ
diff --git a/assets/merchant_sprite.png b/assets/merchant_sprite.png
new file mode 100644
index 0000000..6d560b6
Binary files /dev/null and b/assets/merchant_sprite.png differ
diff --git a/assets/npc_merchant.png b/assets/npc_merchant.png
new file mode 100644
index 0000000..24e9dc3
Binary files /dev/null and b/assets/npc_merchant.png differ
diff --git a/assets/npc_zombie.png b/assets/npc_zombie.png
new file mode 100644
index 0000000..08723cc
Binary files /dev/null and b/assets/npc_zombie.png differ
diff --git a/assets/objects_pack.png b/assets/objects_pack.png
new file mode 100644
index 0000000..d352a80
Binary files /dev/null and b/assets/objects_pack.png differ
diff --git a/assets/objects_pack2.png b/assets/objects_pack2.png
new file mode 100644
index 0000000..a9642b0
Binary files /dev/null and b/assets/objects_pack2.png differ
diff --git a/assets/player.png b/assets/player.png
new file mode 100644
index 0000000..81dbbb1
Binary files /dev/null and b/assets/player.png differ
diff --git a/assets/player_sprite.png b/assets/player_sprite.png
new file mode 100644
index 0000000..16a9791
Binary files /dev/null and b/assets/player_sprite.png differ
diff --git a/assets/stone_sprite.png b/assets/stone_sprite.png
new file mode 100644
index 0000000..9f0b905
Binary files /dev/null and b/assets/stone_sprite.png differ
diff --git a/assets/stone_texture.png b/assets/stone_texture.png
new file mode 100644
index 0000000..4b9e087
Binary files /dev/null and b/assets/stone_texture.png differ
diff --git a/assets/structure_house.png b/assets/structure_house.png
new file mode 100644
index 0000000..3c30082
Binary files /dev/null and b/assets/structure_house.png differ
diff --git a/assets/tree_sprite.png b/assets/tree_sprite.png
new file mode 100644
index 0000000..5f028fb
Binary files /dev/null and b/assets/tree_sprite.png differ
diff --git a/assets/trees_vegetation.png b/assets/trees_vegetation.png
new file mode 100644
index 0000000..a55a0b9
Binary files /dev/null and b/assets/trees_vegetation.png differ
diff --git a/assets/walls_pack.png b/assets/walls_pack.png
new file mode 100644
index 0000000..64b61be
Binary files /dev/null and b/assets/walls_pack.png differ
diff --git a/assets/wheat_sprite.png b/assets/wheat_sprite.png
new file mode 100644
index 0000000..2f9aee7
Binary files /dev/null and b/assets/wheat_sprite.png differ
diff --git a/assets/zombie_sprite.png b/assets/zombie_sprite.png
new file mode 100644
index 0000000..1cac2bd
Binary files /dev/null and b/assets/zombie_sprite.png differ
diff --git a/dev_plan.md b/dev_plan.md
index 4d24b22..c0b15ea 100644
--- a/dev_plan.md
+++ b/dev_plan.md
@@ -181,5 +181,5 @@ Format potrditve:
FAZA [N]: [STATUS]
- Testirano: [DA/NE]
- Opombe: [opombe naročnika]
-- Odobreno: [DA/NE]
+- Odobreno: [DA/NE]a
```
diff --git a/index.html b/index.html
index 7201325..81423d1 100644
--- a/index.html
+++ b/index.html
@@ -4,6 +4,9 @@
+
+
NovaFarma - 2.5D Survival Game
+
+
+
+
@@ -38,9 +66,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
@@ -49,6 +89,8 @@
+
+
diff --git a/novafarma.code-workspace b/novafarma.code-workspace
new file mode 100644
index 0000000..876a149
--- /dev/null
+++ b/novafarma.code-workspace
@@ -0,0 +1,8 @@
+{
+ "folders": [
+ {
+ "path": "."
+ }
+ ],
+ "settings": {}
+}
\ No newline at end of file
diff --git a/src/entities/NPC.js b/src/entities/NPC.js
index aaa4280..14e5fd9 100644
--- a/src/entities/NPC.js
+++ b/src/entities/NPC.js
@@ -33,10 +33,14 @@ class NPC {
}
createSprite() {
- // Generiraj NPC teksturo glede na tip
- const texKey = `npc_${this.type}`;
+ // Check for custom sprites first
+ let texKey = `npc_${this.type}`;
- if (!this.scene.textures.exists(texKey)) {
+ if (this.type === 'zombie' && this.scene.textures.exists('zombie_sprite')) {
+ texKey = 'zombie_sprite';
+ } else if (this.type === 'merchant' && this.scene.textures.exists('merchant_sprite')) {
+ texKey = 'merchant_sprite';
+ } else if (!this.scene.textures.exists(texKey)) {
TextureGenerator.createNPCSprite(this.scene, texKey, this.type);
}
@@ -47,7 +51,8 @@ class NPC {
screenPos.y + this.offsetY,
texKey
);
- this.sprite.setOrigin(0.5, 1); // Anchor na dnu sprite-a
+ this.sprite.setOrigin(0.5, 1);
+ this.sprite.setScale(0.2); // Mali, detajlni sprite
// Depth sorting
this.updateDepth();
diff --git a/src/entities/Player.js b/src/entities/Player.js
index 47c4e90..7e67caf 100644
--- a/src/entities/Player.js
+++ b/src/entities/Player.js
@@ -31,23 +31,27 @@ class Player {
}
createSprite() {
- // Generiraj player teksturo (static sprite)
- TextureGenerator.createPlayerSprite(this.scene, 'player');
+ // Use custom sprite if available, otherwise procedural
+ let texKey = 'player';
+
+ if (this.scene.textures.exists('player_sprite')) {
+ texKey = 'player_sprite';
+ } else if (!this.scene.textures.exists(texKey)) {
+ TextureGenerator.createPlayerSprite(this.scene, texKey);
+ }
// Kreira sprite
const screenPos = this.iso.toScreen(this.gridX, this.gridY);
this.sprite = this.scene.add.sprite(
screenPos.x + this.offsetX,
screenPos.y + this.offsetY,
- 'player'
+ texKey
);
- this.sprite.setOrigin(0.5, 1); // Anchor na dnu sprite-a
+ this.sprite.setOrigin(0.5, 1);
+ this.sprite.setScale(0.2); // Mali, detajlni sprite
// Depth sorting
this.updateDepth();
-
- // Walking animacija je onemogočena za FAZA 2 (fix za canvas texture issue)
- // TODO: Dodaj proper sprite sheet animacijo v FAZA 3
}
setupControls() {
diff --git a/src/game.js b/src/game.js
index b6f6e07..16d29ce 100644
--- a/src/game.js
+++ b/src/game.js
@@ -1,13 +1,21 @@
// Phaser Game Configuration
const config = {
- type: Phaser.AUTO,
- width: 1280,
- height: 720,
+ type: Phaser.CANVAS, // Canvas renderer za pixel-perfect ostrino
+ width: 640, // Pixel Art Viewport
+ height: 360, // Pixel Art Viewport (16:9)
parent: 'game-container',
backgroundColor: '#1a1a2e',
pixelArt: true,
antialias: false,
roundPixels: true,
+ render: {
+ pixelArt: true,
+ antialias: false,
+ roundPixels: true,
+ transparent: false,
+ clearBeforeRender: true,
+ powerPreference: 'high-performance'
+ },
physics: {
default: 'arcade',
arcade: {
@@ -15,10 +23,13 @@ const config = {
debug: false
}
},
- scene: [BootScene, PreloadScene, GameScene],
+ scene: [BootScene, PreloadScene, StoryScene, GameScene, UIScene],
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH
+ },
+ input: {
+ gamepad: true
}
};
diff --git a/src/scenes/GameScene.js b/src/scenes/GameScene.js
index 208b314..6f28b67 100644
--- a/src/scenes/GameScene.js
+++ b/src/scenes/GameScene.js
@@ -18,21 +18,37 @@ class GameScene extends Phaser.Scene {
// Setup kamere
this.cameras.main.setBackgroundColor('#1a1a2e');
+ // Initialize Isometric Utils
+ this.iso = new IsometricUtils();
+
// Inicializiraj terrain sistem - 100x100 mapa
console.log('🌍 Initializing terrain...');
- this.terrainSystem = new TerrainSystem(this, 100, 100);
- this.terrainSystem.generate();
+ try {
+ this.terrainSystem = new TerrainSystem(this, 100, 100);
+ this.terrainSystem.generate();
- // Terrain offset
- this.terrainOffsetX = width / 2;
- this.terrainOffsetY = 100;
- this.terrainContainer = this.terrainSystem.render(this.terrainOffsetX, this.terrainOffsetY);
+ // Terrain offset
+ this.terrainOffsetX = width / 2;
+ this.terrainOffsetY = 100;
- // Dodaj igralca - spawn na sredini mape S TERRAIN OFFSETOM
+ // Initialization for culling
+ this.terrainSystem.init(this.terrainOffsetX, this.terrainOffsetY);
+
+ // Initial force update to render active tiles before first frame
+ this.terrainSystem.updateCulling(this.cameras.main);
+
+ // FAZA 14: Spawn Ruin (Town Project) at fixed location near player
+ console.log('🏚️ Spawning Ruin...');
+ this.terrainSystem.placeStructure(55, 55, 'ruin');
+ } catch (e) {
+ console.error("Terrain system failed:", e);
+ }
+
+ // Dodaj igralca
console.log('👤 Initializing player...');
this.player = new Player(this, 50, 50, this.terrainOffsetX, this.terrainOffsetY);
- // Dodaj 3 NPCje - random pozicije
+ // Dodaj 3 NPCje
console.log('🧟 Initializing NPCs...');
const npcTypes = ['zombie', 'villager', 'merchant'];
for (let i = 0; i < 3; i++) {
@@ -42,36 +58,59 @@ class GameScene extends Phaser.Scene {
this.npcs.push(npc);
}
- // Kamera sledi igralcu
- this.cameras.main.startFollow(this.player.sprite, true, 0.1, 0.1);
+ // Kamera sledi igralcu z izboljšanimi nastavitvami
+ this.cameras.main.startFollow(this.player.sprite, true, 1.0, 1.0); // Instant follow (was 0.1)
+
+ // Nastavi deadzone (100px border)
+ this.cameras.main.setDeadzone(100, 100);
+
+ // Round pixels za crisp pixel art
+ this.cameras.main.roundPixels = true;
+
+ // Parallax oblaki
+ this.createClouds();
// Kamera kontrole
this.setupCamera();
- // UI elementi
- this.createUI();
+ // Initialize Time & Stats
+ console.log('⏳ Initializing Time & Stats...');
+ this.timeSystem = new TimeSystem(this);
+ this.timeSystem.create();
- // Debug info
- this.debugText = this.add.text(10, 10, '', {
- fontFamily: 'Courier New',
- fontSize: '12px',
- fill: '#ffffff',
- backgroundColor: '#000000',
- padding: { x: 5, y: 3 }
- });
- this.debugText.setScrollFactor(0);
- this.debugText.setDepth(1000);
+ this.statsSystem = new StatsSystem(this);
+ this.inventorySystem = new InventorySystem(this);
+ this.interactionSystem = new InteractionSystem(this);
+ this.farmingSystem = new FarmingSystem(this);
+ this.buildingSystem = new BuildingSystem(this);
- // FPS counter
- this.fpsText = this.add.text(10, height - 30, 'FPS: 60', {
- fontFamily: 'Courier New',
- fontSize: '14px',
- fill: '#00ff41'
- });
- this.fpsText.setScrollFactor(0);
- this.fpsText.setDepth(1000);
+ // Initialize Weather System
+ console.log('🌦️ Initializing Weather System...');
+ this.weatherSystem = new WeatherSystem(this);
- console.log('✅ GameScene ready - FAZA 3!');
+ // Initialize Day/Night Cycle
+ console.log('🌅 Initializing Day/Night System...');
+ this.dayNightSystem = new DayNightSystem(this, this.timeSystem);
+
+ // Initialize Sound Manager
+ console.log('🎵 Initializing Sound Manager...');
+ this.soundManager = new SoundManager(this);
+
+ // Initialize Parallax System
+ console.log('🌄 Initializing Parallax System...');
+ this.parallaxSystem = new ParallaxSystem(this);
+
+ // Launch UI Scene
+ console.log('🖥️ Launching UI Scene...');
+ this.scene.launch('UIScene');
+
+ // Initialize Save System
+ this.saveSystem = new SaveSystem(this);
+
+ // Auto-load if available (optional, for now manual)
+ // this.saveSystem.loadGame();
+
+ console.log('✅ GameScene ready - FAZA 17!');
}
setupCamera() {
@@ -88,51 +127,81 @@ class GameScene extends Phaser.Scene {
cam.setZoom(newZoom);
});
- // Pan kontrole (Right click + drag) - DISABLED za FAZA 2
- // Player movement sedaj uporablja WASD
-
// Q/E za zoom
this.zoomKeys = this.input.keyboard.addKeys({
zoomIn: Phaser.Input.Keyboard.KeyCodes.Q,
zoomOut: Phaser.Input.Keyboard.KeyCodes.E
});
- }
- createUI() {
- const width = this.cameras.main.width;
-
- // Naslov
- const title = this.add.text(width / 2, 20, 'FAZA 3: NPC-ji in Dekoracije', {
- fontFamily: 'Courier New',
- fontSize: '20px',
- fill: '#00ff41',
- fontStyle: 'bold'
- });
- title.setOrigin(0.5, 0);
- title.setScrollFactor(0);
- title.setDepth(1000);
-
- // Kontrole info
- const controlsText = this.add.text(width - 10, 10,
- 'Kontrole:\n' +
- 'WASD - Gibanje igralca\n' +
- 'Q/E - Zoom\n' +
- 'Mouse Wheel - Zoom',
- {
- fontFamily: 'Courier New',
- fontSize: '11px',
- fill: '#888888',
- backgroundColor: '#000000',
- padding: { x: 5, y: 3 },
- align: 'right'
+ // Save/Load Keys
+ this.input.keyboard.on('keydown-F8', () => {
+ // Save
+ if (this.saveSystem) {
+ this.saveSystem.saveGame();
+ console.log('💾 Game Saved! (F8)');
}
- );
- controlsText.setOrigin(1, 0);
- controlsText.setScrollFactor(0);
- controlsText.setDepth(1000);
+ });
+
+ this.input.keyboard.on('keydown-F9', () => {
+ // Load
+ if (this.saveSystem) {
+ this.saveSystem.loadGame();
+ console.log('📂 Game Loaded! (F9)');
+ }
+ });
+
+ // Build Mode Keys
+ this.input.keyboard.on('keydown-B', () => {
+ if (this.buildingSystem) this.buildingSystem.toggleBuildMode();
+ });
+
+ this.input.keyboard.on('keydown-ONE', () => {
+ if (this.buildingSystem && this.buildingSystem.isBuildMode) this.buildingSystem.selectBuilding('fence');
+ else if (this.scene.get('UIScene')) this.scene.get('UIScene').selectSlot(0);
+ });
+ this.input.keyboard.on('keydown-TWO', () => {
+ if (this.buildingSystem && this.buildingSystem.isBuildMode) this.buildingSystem.selectBuilding('wall');
+ else if (this.scene.get('UIScene')) this.scene.get('UIScene').selectSlot(1);
+ });
+ this.input.keyboard.on('keydown-THREE', () => {
+ if (this.buildingSystem && this.buildingSystem.isBuildMode) this.buildingSystem.selectBuilding('house');
+ else if (this.scene.get('UIScene')) this.scene.get('UIScene').selectSlot(2);
+ });
+
+ // Soft Reset (F4) - Force Reload Page
+ this.input.keyboard.on('keydown-F4', () => {
+ console.log('🔄 Soft Reset Initiated (Force Reload)...');
+ window.location.reload();
+ });
+
+ // Mute Toggle (M key)
+ this.input.keyboard.on('keydown-M', () => {
+ if (this.soundManager) {
+ this.soundManager.toggleMute();
+ }
+ });
+
}
update(time, delta) {
+ // Update Systems
+ if (this.timeSystem) this.timeSystem.update(delta);
+ if (this.statsSystem) this.statsSystem.update(delta);
+ if (this.interactionSystem) this.interactionSystem.update(delta);
+ if (this.farmingSystem) this.farmingSystem.update(delta);
+ if (this.weatherSystem) this.weatherSystem.update(delta);
+ if (this.dayNightSystem) this.dayNightSystem.update();
+
+ // Update Parallax (foreground grass fading)
+ if (this.parallaxSystem && this.player) {
+ const playerPos = this.player.getPosition();
+ const screenPos = this.iso.toScreen(playerPos.x, playerPos.y);
+ this.parallaxSystem.update(
+ screenPos.x + this.terrainOffsetX,
+ screenPos.y + this.terrainOffsetY
+ );
+ }
+
// Update player
if (this.player) {
this.player.update(delta);
@@ -143,32 +212,61 @@ class GameScene extends Phaser.Scene {
npc.update(delta);
}
- // Update FPS
- if (this.fpsText) {
- this.fpsText.setText(`FPS: ${Math.round(this.game.loop.actualFps)}`);
+ // Update Terrain Culling
+ if (this.terrainSystem) {
+ this.terrainSystem.updateCulling(this.cameras.main);
}
- // Zoom controls
- const cam = this.cameras.main;
- if (this.zoomKeys) {
- if (this.zoomKeys.zoomIn.isDown) {
- cam.setZoom(Phaser.Math.Clamp(cam.zoom + 0.01, 0.3, 2.0));
- }
- if (this.zoomKeys.zoomOut.isDown) {
- cam.setZoom(Phaser.Math.Clamp(cam.zoom - 0.01, 0.3, 2.0));
+ // Update clouds
+ if (this.clouds) {
+ for (const cloud of this.clouds) {
+ cloud.sprite.x += cloud.speed * (delta / 1000);
+ if (cloud.sprite.x > this.terrainOffsetX + 2000) { // Reset far right
+ cloud.sprite.x = this.terrainOffsetX - 2000;
+ cloud.sprite.y = Phaser.Math.Between(0, 1000);
+ }
}
}
- // Debug info update
- if (this.debugText && this.player) {
+ // Send debug info to UI Scene
+ if (this.player) {
const playerPos = this.player.getPosition();
+ const cam = this.cameras.main;
+ const visibleTiles = this.terrainSystem ? this.terrainSystem.visibleTiles.size : 0;
- this.debugText.setText(
- `FAZA 3 - NPCs & Decorations\n` +
- `Zoom: ${cam.zoom.toFixed(2)}\n` +
- `Player: (${playerPos.x}, ${playerPos.y})\n` +
- `NPCs: ${this.npcs.length}`
- );
+ const uiScene = this.scene.get('UIScene');
+ if (uiScene && uiScene.debugText) {
+ const activeCrops = this.terrainSystem && this.terrainSystem.cropsMap ? this.terrainSystem.cropsMap.size : 0;
+ const dropsCount = this.interactionSystem && this.interactionSystem.drops ? this.interactionSystem.drops.length : 0;
+
+ uiScene.debugText.setText(
+ `FAZA 11 - Building\n` +
+ `[F5] Save | [F9] Load | [B] Build Mode\n` +
+ `Time: ${this.timeSystem ? this.timeSystem.gameTime.toFixed(1) : '?'}h\n` +
+ `Active Crops: ${activeCrops}\n` +
+ `Loot Drops: ${dropsCount}\n` +
+ `Player: (${playerPos.x}, ${playerPos.y})`
+ );
+ }
+ }
+ }
+
+ createClouds() {
+ if (!this.textures.exists('cloud')) TextureGenerator.createCloudSprite(this, 'cloud');
+
+ this.clouds = [];
+ console.log('☁️ Creating parallax clouds...');
+ for (let i = 0; i < 8; i++) {
+ const x = Phaser.Math.Between(-1000, 3000);
+ const y = Phaser.Math.Between(-500, 1500);
+
+ const cloud = this.add.sprite(x, y, 'cloud');
+ cloud.setAlpha(0.4);
+ cloud.setScrollFactor(0.2); // Parallax effect
+ cloud.setDepth(2000); // Nad vsem
+ cloud.setScale(Phaser.Math.FloatBetween(2, 4)); // Veliki oblaki
+
+ this.clouds.push({ sprite: cloud, speed: Phaser.Math.FloatBetween(10, 30) });
}
}
}
diff --git a/src/scenes/PreloadScene.js b/src/scenes/PreloadScene.js
index fa2930b..d4057fa 100644
--- a/src/scenes/PreloadScene.js
+++ b/src/scenes/PreloadScene.js
@@ -5,32 +5,125 @@ class PreloadScene extends Phaser.Scene {
}
preload() {
- console.log('📦 PreloadScene: Loading assets...');
+ console.log('⏳ PreloadScene: Loading assets...');
- // TODO: Tu bomo nalagali sprite-e, tile-e, audio, itd.
- // Za fazo 0 pustimo prazno - samo testiramo osnovni setup
+ // Load ALL custom sprites
+ this.load.image('player_sprite', 'assets/player_sprite.png');
+ this.load.image('zombie_sprite', 'assets/zombie_sprite.png');
+ this.load.image('merchant_sprite', 'assets/merchant_sprite.png');
+ this.load.image('house_sprite', 'assets/house_sprite.png');
+ this.load.image('stone_sprite', 'assets/stone_sprite.png');
+ this.load.image('tree_sprite', 'assets/tree_sprite.png');
+ this.load.image('grass_sprite', 'assets/grass_sprite.png');
+ this.load.image('grass_tile', 'assets/grass_tile.png');
+ this.load.image('leaf_sprite', 'assets/leaf_sprite.png');
+ this.load.image('wheat_sprite', 'assets/wheat_sprite.png');
+ this.load.image('stone_texture', 'assets/stone_texture.png');
+
+ // New asset packs
+ this.load.image('objects_pack', 'assets/objects_pack.png');
+ this.load.image('walls_pack', 'assets/walls_pack.png');
+ this.load.image('ground_tiles', 'assets/ground_tiles.png');
+ this.load.image('objects_pack2', 'assets/objects_pack2.png');
+ this.load.image('trees_vegetation', 'assets/trees_vegetation.png');
+
+ // Wait for load completion then process transparency
+ this.load.once('complete', () => {
+ this.processAllTransparency();
+ });
+ }
+
+ processAllTransparency() {
+ // Process ALL sprites to remove backgrounds
+ const spritesToProcess = [
+ 'player_sprite',
+ 'zombie_sprite',
+ 'merchant_sprite',
+ 'house_sprite',
+ 'stone_sprite',
+ 'tree_sprite',
+ 'grass_sprite',
+ 'leaf_sprite',
+ 'wheat_sprite',
+ 'stone_texture'
+ ];
+
+ spritesToProcess.forEach(spriteKey => {
+ this.processSpriteTransparency(spriteKey);
+ });
+
+ console.log('✅ All sprites transparency processed!');
+ }
+
+ processSpriteTransparency(spriteKey) {
+ if (!this.textures.exists(spriteKey)) return;
+
+ const texture = this.textures.get(spriteKey);
+ const source = texture.getSourceImage();
+
+ // Create canvas to process image
+ const canvas = document.createElement('canvas');
+ canvas.width = source.width;
+ canvas.height = source.height;
+ const ctx = canvas.getContext('2d', { willReadFrequently: true });
+
+ // Draw original image
+ ctx.drawImage(source, 0, 0);
+
+ // Get image data
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+ const data = imageData.data;
+
+ // Remove backgrounds
+ for (let i = 0; i < data.length; i += 4) {
+ const r = data[i];
+ const g = data[i + 1];
+ const b = data[i + 2];
+
+ // Remove white/light gray backgrounds (all sprites)
+ if (r > 200 && g > 200 && b > 200) {
+ data[i + 3] = 0;
+ }
+
+ // Special: Remove brown/tan backgrounds (merchant sprite)
+ if (spriteKey === 'merchant_sprite') {
+ // Brown detection: R > G > B, warm tones
+ const isBrown = r > 100 && r > g && g > b && (r - b) > 40;
+ if (isBrown) {
+ data[i + 3] = 0;
+ }
+ }
+ }
+
+ // Put processed data back
+ ctx.putImageData(imageData, 0, 0);
+
+ // Create new texture from processed canvas
+ this.textures.remove(spriteKey);
+ this.textures.addCanvas(spriteKey, canvas);
}
create() {
console.log('✅ PreloadScene: Assets loaded!');
window.gameState.currentScene = 'PreloadScene';
- // Prikaz začetnega sporočila
const width = this.cameras.main.width;
const height = this.cameras.main.height;
- const title = this.add.text(width / 2, height / 2 - 50, 'NOVAFARMA', {
+ const title = this.add.text(width / 2, height / 2 - 50, 'KRVAVA ŽETEV', {
fontFamily: 'Courier New',
fontSize: '48px',
- fill: '#00ff41',
- fontStyle: 'bold'
+ fill: '#ff0000',
+ fontStyle: 'bold',
+ stroke: '#000000',
+ strokeThickness: 6
});
title.setOrigin(0.5);
- const subtitle = this.add.text(width / 2, height / 2 + 10, '2.5D Isometric Survival Game', {
+ const subtitle = this.add.text(width / 2, height / 2 + 10, 'Zombie Roots', {
fontFamily: 'Courier New',
- fontSize: '20px',
- fill: '#ffffff'
+ fontSize: '24px',
+ fill: '#00ff41'
});
subtitle.setOrigin(0.5);
@@ -41,7 +134,6 @@ class PreloadScene extends Phaser.Scene {
});
instruction.setOrigin(0.5);
- // Blinking effect
this.tweens.add({
targets: instruction,
alpha: 0.3,
@@ -50,10 +142,25 @@ class PreloadScene extends Phaser.Scene {
repeat: -1
});
- // Pritisk SPACE za začetek igre
- this.input.keyboard.once('keydown-SPACE', () => {
- console.log('🎮 Starting GameScene...');
- this.scene.start('GameScene');
+ const startGame = () => {
+ console.log('🎮 Starting StoryScene...');
+ this.input.keyboard.off('keydown');
+ this.input.off('pointerdown');
+ this.scene.start('StoryScene');
+ };
+
+ this.time.delayedCall(3000, () => {
+ startGame();
+ });
+
+ this.input.keyboard.on('keydown', (event) => {
+ if (event.code === 'Space' || event.code === 'Enter') {
+ startGame();
+ }
+ });
+
+ this.input.on('pointerdown', () => {
+ startGame();
});
}
}
diff --git a/src/scenes/StoryScene.js b/src/scenes/StoryScene.js
new file mode 100644
index 0000000..bd0ee4a
--- /dev/null
+++ b/src/scenes/StoryScene.js
@@ -0,0 +1,70 @@
+class StoryScene extends Phaser.Scene {
+ constructor() {
+ super({ key: 'StoryScene' });
+ }
+
+ create() {
+ const width = this.cameras.main.width;
+ const height = this.cameras.main.height;
+
+ // Black background
+ this.add.rectangle(0, 0, width, height, 0x000000).setOrigin(0);
+
+ const storyText =
+ `Leto 2084.
+Svet, kot smo ga poznali, je izginil.
+
+Virus "Zmaj-Volka" je spremenil človeštvo.
+Mesta so ruševine. Narava je divja.
+
+Toda ti si drugačen.
+Preživel si napad. Okužen, a imun.
+Si HIBRID.
+
+Zombiji te ne napadajo... čutijo te.
+Zanje si ALFA.
+
+Tvoja naloga:
+1. Najdi izgubljeno sestro.
+2. Maščuj starše.
+3. Obnovi civilizacijo iz pepela.
+
+Dobrodošel v KRVAVI ŽETVI.`;
+
+ const textObj = this.add.text(width / 2, height + 100, storyText, {
+ fontFamily: 'Courier New',
+ fontSize: '24px',
+ fill: '#00ff41',
+ align: 'center',
+ lineSpacing: 10
+ });
+ textObj.setOrigin(0.5, 0);
+
+ // Scroll animation
+ this.tweens.add({
+ targets: textObj,
+ y: 50,
+ duration: 10000, // 10s scroll
+ ease: 'Linear',
+ onComplete: () => {
+ this.time.delayedCall(2000, () => {
+ this.scene.start('GameScene');
+ });
+ }
+ });
+
+ // Skip instructions
+ const skip = this.add.text(width - 20, height - 20, '[SPACE] Skip', {
+ fontSize: '16px', fill: '#666'
+ }).setOrigin(1);
+
+ // Input to skip
+ this.input.keyboard.on('keydown-SPACE', () => {
+ this.scene.start('GameScene');
+ });
+
+ this.input.on('pointerdown', () => {
+ this.scene.start('GameScene');
+ });
+ }
+}
diff --git a/src/scenes/UIScene.js b/src/scenes/UIScene.js
new file mode 100644
index 0000000..26af533
--- /dev/null
+++ b/src/scenes/UIScene.js
@@ -0,0 +1,451 @@
+class UIScene extends Phaser.Scene {
+ constructor() {
+ super({ key: 'UIScene' });
+ }
+
+ create() {
+ console.log('🖥️ UIScene: Initialized!');
+
+ // Pridobi reference na GameScene podatke (ko bodo na voljo)
+ this.gameScene = this.scene.get('GameScene');
+
+ // Setup UI Container
+ this.width = this.cameras.main.width;
+ this.height = this.cameras.main.height;
+
+ this.createStatusBars();
+ this.createInventoryBar();
+ this.createGoldDisplay();
+ this.createClock();
+ this.createDebugInfo();
+
+ // Listen for events from GameScene if needed
+ }
+
+ createStatusBars() {
+ const x = 20;
+ const y = 20;
+ const width = 200;
+ const height = 20;
+ const padding = 10;
+
+ // Style
+ const boxStyle = {
+ fillStyle: { color: 0x000000, alpha: 0.5 },
+ lineStyle: { width: 2, color: 0xffffff, alpha: 0.8 }
+ };
+
+ // 1. Health Bar
+ this.add.text(x, y - 5, 'HP', { fontSize: '12px', fontFamily: 'Courier New', fill: '#ffffff' });
+ this.healthBar = this.createBar(x + 30, y, width, height, 0xff0000);
+ this.setBarValue(this.healthBar, 100);
+
+ // 2. Hunger Bar
+ this.add.text(x, y + height + padding - 5, 'HUN', { fontSize: '12px', fontFamily: 'Courier New', fill: '#ffffff' });
+ this.hungerBar = this.createBar(x + 30, y + height + padding, width, height, 0xff8800);
+ this.setBarValue(this.hungerBar, 80);
+
+ // 3. Thirst Bar
+ this.add.text(x, y + (height + padding) * 2 - 5, 'H2O', { fontSize: '12px', fontFamily: 'Courier New', fill: '#ffffff' });
+ this.thirstBar = this.createBar(x + 30, y + (height + padding) * 2, width, height, 0x0088ff);
+ this.setBarValue(this.thirstBar, 90);
+ }
+
+ createBar(x, y, width, height, color) {
+ // Background
+ const bg = this.add.graphics();
+ bg.fillStyle(0x000000, 0.5);
+ bg.fillRect(x, y, width, height);
+ bg.lineStyle(2, 0xffffff, 0.2);
+ bg.strokeRect(x, y, width, height);
+
+ // Fill
+ const fill = this.add.graphics();
+ fill.fillStyle(color, 1);
+ fill.fillRect(x + 2, y + 2, width - 4, height - 4);
+
+ return { bg, fill, x, y, width, height, color };
+ }
+
+ setBarValue(bar, percent) {
+ // Clamp 0-100
+ percent = Phaser.Math.Clamp(percent, 0, 100);
+
+ bar.fill.clear();
+ bar.fill.fillStyle(bar.color, 1);
+
+ const maxWidth = bar.width - 4;
+ const currentWidth = (maxWidth * percent) / 100;
+
+ bar.fill.fillRect(bar.x + 2, bar.y + 2, currentWidth, bar.height - 4);
+ }
+
+ createInventoryBar() {
+ const slotCount = 9;
+ const slotSize = 48; // 48x48 sloti
+ const padding = 5;
+
+ const totalWidth = (slotCount * slotSize) + ((slotCount - 1) * padding);
+ const startX = (this.width - totalWidth) / 2;
+ const startY = this.height - slotSize - 20;
+
+ this.inventorySlots = [];
+ this.selectedSlot = 0;
+
+ for (let i = 0; i < slotCount; i++) {
+ const x = startX + i * (slotSize + padding);
+
+ // Slot Background
+ const slot = this.add.graphics();
+
+ // Draw function to update style based on selection
+ slot.userData = { x, y: startY, size: slotSize, index: i };
+ this.drawSlot(slot, false);
+
+ // Add number text
+ this.add.text(x + 2, startY + 2, (i + 1).toString(), {
+ fontSize: '10px',
+ fontFamily: 'monospace',
+ fill: '#ffffff'
+ });
+
+ this.inventorySlots.push(slot);
+ }
+
+ // Select first one initially
+ this.selectSlot(0);
+
+ // Keyboard inputs 1-9
+ this.input.keyboard.on('keydown', (event) => {
+ const num = parseInt(event.key);
+ if (!isNaN(num) && num >= 1 && num <= 9) {
+ this.selectSlot(num - 1);
+ }
+ });
+
+ // Mouse scroll for inventory (optional)
+ this.input.on('wheel', (pointer, gameObjects, deltaX, deltaY, deltaZ) => {
+ if (deltaY > 0) {
+ this.selectSlot((this.selectedSlot + 1) % slotCount);
+ } else if (deltaY < 0) {
+ this.selectSlot((this.selectedSlot - 1 + slotCount) % slotCount);
+ }
+ });
+ }
+
+ drawSlot(graphics, isSelected) {
+ const { x, y, size } = graphics.userData;
+
+ graphics.clear();
+
+ // Background
+ graphics.fillStyle(0x000000, 0.6);
+ graphics.fillRect(x, y, size, size);
+
+ // Border
+ if (isSelected) {
+ graphics.lineStyle(3, 0xffff00, 1); // Yellow thick border for selection
+ } else {
+ graphics.lineStyle(2, 0x888888, 0.5); // Grey thin border
+ }
+ graphics.strokeRect(x, y, size, size);
+ }
+
+ selectSlot(index) {
+ // Deselect current
+ if (this.inventorySlots[this.selectedSlot]) {
+ this.drawSlot(this.inventorySlots[this.selectedSlot], false);
+ }
+
+ this.selectedSlot = index;
+
+ // Select new
+ this.drawSlot(this.inventorySlots[this.selectedSlot], true);
+ }
+
+ updateInventory(slots) {
+ if (!this.inventorySlots) return;
+ for (let i = 0; i < this.inventorySlots.length; i++) {
+ const slotGraphics = this.inventorySlots[i];
+ // Clear previous item info (we stored it in container? No, just graphics)
+ // Ideally slots should be containers.
+ // For now, let's just redraw the slot and add text on top.
+ // To do this cleanly, let's remove old item text/sprites if we track them.
+
+ if (slotGraphics.itemText) slotGraphics.itemText.destroy();
+
+ if (slots[i]) {
+ const { x, y, size } = slotGraphics.userData;
+ // Simple representation: Text
+ const text = this.add.text(x + size / 2, y + size / 2,
+ `${slots[i].type.substring(0, 2)}\n${slots[i].count}`,
+ { fontSize: '10px', align: 'center', color: '#ffff00' }
+ ).setOrigin(0.5);
+
+ slotGraphics.itemText = text;
+ }
+ }
+ }
+
+ createClock() {
+ // Clock box top right
+ const x = this.width - 150;
+ const y = 20;
+
+ // Background
+ const bg = this.add.graphics();
+ bg.fillStyle(0x000000, 0.5);
+ bg.fillRect(x, y, 130, 40);
+ bg.lineStyle(2, 0xffffff, 0.8);
+ bg.strokeRect(x, y, 130, 40);
+
+ this.clockText = this.add.text(x + 65, y + 20, 'Day 1 - 08:00', {
+ fontSize: '14px',
+ fontFamily: 'Courier New',
+ fill: '#ffffff',
+ fontStyle: 'bold'
+ });
+ this.clockText.setOrigin(0.5, 0.5);
+ }
+
+ createGoldDisplay() {
+ const x = this.width - 150;
+ const y = 70; // Below clock
+
+ // Background
+ const bg = this.add.graphics();
+ bg.fillStyle(0xDAA520, 0.2); // Goldish bg
+ bg.fillRect(x, y, 130, 30);
+ bg.lineStyle(2, 0xFFD700, 0.8);
+ bg.strokeRect(x, y, 130, 30);
+
+ this.goldText = this.add.text(x + 65, y + 15, 'GOLD: 0', {
+ fontSize: '16px',
+ fontFamily: 'Courier New',
+ fill: '#FFD700', // Gold color
+ fontStyle: 'bold'
+ });
+ this.goldText.setOrigin(0.5, 0.5);
+ }
+
+ updateGold(amount) {
+ if (this.goldText) {
+ this.goldText.setText(`GOLD: ${amount}`);
+ }
+ }
+
+ createDebugInfo() {
+ this.debugText = this.add.text(10, 100, '', {
+ fontSize: '12px',
+ fontFamily: 'monospace',
+ fill: '#00ff00'
+ });
+ }
+
+ update() {
+ // Here we could update bars based on player stats
+ // if (this.gameScene && this.gameScene.player) { ... }
+ }
+ toggleBuildMenu(isVisible) {
+ if (!this.buildMenuContainer) {
+ this.createBuildMenuInfo();
+ }
+ this.buildMenuContainer.setVisible(isVisible);
+ }
+
+ createBuildMenuInfo() {
+ this.buildMenuContainer = this.add.container(this.width / 2, 100);
+
+ const bg = this.add.graphics();
+ bg.fillStyle(0x000000, 0.7);
+ bg.fillRect(-150, 0, 300, 100);
+ bg.lineStyle(2, 0x00FF00, 1);
+ bg.strokeRect(-150, 0, 300, 100);
+
+ this.buildMenuContainer.add(bg);
+
+ const title = this.add.text(0, 10, 'BUILD MODE [B]', { fontSize: '18px', fill: '#00FF00', fontStyle: 'bold' }).setOrigin(0.5, 0);
+ this.buildMenuContainer.add(title);
+
+ const info = this.add.text(0, 40,
+ '[1] Fence (2 Wood)\n[2] Wall (2 Stone)\n[3] House (20W 20S 50G)',
+ { fontSize: '14px', fill: '#ffffff', align: 'center' }
+ ).setOrigin(0.5, 0);
+ this.buildMenuContainer.add(info);
+
+ this.selectedBuildingText = this.add.text(0, 80, 'Selected: Fence', { fontSize: '14px', fill: '#FFFF00' }).setOrigin(0.5, 0);
+ this.buildMenuContainer.add(this.selectedBuildingText);
+
+ this.buildMenuContainer.setVisible(false);
+ }
+
+ updateBuildSelection(name) {
+ if (this.selectedBuildingText) {
+ this.selectedBuildingText.setText(`Selected: ${name.toUpperCase()}`);
+ }
+ }
+ showProjectMenu(ruinData, onContribute) {
+ if (!this.projectMenuContainer) {
+ this.createProjectMenu();
+ }
+
+ // Update info
+ const costText = `Req: ${ruinData.reqWood} Wood, ${ruinData.reqStone} Stone`;
+ this.projectInfoText.setText(`RESTORING RUINS\n${costText}`);
+
+ this.onContributeCallback = onContribute;
+ this.projectMenuContainer.setVisible(true);
+ this.projectMenuContainer.setDepth(10000);
+ }
+
+ createProjectMenu() {
+ this.projectMenuContainer = this.add.container(this.width / 2, this.height / 2);
+
+ // BG
+ const bg = this.add.graphics();
+ bg.fillStyle(0x222222, 0.9);
+ bg.fillRect(-150, -100, 300, 200);
+ bg.lineStyle(2, 0x00FFFF, 1);
+ bg.strokeRect(-150, -100, 300, 200);
+ this.projectMenuContainer.add(bg);
+
+ // Title
+ const title = this.add.text(0, -80, 'PROJECT: RESTORATION', { fontSize: '20px', fill: '#00FFFF', fontStyle: 'bold' }).setOrigin(0.5);
+ this.projectMenuContainer.add(title);
+
+ // Info
+ this.projectInfoText = this.add.text(0, -20, 'Req: ???', { fontSize: '16px', fill: '#ffffff', align: 'center' }).setOrigin(0.5);
+ this.projectMenuContainer.add(this.projectInfoText);
+
+ // Button
+ const btnBg = this.add.rectangle(0, 50, 200, 40, 0x00aa00);
+ btnBg.setInteractive();
+ btnBg.on('pointerdown', () => {
+ if (this.onContributeCallback) this.onContributeCallback();
+ // Close menu? Or keep open to see result?
+ // For now close
+ this.projectMenuContainer.setVisible(false);
+ });
+ this.projectMenuContainer.add(btnBg);
+
+ const btnText = this.add.text(0, 50, 'CONTRIBUTE', { fontSize: '18px', fill: '#ffffff' }).setOrigin(0.5);
+ this.projectMenuContainer.add(btnText);
+
+ // Close Button
+ const closeBtn = this.add.text(130, -90, 'X', { fontSize: '20px', fill: '#ff0000' }).setOrigin(0.5);
+ closeBtn.setInteractive();
+ closeBtn.on('pointerdown', () => this.projectMenuContainer.setVisible(false));
+ this.projectMenuContainer.add(closeBtn);
+
+ this.projectMenuContainer.setVisible(false);
+ }
+
+ showTradeMenu(inventorySystem) {
+ if (!this.tradeMenuContainer) {
+ this.createTradeMenu(inventorySystem);
+ }
+
+ this.updateTradeMenu(inventorySystem);
+ this.tradeMenuContainer.setVisible(true);
+ this.tradeMenuContainer.setDepth(10000);
+ }
+
+ createTradeMenu(inventorySystem) {
+ this.tradeMenuContainer = this.add.container(this.width / 2, this.height / 2);
+
+ // BG
+ const bg = this.add.graphics();
+ bg.fillStyle(0x222222, 0.95);
+ bg.fillRect(-200, -150, 400, 300);
+ bg.lineStyle(2, 0xFFD700, 1); // Gold border
+ bg.strokeRect(-200, -150, 400, 300);
+ this.tradeMenuContainer.add(bg);
+
+ // Title
+ const title = this.add.text(0, -130, 'MERCHANT SHOP', { fontSize: '24px', fill: '#FFD700', fontStyle: 'bold' }).setOrigin(0.5);
+ this.tradeMenuContainer.add(title);
+
+ // Close Button
+ const closeBtn = this.add.text(180, -140, 'X', { fontSize: '20px', fill: '#ff0000', fontStyle: 'bold' }).setOrigin(0.5);
+ closeBtn.setInteractive({ useHandCursor: true });
+ closeBtn.on('pointerdown', () => this.tradeMenuContainer.setVisible(false));
+ this.tradeMenuContainer.add(closeBtn);
+
+ // Content Container (for items)
+ this.tradeItemsContainer = this.add.container(0, 0);
+ this.tradeMenuContainer.add(this.tradeItemsContainer);
+ }
+
+ updateTradeMenu(inventorySystem) {
+ this.tradeItemsContainer.removeAll(true);
+
+ // Items to Sell (Player has -> Merchant wants)
+ // Hardcoded prices for now
+ const prices = {
+ 'wheat': { price: 10, type: 'sell' },
+ 'wood': { price: 2, type: 'sell' },
+ 'seeds': { price: 5, type: 'buy' }
+ };
+
+ const startY = -80;
+ let index = 0;
+
+ // Header
+ const header = this.add.text(-180, startY, 'ITEM PRICE ACTION', { fontSize: '16px', fill: '#888888' });
+ this.tradeItemsContainer.add(header);
+
+ // 1. Sell Wheat
+ this.createTradeRow(inventorySystem, 'wheat', prices.wheat.price, 'SELL', index++, startY + 30);
+ // 2. Sell Wood
+ this.createTradeRow(inventorySystem, 'wood', prices.wood.price, 'SELL', index++, startY + 30);
+ // 3. Buy Seeds
+ this.createTradeRow(inventorySystem, 'seeds', prices.seeds.price, 'BUY', index++, startY + 30);
+ }
+
+ createTradeRow(inv, itemKey, price, action, index, yOffset) {
+ const y = yOffset + (index * 40);
+
+ // Name
+ const name = this.add.text(-180, y, itemKey.toUpperCase(), { fontSize: '18px', fill: '#ffffff' });
+ this.tradeItemsContainer.add(name);
+
+ // Price
+ const priceText = this.add.text(-50, y, `${price}g`, { fontSize: '18px', fill: '#FFD700' });
+ this.tradeItemsContainer.add(priceText);
+
+ // Button
+ const btnX = 100;
+ const btnBg = this.add.rectangle(btnX, y + 10, 80, 30, action === 'BUY' ? 0x008800 : 0x880000);
+ btnBg.setInteractive({ useHandCursor: true });
+
+ const btnLabel = this.add.text(btnX, y + 10, action, { fontSize: '16px', fill: '#ffffff' }).setOrigin(0.5);
+
+ btnBg.on('pointerdown', () => {
+ if (action === 'SELL') {
+ if (inv.hasItem(itemKey, 1)) {
+ inv.removeItem(itemKey, 1);
+ inv.gold += price;
+ inv.updateUI();
+ // Refresh visuals?
+ } else {
+ // Fail feedback
+ btnLabel.setText('NO ITEM');
+ this.scene.time.delayedCall(500, () => btnLabel.setText(action));
+ }
+ } else if (action === 'BUY') {
+ if (inv.gold >= price) {
+ inv.gold -= price;
+ inv.addItem(itemKey, 1);
+ inv.updateUI();
+ } else {
+ // Fail feedback
+ btnLabel.setText('NO GOLD');
+ this.scene.time.delayedCall(500, () => btnLabel.setText(action));
+ }
+ }
+ });
+
+ this.tradeItemsContainer.add(btnBg);
+ this.tradeItemsContainer.add(btnLabel);
+ }
+}
diff --git a/src/systems/BuildingSystem.js b/src/systems/BuildingSystem.js
new file mode 100644
index 0000000..6bc600e
--- /dev/null
+++ b/src/systems/BuildingSystem.js
@@ -0,0 +1,112 @@
+class BuildingSystem {
+ constructor(scene) {
+ this.scene = scene;
+ this.isBuildMode = false;
+ this.selectedBuilding = 'fence'; // fence, wall, house
+
+ this.buildingsData = {
+ fence: { name: 'Fence', cost: { wood: 2 }, w: 1, h: 1 },
+ wall: { name: 'Stone Wall', cost: { stone: 2 }, w: 1, h: 1 },
+ house: { name: 'House', cost: { wood: 20, stone: 20, gold: 50 }, w: 1, h: 1 } // Visual is bigger but anchor is 1 tile
+ };
+
+ // Textures init
+ if (!this.scene.textures.exists('struct_fence')) TextureGenerator.createStructureSprite(this.scene, 'struct_fence', 'fence');
+ if (!this.scene.textures.exists('struct_wall')) TextureGenerator.createStructureSprite(this.scene, 'struct_wall', 'wall');
+ if (!this.scene.textures.exists('struct_house')) TextureGenerator.createStructureSprite(this.scene, 'struct_house', 'house');
+ }
+
+ toggleBuildMode() {
+ this.isBuildMode = !this.isBuildMode;
+ console.log(`🔨 Build Mode: ${this.isBuildMode}`);
+
+ // Update UI
+ const uiScene = this.scene.scene.get('UIScene');
+ if (uiScene) {
+ uiScene.toggleBuildMenu(this.isBuildMode);
+ }
+ }
+
+ selectBuilding(type) {
+ if (this.buildingsData[type]) {
+ this.selectedBuilding = type;
+ console.log(`🔨 Selected: ${this.selectedBuilding}`);
+
+ // UI feedback?
+ const uiScene = this.scene.scene.get('UIScene');
+ if (uiScene) uiScene.updateBuildSelection(type);
+ }
+ }
+
+ tryBuild(gridX, gridY) {
+ if (!this.isBuildMode) return false;
+
+ const building = this.buildingsData[this.selectedBuilding];
+ const inv = this.scene.inventorySystem;
+ const terrain = this.scene.terrainSystem;
+
+ // 1. Check Cost
+ if (building.cost.wood) {
+ if (!inv.hasItem('wood', building.cost.wood)) {
+ console.log('❌ Not enough Wood!');
+ this.showFloatingText('Need Wood!', gridX, gridY, '#FF0000');
+ return true; // We handled the click, even if failed
+ }
+ }
+ if (building.cost.stone) {
+ if (!inv.hasItem('stone', building.cost.stone)) {
+ console.log('❌ Not enough Stone!');
+ this.showFloatingText('Need Stone!', gridX, gridY, '#FF0000');
+ return true;
+ }
+ }
+ if (building.cost.gold) {
+ if (inv.gold < building.cost.gold) {
+ console.log('❌ Not enough Gold!');
+ this.showFloatingText('Need Gold!', gridX, gridY, '#FF0000');
+ return true;
+ }
+ }
+
+ // 2. Check Space
+ const tile = terrain.getTile(gridX, gridY);
+ if (!tile || tile.type === 'water' || tile.hasDecoration || tile.hasCrop || tile.hasBuilding) {
+ console.log('❌ Space occupied!');
+ this.showFloatingText('Occupied!', gridX, gridY, '#FF0000');
+ return true;
+ }
+
+ // 3. Consume Resources
+ if (building.cost.wood) inv.removeItem('wood', building.cost.wood);
+ if (building.cost.stone) inv.removeItem('stone', building.cost.stone);
+ if (building.cost.gold) {
+ inv.gold -= building.cost.gold;
+ inv.updateUI();
+ }
+
+ // 4. Place Building
+ // Using decorations layer for now, but marking as building
+ // Need to add texture to TerrainSystem pool?
+ // Or better: TerrainSystem should handle 'placing structure'
+
+ // Let's modify TerrainSystem to support 'structures' better or just hack decorations
+ const success = terrain.placeStructure(gridX, gridY, `struct_${this.selectedBuilding}`);
+ if (success) {
+ this.showFloatingText(`Built ${building.name}!`, gridX, gridY, '#00FF00');
+ }
+
+ return true;
+ }
+
+ showFloatingText(text, gridX, gridY, color) {
+ const iso = new IsometricUtils(48, 24);
+ const pos = iso.toScreen(gridX, gridY);
+ const popup = this.scene.add.text(
+ pos.x + this.scene.terrainOffsetX,
+ pos.y + this.scene.terrainOffsetY - 40,
+ text,
+ { fontSize: '14px', fill: color, stroke: '#000', strokeThickness: 3 }
+ ).setOrigin(0.5);
+ this.scene.tweens.add({ targets: popup, y: popup.y - 30, alpha: 0, duration: 2000, onComplete: () => popup.destroy() });
+ }
+}
diff --git a/src/systems/DayNightSystem.js b/src/systems/DayNightSystem.js
new file mode 100644
index 0000000..15c33c5
--- /dev/null
+++ b/src/systems/DayNightSystem.js
@@ -0,0 +1,91 @@
+class DayNightSystem {
+ constructor(scene, timeSystem) {
+ this.scene = scene;
+ this.timeSystem = timeSystem;
+
+ // Visual overlay
+ this.overlay = null;
+ this.currentPhase = 'day'; // dawn, day, dusk, night
+
+ this.init();
+ }
+
+ init() {
+ // Create lighting overlay
+ this.overlay = this.scene.add.graphics();
+ this.overlay.setDepth(4999); // Below weather, above everything else
+ this.overlay.setScrollFactor(0); // Fixed to camera
+ }
+
+ update() {
+ if (!this.timeSystem) return;
+
+ const hour = this.timeSystem.getCurrentHour();
+ const phase = this.getPhase(hour);
+
+ if (phase !== this.currentPhase) {
+ this.currentPhase = phase;
+ console.log(`🌅 Time of Day: ${phase} (${hour}:00)`);
+ }
+
+ this.updateLighting(hour);
+ }
+
+ getPhase(hour) {
+ if (hour >= 5 && hour < 7) return 'dawn'; // 5-7
+ if (hour >= 7 && hour < 18) return 'day'; // 7-18
+ if (hour >= 18 && hour < 20) return 'dusk'; // 18-20
+ return 'night'; // 20-5
+ }
+
+ updateLighting(hour) {
+ const width = this.scene.cameras.main.width;
+ const height = this.scene.cameras.main.height;
+
+ this.overlay.clear();
+
+ let color = 0x000033; // Default night blue
+ let alpha = 0;
+
+ if (hour >= 0 && hour < 5) {
+ // Deep Night (0-5h) - Dark blue
+ color = 0x000033;
+ alpha = 0.6;
+ } else if (hour >= 5 && hour < 7) {
+ // Dawn (5-7h) - Orange/Pink gradient
+ color = 0xFF6B35;
+ const progress = (hour - 5) / 2; // 0-1
+ alpha = 0.6 - (progress * 0.6); // 0.6 -> 0
+ } else if (hour >= 7 && hour < 18) {
+ // Day (7-18h) - No overlay (bright)
+ alpha = 0;
+ } else if (hour >= 18 && hour < 20) {
+ // Dusk (18-20h) - Orange/Purple
+ color = 0x8B4789;
+ const progress = (hour - 18) / 2; // 0-1
+ alpha = progress * 0.5; // 0 -> 0.5
+ } else if (hour >= 20 && hour < 24) {
+ // Night (20-24h) - Dark blue
+ color = 0x000033;
+ const progress = (hour - 20) / 4; // 0-1
+ alpha = 0.5 + (progress * 0.1); // 0.5 -> 0.6
+ }
+
+ if (alpha > 0) {
+ this.overlay.fillStyle(color, alpha);
+ this.overlay.fillRect(0, 0, width, height);
+ }
+ }
+
+ getCurrentPhase() {
+ return this.currentPhase;
+ }
+
+ isNight() {
+ return this.currentPhase === 'night';
+ }
+
+ isDay() {
+ return this.currentPhase === 'day';
+ }
+}
diff --git a/src/systems/FarmingSystem.js b/src/systems/FarmingSystem.js
new file mode 100644
index 0000000..7a256d3
--- /dev/null
+++ b/src/systems/FarmingSystem.js
@@ -0,0 +1,116 @@
+class FarmingSystem {
+ constructor(scene) {
+ this.scene = scene;
+ this.growthTimer = 0;
+ this.growthTickRate = 5000; // Check growth every 5 seconds (real time)
+ // Or better: based on TimeSystem days?
+ // For fast testing: rapid growth.
+ }
+
+ // Called by InteractionSystem
+ interact(gridX, gridY, toolType) {
+ const terrain = this.scene.terrainSystem;
+ const tile = terrain.getTile(gridX, gridY);
+
+ if (!tile) return false;
+
+ // 1. HARVEST (Right click or just click ripe crop?)
+ // Let's say if it has crop and it is ripe, harvest it regardless of tool.
+ if (tile.hasCrop) {
+ const crop = terrain.cropsMap.get(`${gridX},${gridY}`);
+ if (crop && crop.stage === 4) {
+ this.harvest(gridX, gridY);
+ return true;
+ }
+ }
+
+ // 2. TILLING (Requires Hoe)
+ if (toolType === 'hoe') {
+ if (tile.type === 'grass' || tile.type === 'dirt') {
+ if (!tile.hasDecoration && !tile.hasCrop) {
+ console.log('🚜 Tilling soil...');
+ terrain.setTileType(gridX, gridY, 'farmland');
+ // Play sound
+ return true;
+ }
+ }
+ }
+
+ // 3. PLANTING (Requires Seeds)
+ if (toolType === 'seeds') {
+ if (tile.type === 'farmland' && !tile.hasCrop && !tile.hasDecoration) {
+ console.log('🌱 Planting seeds...');
+ this.plant(gridX, gridY);
+ return true; // Consume seed logic handled by caller?
+ }
+ }
+
+ return false;
+ }
+
+ plant(x, y) {
+ const terrain = this.scene.terrainSystem;
+ const cropData = {
+ gridX: x,
+ gridY: y,
+ stage: 1, // Seeds
+ type: 'wheat', // Default for now
+ timer: 0,
+ maxTime: 10 // Seconds per stage?
+ };
+ terrain.addCrop(x, y, cropData);
+ }
+
+ harvest(x, y) {
+ const terrain = this.scene.terrainSystem;
+ console.log('🌾 Harvesting!');
+
+ // Spawn loot
+ if (this.scene.interactionSystem) {
+ this.scene.interactionSystem.spawnLoot(x, y, 'wheat');
+ this.scene.interactionSystem.spawnLoot(x, y, 'seeds'); // Return seeds
+ // 50% chance for extra seeds
+ if (Math.random() > 0.5) this.scene.interactionSystem.spawnLoot(x, y, 'seeds');
+ }
+
+ // Remove crop
+ terrain.removeCrop(x, y);
+
+ // Revert to dirt? Or keep farmland? Usually keeps farmland.
+ }
+
+ update(delta) {
+ // Growth Logic
+ // Iterate all crops? Expensive?
+ // Better: Random tick like Minecraft or list iteration.
+ // Since we have cropsMap, iteration is easy.
+
+ // Only run every 1 second (1000ms) to save PERF
+ this.growthTimer += delta;
+ if (this.growthTimer < 1000) return;
+ const secondsPassed = this.growthTimer / 1000;
+ this.growthTimer = 0;
+
+ const terrain = this.scene.terrainSystem;
+ if (!terrain) return;
+
+ for (const [key, crop] of terrain.cropsMap) {
+ if (crop.stage < 4) {
+ crop.timer += secondsPassed;
+
+ // Growth thresholds (fast for testing)
+ // Stage 1 -> 2: 5s
+ // Stage 2 -> 3: 10s
+ // Stage 3 -> 4: 15s
+ const needed = 5;
+
+ if (crop.timer >= needed) {
+ crop.stage++;
+ crop.timer = 0;
+ terrain.updateCropVisual(crop.gridX, crop.gridY, crop.stage);
+ // Particle effect?
+ }
+ }
+ }
+ }
+}
diff --git a/src/systems/InteractionSystem.js b/src/systems/InteractionSystem.js
new file mode 100644
index 0000000..cfbc23c
--- /dev/null
+++ b/src/systems/InteractionSystem.js
@@ -0,0 +1,251 @@
+class InteractionSystem {
+ constructor(scene) {
+ this.scene = scene;
+ this.iso = new IsometricUtils(48, 24);
+
+ // Input listener setup (only once)
+ this.scene.input.on('pointerdown', (pointer) => {
+ if (pointer.button === 0) { // Left Click
+ this.handleLeftClick(pointer);
+ }
+ });
+
+ // Loot Array
+ this.drops = [];
+ }
+
+ handleLeftClick(pointer) {
+ if (!this.scene.player) return;
+
+ // 1. Account for camera and offset
+ const worldX = pointer.worldX - this.scene.terrainOffsetX;
+ const worldY = pointer.worldY - this.scene.terrainOffsetY;
+
+ // 2. Convert to Grid
+ const gridPos = this.iso.toGrid(worldX, worldY);
+
+ // 3. Check distance
+ const playerPos = this.scene.player.getPosition();
+ const dist = Phaser.Math.Distance.Between(playerPos.x, playerPos.y, gridPos.x, gridPos.y);
+
+ // Allow interaction within radius of 2.5 tiles
+ if (dist > 2.5) {
+ console.log('Too far:', dist.toFixed(1));
+ return;
+ }
+
+ console.log(`☝️ Clicked tile: ${gridPos.x},${gridPos.y}`);
+
+ // DETERMINE TOOL / ACTION
+ let activeTool = null;
+ const uiScene = this.scene.scene.get('UIScene');
+ const invSys = this.scene.inventorySystem;
+
+ if (uiScene && invSys) {
+ const selectedIdx = uiScene.selectedSlot;
+ const slotData = invSys.slots[selectedIdx];
+ if (slotData) activeTool = slotData.type;
+ }
+
+ // 0. Build Mode Override
+ if (this.scene.buildingSystem && this.scene.buildingSystem.isBuildMode) {
+ this.scene.buildingSystem.tryBuild(gridPos.x, gridPos.y);
+ return; // Consume click
+ }
+
+ // 3.5 Check for NPC Click
+ if (this.scene.npcs) {
+ for (const npc of this.scene.npcs) {
+ if (Math.abs(npc.gridX - gridPos.x) < 2.5 && Math.abs(npc.gridY - gridPos.y) < 2.5) {
+ console.log(`🗣️ Interact with NPC: ${npc.type}`);
+
+ if (npc.type === 'zombie') {
+ // Taming Logic
+ npc.toggleState();
+ return; // Done
+ }
+
+ if (npc.type === 'merchant') {
+ // Open Trade Menu
+ if (uiScene && invSys) {
+ uiScene.showTradeMenu(invSys);
+ }
+ return; // Stop processing
+ }
+ return; // Stop processing other clicks (farming/terrain) if clicked NPC
+ }
+ }
+ }
+
+ // 4. Try Farming Action (Tilling, Planting, Harvesting)
+ if (this.scene.farmingSystem) {
+ const didFarm = this.scene.farmingSystem.interact(gridPos.x, gridPos.y, activeTool);
+ if (didFarm) {
+ // Animation?
+ return;
+ }
+ }
+
+ // 5. Try damage decoration (fallback)
+ // 5. Try damage or interact decoration
+ if (this.scene.terrainSystem) {
+ const id = `${gridPos.x},${gridPos.y}`;
+ if (this.scene.terrainSystem.decorationsMap.has(id)) {
+ const decor = this.scene.terrainSystem.decorationsMap.get(id);
+
+
+ // Ruin Interaction - Town Restoration
+ if (decor.type === 'ruin' || decor.type === 'ruin_borut') {
+ // Check if near
+ if (dist > 2.5) {
+ console.log('Ruin too far.');
+ return;
+ }
+
+ // Show Project Menu
+ const uiScene = this.scene.scene.get('UIScene');
+ if (uiScene) {
+ // Define requirements based on ruin type
+ let req = { reqWood: 100, reqStone: 50, reqGold: 50 };
+ let ruinName = "Borut's Smithy"; // Default
+ let npcType = 'merchant';
+
+ if (decor.type === 'ruin') {
+ ruinName = "Merchant House";
+ req = { reqWood: 50, reqStone: 30, reqGold: 30 };
+ }
+
+ uiScene.showProjectMenu(req, () => {
+ // On Contribute Logic
+ if (invSys) {
+ const hasWood = invSys.hasItem('wood', req.reqWood);
+ const hasStone = invSys.hasItem('stone', req.reqStone || 0);
+ const hasGold = invSys.hasItem('gold', req.reqGold || 0);
+
+ // Check all requirements
+ if (hasWood && hasStone && hasGold) {
+ // Consume materials
+ invSys.removeItem('wood', req.reqWood);
+ invSys.removeItem('stone', req.reqStone);
+ invSys.removeItem('gold', req.reqGold);
+ invSys.updateUI();
+
+ console.log(`🏗️ Restoring ${ruinName}...`);
+
+ // Transform Ruin -> House
+ this.scene.terrainSystem.removeDecoration(gridPos.x, gridPos.y);
+ this.scene.terrainSystem.placeStructure(gridPos.x, gridPos.y, 'house');
+
+ // Spawn NPC nearby
+ const npc = new NPC(this.scene, gridPos.x + 1, gridPos.y + 1,
+ this.scene.terrainOffsetX, this.scene.terrainOffsetY, npcType);
+ this.scene.npcs.push(npc);
+
+ // Increase friendship (hearts)
+ if (this.scene.statsSystem) {
+ this.scene.statsSystem.addFriendship(npcType, 10); // +10 hearts
+ }
+
+ console.log(`✅ ${ruinName} Restored! +10 ❤️ Friendship`);
+
+ // Play build sound
+ if (this.scene.soundManager) this.scene.soundManager.playBuild();
+ } else {
+ // Not enough materials
+ const missing = [];
+ if (!hasWood) missing.push(`${req.reqWood} Wood`);
+ if (!hasStone) missing.push(`${req.reqStone} Stone`);
+ if (!hasGold) missing.push(`${req.reqGold} Gold`);
+
+ console.log(`❌ Not enough materials! Need: ${missing.join(', ')}`);
+ alert(`Potrebuješ še: ${missing.join(', ')} da obnoviš ${ruinName}.`);
+ }
+ }
+ });
+ }
+ return; // Don't damage it
+ }
+ }
+
+ const result = this.scene.terrainSystem.damageDecoration(gridPos.x, gridPos.y, 1);
+
+ if (result === 'destroyed') {
+ // Play chop sound
+ if (this.scene.soundManager) this.scene.soundManager.playChop();
+
+ // Spawn loot
+ this.spawnLoot(gridPos.x, gridPos.y, 'wood');
+ } else if (result === 'hit') {
+ // Play hit sound
+ if (this.scene.soundManager) this.scene.soundManager.playChop();
+ }
+ }
+ }
+
+ spawnLoot(gridX, gridY, type) {
+ console.log(`🎁 Spawning ${type} at ${gridX},${gridY}`);
+
+ // Convert to Screen
+ const screenPos = this.iso.toScreen(gridX, gridY);
+ const x = screenPos.x + this.scene.terrainOffsetX;
+ const y = screenPos.y + this.scene.terrainOffsetY;
+
+ // Create simplistic item drop sprite
+ let symbol = '?';
+ if (type === 'wood') symbol = '🪵';
+ if (type === 'seeds') symbol = '🌱';
+ if (type === 'wheat') symbol = '🌾';
+ if (type === 'hoe') symbol = '🛠️';
+
+ const drop = this.scene.add.text(x, y - 20, symbol, { fontSize: '20px' });
+ drop.setOrigin(0.5);
+ drop.setDepth(this.iso.getDepth(gridX, gridY) + 500); // above tiles
+
+ // Bounce animation
+ this.scene.tweens.add({
+ targets: drop,
+ y: y - 40,
+ duration: 500,
+ yoyo: true,
+ ease: 'Sine.easeOut',
+ repeat: -1
+ });
+
+ this.drops.push({
+ gridX,
+ gridY,
+ sprite: drop,
+ type: type
+ });
+ }
+
+ update() {
+ // Check for player pickup
+ if (!this.scene.player) return;
+
+ const playerPos = this.scene.player.getPosition();
+
+ // Filter drops to pick up
+ for (let i = this.drops.length - 1; i >= 0; i--) {
+ const drop = this.drops[i];
+
+ // Check if player is ON the drop tile
+ if (Math.abs(drop.gridX - playerPos.x) < 0.8 && Math.abs(drop.gridY - playerPos.y) < 0.8) {
+ // Pick up!
+ console.log('🎒 Picked up:', drop.type);
+
+ // Play pickup sound
+ if (this.scene.soundManager) this.scene.soundManager.playPickup();
+
+ // Add to inventory
+ if (this.scene.inventorySystem) {
+ this.scene.inventorySystem.addItem(drop.type, 1);
+ }
+
+ // Destroy visual
+ drop.sprite.destroy();
+ this.drops.splice(i, 1);
+ }
+ }
+ }
+}
diff --git a/src/systems/InventorySystem.js b/src/systems/InventorySystem.js
new file mode 100644
index 0000000..e8e941b
--- /dev/null
+++ b/src/systems/InventorySystem.js
@@ -0,0 +1,85 @@
+class InventorySystem {
+ constructor(scene) {
+ this.scene = scene;
+
+ // Data structure: Array of slots
+ // Each slot: { type: 'wood', count: 5 } or null
+ this.slots = new Array(9).fill(null);
+ this.maxStack = 99;
+
+ // Initial test items
+ this.addItem('hoe', 1);
+ this.addItem('seeds', 10);
+ this.addItem('wood', 100); // For restoration
+ this.addItem('stone', 100); // For restoration
+
+ this.gold = 0;
+ }
+
+ addItem(type, count) {
+ // 1. Try to stack
+ for (let i = 0; i < this.slots.length; i++) {
+ if (this.slots[i] && this.slots[i].type === type) {
+ const space = this.maxStack - this.slots[i].count;
+ if (space > 0) {
+ const toAdd = Math.min(space, count);
+ this.slots[i].count += toAdd;
+ count -= toAdd;
+ if (count === 0) break;
+ }
+ }
+ }
+
+ // 2. Empty slots
+ if (count > 0) {
+ for (let i = 0; i < this.slots.length; i++) {
+ if (!this.slots[i]) {
+ const toAdd = Math.min(this.maxStack, count);
+ this.slots[i] = { type: type, count: toAdd };
+ count -= toAdd;
+ if (count === 0) break;
+ }
+ }
+ }
+
+ this.updateUI();
+
+ return count === 0; // True if everything added
+ }
+
+ removeItem(type, count) {
+ for (let i = 0; i < this.slots.length; i++) {
+ if (this.slots[i] && this.slots[i].type === type) {
+ if (this.slots[i].count >= count) {
+ this.slots[i].count -= count;
+ if (this.slots[i].count === 0) this.slots[i] = null;
+ this.updateUI();
+ return true;
+ } else {
+ count -= this.slots[i].count;
+ this.slots[i] = null;
+ }
+ }
+ }
+ this.updateUI();
+ return false; // Not enough items
+ }
+
+ updateUI() {
+ const uiScene = this.scene.scene.get('UIScene');
+ if (uiScene) {
+ uiScene.updateInventory(this.slots);
+ if (uiScene.updateGold) uiScene.updateGold(this.gold);
+ }
+ }
+
+ hasItem(type, count) {
+ let total = 0;
+ for (const slot of this.slots) {
+ if (slot && slot.type === type) {
+ total += slot.count;
+ }
+ }
+ return total >= count;
+ }
+}
diff --git a/src/systems/ParallaxSystem.js b/src/systems/ParallaxSystem.js
new file mode 100644
index 0000000..d6af1b8
--- /dev/null
+++ b/src/systems/ParallaxSystem.js
@@ -0,0 +1,198 @@
+class ParallaxSystem {
+ constructor(scene) {
+ this.scene = scene;
+ this.layers = [];
+
+ // Layer depths (Phaser depth sorting)
+ this.DEPTH = {
+ SKY: -1000,
+ DISTANT_HILLS: -500,
+ FAR_TREES: -100,
+ TERRAIN: 0,
+ GAME_OBJECTS: 1000,
+ FOREGROUND_GRASS: 5000,
+ FOREGROUND_LEAVES: 5500
+ };
+
+ this.init();
+ }
+
+ init() {
+ console.log('🌄 ParallaxSystem: Initialized');
+
+ // Layer 1: Sky/Hills (Distant background)
+ this.createSkyLayer();
+ this.createDistantHills();
+
+ // Layer 4: Foreground overlay (High grass patches)
+ this.createForegroundGrass();
+ }
+
+ createSkyLayer() {
+ // Gradient sky rectangle
+ const width = 3000;
+ const height = 2000;
+
+ const skyBg = this.scene.add.rectangle(0, 0, width, height, 0x87CEEB); // Sky blue
+ skyBg.setOrigin(0, 0);
+ skyBg.setScrollFactor(0); // Fixed (no parallax)
+ skyBg.setDepth(this.DEPTH.SKY);
+
+ this.layers.push({
+ name: 'sky',
+ objects: [skyBg],
+ scrollFactor: 0
+ });
+ }
+
+ createDistantHills() {
+ // Create simple hill silhouettes in background
+ const hillCount = 5;
+ const hills = [];
+
+ for (let i = 0; i < hillCount; i++) {
+ const x = i * 800 - 1000;
+ const y = 600;
+ const width = Phaser.Math.Between(400, 800);
+ const height = Phaser.Math.Between(150, 300);
+
+ // Create hill as ellipse
+ const hill = this.scene.add.ellipse(x, y, width, height, 0x4a5f3a); // Dark green
+ hill.setAlpha(0.4);
+ hill.setDepth(this.DEPTH.DISTANT_HILLS);
+ hill.setScrollFactor(0.2, 0.2); // Slow parallax
+
+ hills.push(hill);
+ }
+
+ this.layers.push({
+ name: 'distant_hills',
+ objects: hills,
+ scrollFactor: 0.2
+ });
+ }
+
+ createForegroundGrass() {
+ // Create tall grass patches that appear in front of player
+ const grassPatches = [];
+ const patchCount = 30;
+
+ for (let i = 0; i < patchCount; i++) {
+ const x = Phaser.Math.Between(-500, 2500);
+ const y = Phaser.Math.Between(-500, 2500);
+
+ const grass = this.createGrassPatch(x, y);
+ grass.setDepth(this.DEPTH.FOREGROUND_GRASS);
+ grass.setScrollFactor(1.05, 1.05); // Slight forward parallax
+ grass.setAlpha(0.6);
+
+ grassPatches.push(grass);
+ }
+
+ this.layers.push({
+ name: 'foreground_grass',
+ objects: grassPatches,
+ scrollFactor: 1.05
+ });
+ }
+
+ createGrassPatch(x, y) {
+ // Create procedural grass patch
+ const graphics = this.scene.add.graphics();
+
+ // Draw several grass blades
+ for (let i = 0; i < 6; i++) {
+ const offsetX = Phaser.Math.Between(-10, 10);
+ const offsetY = Phaser.Math.Between(-5, 5);
+ const height = Phaser.Math.Between(20, 40);
+
+ graphics.fillStyle(0x3a5f2a, 0.8); // Dark grass green
+
+ // Draw grass blade (thin triangle)
+ graphics.beginPath();
+ graphics.moveTo(x + offsetX, y + offsetY);
+ graphics.lineTo(x + offsetX - 2, y + offsetY + height);
+ graphics.lineTo(x + offsetX + 2, y + offsetY + height);
+ graphics.closePath();
+ graphics.fillPath();
+ }
+
+ return graphics;
+ }
+
+ update(playerX, playerY) {
+ // Update foreground grass visibility based on player position
+ // Hide/show grass patches that are too close to player for better gameplay
+
+ const foregroundLayer = this.layers.find(l => l.name === 'foreground_grass');
+ if (!foregroundLayer) return;
+
+ for (const grass of foregroundLayer.objects) {
+ const distance = Phaser.Math.Distance.Between(
+ grass.x, grass.y,
+ playerX, playerY
+ );
+
+ // Fade out grass when player is very close
+ if (distance < 50) {
+ grass.setAlpha(0.2);
+ } else if (distance < 100) {
+ grass.setAlpha(0.4);
+ } else {
+ grass.setAlpha(0.6);
+ }
+ }
+ }
+
+ addFarTree(x, y) {
+ // Add a background tree (Layer 2)
+ if (!this.scene.textures.exists('tree')) return;
+
+ const tree = this.scene.add.sprite(x, y, 'tree');
+ tree.setDepth(this.DEPTH.FAR_TREES);
+ tree.setScrollFactor(0.7, 0.7); // Medium parallax
+ tree.setAlpha(0.7);
+ tree.setScale(1.5);
+
+ return tree;
+ }
+
+ addForegroundLeaves(x, y) {
+ // Add falling leaves or branch overlay (Layer 4)
+ const graphics = this.scene.add.graphics();
+
+ // Draw some leaves
+ for (let i = 0; i < 3; i++) {
+ const leafX = x + Phaser.Math.Between(-20, 20);
+ const leafY = y + Phaser.Math.Between(-10, 10);
+
+ graphics.fillStyle(0x2d4a1f, 0.5); // Dark green leaf
+ graphics.fillEllipse(leafX, leafY, 8, 12);
+ }
+
+ graphics.setDepth(this.DEPTH.FOREGROUND_LEAVES);
+ graphics.setScrollFactor(1.1, 1.1); // Fastest parallax (closest)
+
+ return graphics;
+ }
+
+ clearLayer(layerName) {
+ const layer = this.layers.find(l => l.name === layerName);
+ if (!layer) return;
+
+ for (const obj of layer.objects) {
+ obj.destroy();
+ }
+
+ layer.objects = [];
+ }
+
+ destroy() {
+ for (const layer of this.layers) {
+ for (const obj of layer.objects) {
+ obj.destroy();
+ }
+ }
+ this.layers = [];
+ }
+}
diff --git a/src/systems/SaveSystem.js b/src/systems/SaveSystem.js
new file mode 100644
index 0000000..c3420b7
--- /dev/null
+++ b/src/systems/SaveSystem.js
@@ -0,0 +1,246 @@
+class SaveSystem {
+ constructor(scene) {
+ this.scene = scene;
+ this.storageKey = 'novafarma_savefile';
+ }
+
+ saveGame() {
+ console.log('💾 Saving game...');
+
+ if (!this.scene.player || !this.scene.terrainSystem) {
+ console.error('Cannot save: Player or TerrainSystem missing.');
+ return;
+ }
+
+ const playerPos = this.scene.player.getPosition();
+
+ // Zberi podatke o NPCjih
+ const npcsData = this.scene.npcs.map(npc => ({
+ type: npc.type,
+ x: npc.gridX,
+ y: npc.gridY
+ }));
+
+ // Zberi podatke o terenu
+ const terrainSeed = this.scene.terrainSystem.noise.seed;
+
+ // Zberi dinamične podatke terena (Crops & Modified Decor/Buildings)
+ const cropsData = Array.from(this.scene.terrainSystem.cropsMap.entries());
+ // We only save Decorations that are NOT default?
+ // For simplicity, let's save ALL current decorations, and on Load clear everything and rebuild.
+ // Actually, decorationsMap contains objects with gridX, gridY, type.
+ const decorData = Array.from(this.scene.terrainSystem.decorationsMap.values());
+
+ // Inventory
+ const inventoryData = {
+ slots: this.scene.inventorySystem.slots,
+ gold: this.scene.inventorySystem.gold || 0
+ };
+
+ const saveData = {
+ version: 1.1,
+ timestamp: Date.now(),
+ player: { x: playerPos.x, y: playerPos.y },
+ terrain: {
+ seed: terrainSeed,
+ crops: cropsData, // array of [key, value]
+ decorations: decorData // array of objects
+ },
+ npcs: npcsData,
+ inventory: inventoryData,
+ time: {
+ gameTime: this.scene.timeSystem ? this.scene.timeSystem.gameTime : 8,
+ dayCount: this.scene.timeSystem ? this.scene.timeSystem.dayCount : 1
+ },
+ stats: this.scene.statsSystem ? {
+ health: this.scene.statsSystem.health,
+ hunger: this.scene.statsSystem.hunger,
+ thirst: this.scene.statsSystem.thirst
+ } : null,
+ camera: { zoom: this.scene.cameras.main.zoom }
+ };
+
+ try {
+ const jsonString = JSON.stringify(saveData);
+ localStorage.setItem(this.storageKey, jsonString);
+
+ // Pokaži obvestilo (preko UIScene če obstaja)
+ this.showNotification('GAME SAVED');
+ console.log('✅ Game saved successfully!', saveData);
+ } catch (e) {
+ console.error('❌ Failed to save game:', e);
+ this.showNotification('SAVE FAILED');
+ }
+ }
+
+ loadGame() {
+ console.log('📂 Loading game...');
+
+ const jsonString = localStorage.getItem(this.storageKey);
+ if (!jsonString) {
+ console.log('⚠️ No save file found.');
+ this.showNotification('NO SAVE FOUND');
+ return false;
+ }
+
+ try {
+ const saveData = JSON.parse(jsonString);
+ console.log('Loading save data:', saveData);
+
+ // 1. Load Player
+ if (this.scene.player) {
+ // Zahteva metodo setPosition(gridX, gridY) v Player.js
+ // Trenutno imamo moveToGrid ampak za instant load rabimo direkten set.
+ // Uporabimo updatePosition logic iz NPC, ali pa kar moveToGrid s hitrostjo 0?
+ // Bolje dodati setGridPosition v Player.js.
+ // Za zdaj workaround:
+ this.scene.player.gridX = saveData.player.x;
+ this.scene.player.gridY = saveData.player.y;
+ // Force update screen pos
+ const screenPos = this.scene.player.iso.toScreen(saveData.player.x, saveData.player.y);
+ this.scene.player.sprite.setPosition(
+ screenPos.x + this.scene.player.offsetX,
+ screenPos.y + this.scene.player.offsetY
+ );
+ this.scene.player.updateDepth();
+ }
+
+ // 2. Load Terrain (Regenerate + Restore dynamic)
+ if (this.scene.terrainSystem && saveData.terrain) {
+ // A) Seed / Base Terrain
+ if (saveData.terrain.seed && this.scene.terrainSystem.noise.seed !== saveData.terrain.seed) {
+ // Regenerate world if seed mismatch
+ // (Actually we might want to ALWAYS regenerate to clear default decors then overwrite?)
+ // Current logic: generate() spawns default decorations.
+ // To handle persistence properly:
+ // 1. Clear current decorations
+ // 2. Load saved decorations
+
+ this.scene.terrainSystem.noise = new PerlinNoise(saveData.terrain.seed);
+ // this.scene.terrainSystem.generate(); // This re-adds random flowers
+ // Instead of full generate, we might just re-calc tiles if seed changed?
+ // For now assume seed is constant for "New Game", but let's re-run generate to be safe
+ }
+
+ // Clear EVERYTHING first
+ this.scene.terrainSystem.decorationsMap.clear();
+ this.scene.terrainSystem.decorations = [];
+ this.scene.terrainSystem.cropsMap.clear();
+ // We should also hide active sprites?
+ this.scene.terrainSystem.visibleDecorations.forEach(s => s.setVisible(false));
+ this.scene.terrainSystem.visibleDecorations.clear();
+ this.scene.terrainSystem.visibleCrops.forEach(s => s.setVisible(false));
+ this.scene.terrainSystem.visibleCrops.clear();
+ this.scene.terrainSystem.decorationPool.releaseAll();
+ this.scene.terrainSystem.cropPool.releaseAll();
+
+ // B) Restore Crops
+ if (saveData.terrain.crops) {
+ // Map was saved as array of entries
+ saveData.terrain.crops.forEach(entry => {
+ const [key, cropData] = entry;
+ this.scene.terrainSystem.cropsMap.set(key, cropData);
+
+ // Set flag on tile
+ const [gx, gy] = key.split(',').map(Number);
+ const tile = this.scene.terrainSystem.getTile(gx, gy);
+ if (tile) tile.hasCrop = true;
+ });
+ }
+
+ // C) Restore Decorations (Flowers, Houses, Walls, Fences...)
+ if (saveData.terrain.decorations) {
+ saveData.terrain.decorations.forEach(d => {
+ this.scene.terrainSystem.decorations.push(d);
+ this.scene.terrainSystem.decorationsMap.set(d.id, d);
+
+ const tile = this.scene.terrainSystem.getTile(d.gridX, d.gridY);
+ if (tile) tile.hasDecoration = true;
+ });
+ }
+
+ // Force Update Visuals
+ this.scene.terrainSystem.updateCulling(this.scene.cameras.main);
+ }
+
+ // 3. Load Inventory
+ if (this.scene.inventorySystem && saveData.inventory) {
+ this.scene.inventorySystem.slots = saveData.inventory.slots;
+ this.scene.inventorySystem.gold = saveData.inventory.gold;
+ this.scene.inventorySystem.updateUI();
+ }
+
+ // 4. Load Time & Stats
+ if (this.scene.timeSystem && saveData.time) {
+ this.scene.timeSystem.gameTime = saveData.time.gameTime;
+ this.scene.timeSystem.dayCount = saveData.time.dayCount || 1;
+ }
+ if (this.scene.statsSystem && saveData.stats) {
+ this.scene.statsSystem.health = saveData.stats.health;
+ this.scene.statsSystem.hunger = saveData.stats.hunger;
+ this.scene.statsSystem.thirst = saveData.stats.thirst;
+ }
+
+ // 3. Load NPCs
+ // Pobriši trenutne
+ this.scene.npcs.forEach(npc => npc.destroy());
+ this.scene.npcs = [];
+
+ // Ustvari shranjene
+ if (saveData.npcs) {
+ saveData.npcs.forEach(npcData => {
+ const npc = new NPC(
+ this.scene,
+ npcData.x,
+ npcData.y,
+ this.scene.terrainOffsetX,
+ this.scene.terrainOffsetY,
+ npcData.type
+ );
+ this.scene.npcs.push(npc);
+ });
+ }
+
+ // 4. Camera
+ if (saveData.camera) {
+ this.scene.cameras.main.setZoom(saveData.camera.zoom);
+ }
+
+ this.showNotification('GAME LOADED');
+ return true;
+
+ } catch (e) {
+ console.error('❌ Failed to load game:', e);
+ this.showNotification('LOAD FAILED');
+ return false;
+ }
+ }
+
+ showNotification(text) {
+ const uiScene = this.scene.scene.get('UIScene');
+ if (uiScene) {
+ const width = uiScene.cameras.main.width;
+ const height = uiScene.cameras.main.height;
+
+ const msg = uiScene.add.text(width / 2, height / 2, text, {
+ fontFamily: 'Courier New',
+ fontSize: '32px',
+ fill: '#ffffff',
+ backgroundColor: '#000000',
+ padding: { x: 10, y: 5 }
+ });
+ msg.setOrigin(0.5);
+ msg.setScrollFactor(0);
+
+ uiScene.tweens.add({
+ targets: msg,
+ alpha: 0,
+ duration: 2000,
+ delay: 500,
+ onComplete: () => {
+ msg.destroy();
+ }
+ });
+ }
+ }
+}
diff --git a/src/systems/SoundManager.js b/src/systems/SoundManager.js
new file mode 100644
index 0000000..a657c55
--- /dev/null
+++ b/src/systems/SoundManager.js
@@ -0,0 +1,148 @@
+class SoundManager {
+ constructor(scene) {
+ this.scene = scene;
+ this.musicVolume = 0.3;
+ this.sfxVolume = 0.5;
+ this.isMuted = false;
+ this.currentMusic = null;
+ this.currentAmbient = null;
+ console.log('🎵 SoundManager: Initialized');
+ }
+
+ playSFX(key) {
+ if (this.isMuted) return;
+
+ if (this.scene.sound.get(key)) {
+ this.scene.sound.play(key, { volume: this.sfxVolume });
+ } else {
+ // Enhanced placeholder beeps
+ if (key === 'chop') {
+ this.beepChop();
+ } else if (key === 'pickup') {
+ this.beepPickup();
+ } else if (key === 'plant') {
+ this.beepPlant();
+ } else if (key === 'harvest') {
+ this.beepHarvest();
+ } else if (key === 'build') {
+ this.beepBuild();
+ }
+ }
+ }
+
+ beepChop() {
+ if (!this.scene.sound.context) return;
+ const ctx = this.scene.sound.context;
+ const osc = ctx.createOscillator();
+ const gain = ctx.createGain();
+ osc.connect(gain);
+ gain.connect(ctx.destination);
+ osc.frequency.value = 150;
+ osc.type = 'sawtooth';
+ gain.gain.setValueAtTime(0.15, ctx.currentTime);
+ gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.15);
+ osc.start();
+ osc.stop(ctx.currentTime + 0.15);
+ }
+
+ beepPickup() {
+ if (!this.scene.sound.context) return;
+ const ctx = this.scene.sound.context;
+ const osc = ctx.createOscillator();
+ const gain = ctx.createGain();
+ osc.connect(gain);
+ gain.connect(ctx.destination);
+ osc.frequency.setValueAtTime(600, ctx.currentTime);
+ osc.frequency.linearRampToValueAtTime(1200, ctx.currentTime + 0.1);
+ osc.type = 'sine';
+ gain.gain.setValueAtTime(0.12, ctx.currentTime);
+ gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.1);
+ osc.start();
+ osc.stop(ctx.currentTime + 0.1);
+ }
+
+ beepPlant() {
+ if (!this.scene.sound.context) return;
+ const ctx = this.scene.sound.context;
+ const osc = ctx.createOscillator();
+ const gain = ctx.createGain();
+ osc.connect(gain);
+ gain.connect(ctx.destination);
+ osc.frequency.value = 300;
+ osc.type = 'triangle';
+ gain.gain.setValueAtTime(0.1, ctx.currentTime);
+ gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.12);
+ osc.start();
+ osc.stop(ctx.currentTime + 0.12);
+ }
+
+ beepHarvest() {
+ if (!this.scene.sound.context) return;
+ const ctx = this.scene.sound.context;
+ const osc1 = ctx.createOscillator();
+ const gain1 = ctx.createGain();
+ osc1.connect(gain1);
+ gain1.connect(ctx.destination);
+ osc1.frequency.value = 523;
+ osc1.type = 'sine';
+ gain1.gain.setValueAtTime(0.1, ctx.currentTime);
+ gain1.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.08);
+ osc1.start();
+ osc1.stop(ctx.currentTime + 0.08);
+
+ const osc2 = ctx.createOscillator();
+ const gain2 = ctx.createGain();
+ osc2.connect(gain2);
+ gain2.connect(ctx.destination);
+ osc2.frequency.value = 659;
+ osc2.type = 'sine';
+ gain2.gain.setValueAtTime(0.1, ctx.currentTime + 0.08);
+ gain2.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.16);
+ osc2.start(ctx.currentTime + 0.08);
+ osc2.stop(ctx.currentTime + 0.16);
+ }
+
+ beepBuild() {
+ if (!this.scene.sound.context) return;
+ const ctx = this.scene.sound.context;
+ const osc = ctx.createOscillator();
+ const gain = ctx.createGain();
+ osc.connect(gain);
+ gain.connect(ctx.destination);
+ osc.frequency.value = 80;
+ osc.type = 'square';
+ gain.gain.setValueAtTime(0.2, ctx.currentTime);
+ gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.2);
+ osc.start();
+ osc.stop(ctx.currentTime + 0.2);
+ }
+
+ playAmbient(key, loop = true) {
+ if (this.isMuted) return;
+ if (this.currentAmbient) this.currentAmbient.stop();
+ if (!this.scene.sound.get(key)) return;
+ this.currentAmbient = this.scene.sound.add(key, { volume: this.sfxVolume * 0.5, loop: loop });
+ this.currentAmbient.play();
+ }
+
+ stopAmbient() {
+ if (this.currentAmbient) {
+ this.currentAmbient.stop();
+ this.currentAmbient = null;
+ }
+ }
+
+ toggleMute() {
+ this.isMuted = !this.isMuted;
+ this.scene.sound.mute = this.isMuted;
+ console.log(this.isMuted ? '🔇 Muted' : '🔊 Unmuted');
+ }
+
+ playChop() { this.playSFX('chop'); }
+ playPlant() { this.playSFX('plant'); }
+ playHarvest() { this.playSFX('harvest'); }
+ playBuild() { this.playSFX('build'); }
+ playPickup() { this.playSFX('pickup'); }
+ playRainSound() { this.playAmbient('rain_loop'); }
+ stopRainSound() { this.stopAmbient(); }
+}
diff --git a/src/systems/StatsSystem.js b/src/systems/StatsSystem.js
new file mode 100644
index 0000000..cb1dec9
--- /dev/null
+++ b/src/systems/StatsSystem.js
@@ -0,0 +1,135 @@
+class StatsSystem {
+ constructor(scene) {
+ this.scene = scene;
+
+ // Stats
+ this.health = 100;
+ this.maxHealth = 100;
+
+ this.hunger = 100; // 100 = full
+ this.maxHunger = 100;
+
+ this.thirst = 100; // 100 = not thirsty
+ this.maxThirst = 100;
+
+ // Decay rates (per second)
+ this.hungerDecay = 0.5; // Pade na 0 v 200s (cca 3 min)
+ this.thirstDecay = 0.8; // Pade na 0 v 125s (cca 2 min)
+
+ this.damageTickTimer = 0;
+
+ // Friendship System (Hearts ❤️)
+ this.friendship = {
+ merchant: 0,
+ zombie: 0,
+ villager: 0
+ };
+ }
+
+ update(delta) {
+ const seconds = delta / 1000;
+
+ // Decay
+ if (this.hunger > 0) {
+ this.hunger -= this.hungerDecay * seconds;
+ }
+ if (this.thirst > 0) {
+ this.thirst -= this.thirstDecay * seconds;
+ }
+
+ // Clamp values
+ this.hunger = Math.max(0, this.hunger);
+ this.thirst = Math.max(0, this.thirst);
+
+ // Starvation / Dehydration logic
+ if (this.hunger <= 0 || this.thirst <= 0) {
+ this.damageTickTimer += delta;
+ if (this.damageTickTimer >= 1000) { // Vsako sekundo damage
+ this.damageTickTimer = 0;
+ this.takeDamage(5); // 5 DMG na sekundo če si lačen/žejen
+
+ // Shake camera effect za opozorilo
+ this.scene.cameras.main.shake(100, 0.005);
+ }
+ } else {
+ this.damageTickTimer = 0;
+
+ // Natural regeneration if full
+ if (this.hunger > 80 && this.thirst > 80 && this.health < this.maxHealth) {
+ this.health += 1 * seconds;
+ }
+ }
+
+ this.health = Math.min(this.health, this.maxHealth);
+
+ this.updateUI();
+ }
+
+ takeDamage(amount) {
+ this.health -= amount;
+ if (this.health <= 0) {
+ this.health = 0;
+ this.die();
+ }
+ }
+
+ eat(amount) {
+ this.hunger += amount;
+ this.hunger = Math.min(this.hunger, this.maxHunger);
+ }
+
+ drink(amount) {
+ this.thirst += amount;
+ this.thirst = Math.min(this.thirst, this.maxThirst);
+ }
+
+ die() {
+ console.log('💀 Player died!');
+ // Zaenkrat samo respawn / reset
+ this.health = 100;
+ this.hunger = 100;
+ this.thirst = 100;
+
+ // Teleport to spawn
+ const spawnX = this.scene.terrainOffsetX + 500; // Dummy
+ const spawnY = this.scene.terrainOffsetY + 100; // Dummy
+ // Reset player pos...
+ // V pravi implementaciji bi klicali GameScene.respawnPlayer()
+
+ // Show notification
+ const uiScene = this.scene.scene.get('UIScene');
+ if (uiScene) {
+ const txt = uiScene.add.text(uiScene.width / 2, uiScene.height / 2, 'YOU DIED', {
+ fontSize: '64px', color: '#ff0000', fontStyle: 'bold'
+ }).setOrigin(0.5);
+ uiScene.time.delayedCall(2000, () => txt.destroy());
+ }
+ }
+
+ updateUI() {
+ const uiScene = this.scene.scene.get('UIScene');
+ if (uiScene) {
+ if (uiScene.healthBar) uiScene.setBarValue(uiScene.healthBar, this.health);
+ if (uiScene.hungerBar) uiScene.setBarValue(uiScene.hungerBar, this.hunger);
+ if (uiScene.thirstBar) uiScene.setBarValue(uiScene.thirstBar, this.thirst);
+ }
+ }
+
+ // Friendship System
+ addFriendship(npcType, amount) {
+ if (this.friendship[npcType] !== undefined) {
+ this.friendship[npcType] += amount;
+ console.log(`❤️ +${amount} Friendship with ${npcType} (Total: ${this.friendship[npcType]})`);
+ }
+ }
+
+ getFriendship(npcType) {
+ return this.friendship[npcType] || 0;
+ }
+
+ setFriendship(npcType, amount) {
+ if (this.friendship[npcType] !== undefined) {
+ this.friendship[npcType] = amount;
+ }
+ }
+}
diff --git a/src/systems/TerrainSystem.js b/src/systems/TerrainSystem.js
index a462563..ddf37cf 100644
--- a/src/systems/TerrainSystem.js
+++ b/src/systems/TerrainSystem.js
@@ -1,5 +1,5 @@
// Terrain Generator System
-// Generira proceduralni isometrični teren
+// Generira proceduralni isometrični teren in skrbi za optimizacijo (Culling, Object Pooling)
class TerrainSystem {
constructor(scene, width = 100, height = 100) {
this.scene = scene;
@@ -10,43 +10,670 @@ class TerrainSystem {
this.noise = new PerlinNoise(Date.now());
this.tiles = [];
- this.tileSprites = [];
+ this.decorations = []; // Array za save/load compat
+ this.decorationsMap = new Map(); // Fast lookup key->decor
+ this.cropsMap = new Map(); // Store dynamic crops separately
- // Tipi terena z threshold vrednostmi
+ // Render state monitoring
+ this.visibleTiles = new Map(); // Key: "x,y", Value: Sprite
+ this.visibleDecorations = new Map(); // Key: "x,y", Value: Sprite
+ this.visibleCrops = new Map(); // Key: "x,y", Value: Sprite
+
+ this.offsetX = 0;
+ this.offsetY = 0;
+
+ // Object Pools
+ this.tilePool = new ObjectPool(
+ () => {
+ const sprite = this.scene.add.image(0, 0, 'tile_grass');
+ sprite.setOrigin(0.5, 0); // Isometrični tiles imajo origin zgoraj/center ali po potrebi
+ return sprite;
+ },
+ (sprite) => {
+ sprite.setVisible(true);
+ sprite.setAlpha(1);
+ sprite.clearTint();
+ }
+ );
+
+ this.decorationPool = new ObjectPool(
+ () => {
+ const sprite = this.scene.add.sprite(0, 0, 'flower');
+ sprite.setOrigin(0.5, 1);
+ return sprite;
+ },
+ (sprite) => {
+ sprite.setVisible(true);
+ sprite.setAlpha(1);
+ sprite.clearTint(); // Reset damage tint
+ }
+ );
+
+ this.cropPool = new ObjectPool(
+ () => {
+ const sprite = this.scene.add.sprite(0, 0, 'crop_stage_1'); // Default texture logic needed
+ sprite.setOrigin(0.5, 1);
+ return sprite;
+ },
+ (sprite) => {
+ sprite.setVisible(true);
+ sprite.setAlpha(1);
+ }
+ );
+
+
+ // Tipi terena z threshold vrednostmi + Y-LAYER STACKING
this.terrainTypes = {
- WATER: { threshold: 0.3, color: 0x2166aa, name: 'water' },
- SAND: { threshold: 0.4, color: 0xf4e7c6, name: 'sand' },
- GRASS: { threshold: 0.65, color: 0x5cb85c, name: 'grass' },
- DIRT: { threshold: 0.75, color: 0x8b6f47, name: 'dirt' },
- STONE: { threshold: 1.0, color: 0x7d7d7d, name: 'stone' }
+ WATER: { threshold: 0.3, color: 0x2166aa, name: 'water', texture: 'tile_water', yLayer: -1 },
+ SAND: { threshold: 0.4, color: 0xf4e7c6, name: 'sand', texture: 'tile_sand', yLayer: 0 },
+
+ // Y-LAYER GRASS VARIANTS (A, B, C systém)
+ GRASS_FULL: { threshold: 0.50, color: 0x5cb85c, name: 'grass_full', texture: 'tile_grass_full', yLayer: 0 }, // A: Full grass
+ GRASS_TOP: { threshold: 0.60, color: 0x5cb85c, name: 'grass_top', texture: 'tile_grass_top', yLayer: 1 }, // B: Grass top, dirt sides
+ DIRT: { threshold: 0.75, color: 0x8b6f47, name: 'dirt', texture: 'tile_dirt', yLayer: 2 }, // C: Full dirt
+ STONE: { threshold: 1.0, color: 0x7d7d7d, name: 'stone', texture: 'tile_stone', yLayer: 3 },
+
+ FARMLAND: { threshold: 999, color: 0x4a3c2a, name: 'farmland', texture: 'tile_farmland', yLayer: 0 }
};
}
- // Generiraj teren
+ // Helper da dobi terrain type glede na elevation (Y-layer)
+ getTerrainTypeByElevation(noiseValue, elevation) {
+ // Osnovni terrain type iz noise
+ let baseType = this.getTerrainType(noiseValue);
+
+ // Če je grass, določi Y-layer variant glede na elevation
+ if (baseType.name.includes('grass') || baseType === this.terrainTypes.GRASS_FULL ||
+ baseType === this.terrainTypes.GRASS_TOP) {
+
+ if (elevation > 0.7) {
+ return this.terrainTypes.GRASS_FULL; // A: Najvišja plast (full grass)
+ } else if (elevation > 0.4) {
+ return this.terrainTypes.GRASS_TOP; // B: Srednja (grass top, dirt sides)
+ } else {
+ return this.terrainTypes.DIRT; // C: Nizka (full dirt)
+ }
+ }
+
+ return baseType;
+ }
+
+ // Generiraj teksture za tiles (da ne uporabljamo Počasnih Graphics objektov)
+ createTileTextures() {
+ console.log('🎨 Creating tile textures...');
+
+ for (const type of Object.values(this.terrainTypes)) {
+ const key = `tile_${type.name}`;
+ if (this.scene.textures.exists(key)) continue;
+
+ // Check for custom grass tile
+ if (type.name === 'grass' && this.scene.textures.exists('grass_tile')) {
+ this.scene.textures.addImage(key, this.scene.textures.get('grass_tile').getSourceImage());
+ type.texture = key;
+ continue; // Skip procedural generation
+ }
+
+ const tileW = this.iso.tileWidth;
+ const tileH = this.iso.tileHeight;
+ const thickness = 25; // Minecraft-style thickness (increased from 10)
+
+ const graphics = this.scene.make.graphics({ x: 0, y: 0, add: false });
+
+ // Helper for colors
+ const baseColor = Phaser.Display.Color.IntegerToColor(type.color);
+ let darkColor, darkerColor;
+
+ // Y-LAYER STACKING: Different side colors based on layer
+ if (type.name === 'grass_full') {
+ // A: Full Grass Block - všechno zeleno
+ darkColor = 0x5cb85c; // Green (lighter)
+ darkerColor = 0x4a9d3f; // Green (darker)
+ } else if (type.name === 'grass_top') {
+ // B: Grass Top - zgoraj zeleno, stranice zemlja
+ darkColor = 0x8b6f47; // Dirt color (brown)
+ darkerColor = 0x5d4a2e; // Darker Dirt
+ } else if (type.name === 'dirt') {
+ // C: Full Dirt - vse rjavo
+ darkColor = 0x8b6f47; // Dirt
+ darkerColor = 0x654321; // Darker Dirt
+ } else {
+ // Standard block: Darken base color significantly
+ darkColor = Phaser.Display.Color.IntegerToColor(type.color).darken(30).color;
+ darkerColor = Phaser.Display.Color.IntegerToColor(type.color).darken(50).color;
+ }
+
+ // 1. Draw LEFT Side (Darker) - Minecraft volumetric effect
+ graphics.fillStyle(darkColor, 1);
+ graphics.beginPath();
+ graphics.moveTo(0, tileH / 2); // Left Corner
+ graphics.lineTo(tileW / 2, tileH); // Bottom Corner
+ graphics.lineTo(tileW / 2, tileH + thickness); // Bottom Corner (Lowered)
+ graphics.lineTo(0, tileH / 2 + thickness); // Left Corner (Lowered)
+ graphics.closePath();
+ graphics.fillPath();
+
+ // 2. Draw RIGHT Side (Darkest) - Strong shadow
+ graphics.fillStyle(darkerColor, 1);
+ graphics.beginPath();
+ graphics.moveTo(tileW / 2, tileH); // Bottom Corner
+ graphics.lineTo(tileW, tileH / 2); // Right Corner
+ graphics.lineTo(tileW, tileH / 2 + thickness); // Right Corner (Lowered)
+ graphics.lineTo(tileW / 2, tileH + thickness); // Bottom Corner (Lowered)
+ graphics.closePath();
+ graphics.fillPath();
+
+ // 3. Draw TOP Surface (bright)
+ graphics.fillStyle(type.color, 1);
+ graphics.beginPath();
+ graphics.moveTo(tileW / 2, 0); // Top
+ graphics.lineTo(tileW, tileH / 2); // Right
+ graphics.lineTo(tileW / 2, tileH); // Bottom
+ graphics.lineTo(0, tileH / 2); // Left
+ graphics.closePath();
+ graphics.fillPath();
+
+ // 4. Add Minecraft-style texture pattern on top
+ if (type.name === 'grass_full' || type.name === 'grass_top') {
+ // Grass texture: Random pixel pattern
+ graphics.fillStyle(0x4a9d3f, 0.3); // Slightly darker green
+ for (let i = 0; i < 8; i++) {
+ const px = Math.random() * tileW;
+ const py = Math.random() * tileH;
+ graphics.fillRect(px, py, 2, 2);
+ }
+ } else if (type.name === 'dirt') {
+ // Dirt texture: Darker spots
+ graphics.fillStyle(0x6d5838, 0.4);
+ for (let i = 0; i < 6; i++) {
+ const px = Math.random() * tileW;
+ const py = Math.random() * tileH;
+ graphics.fillRect(px, py, 3, 3);
+ }
+ } else if (type.name === 'stone') {
+ // Stone texture: Gray spots
+ graphics.fillStyle(0x666666, 0.3);
+ for (let i = 0; i < 10; i++) {
+ const px = Math.random() * tileW;
+ const py = Math.random() * tileH;
+ graphics.fillRect(px, py, 2, 1);
+ }
+ }
+
+ // 5. Crisp black outline for block definition
+ graphics.lineStyle(1, 0x000000, 0.3);
+ graphics.strokePath();
+
+ // Generate texture
+ graphics.generateTexture(key, tileW, tileH + thickness);
+ graphics.destroy();
+
+ // Update texture name in type def
+ type.texture = key;
+ }
+ }
+
+ createGravestoneSprite() {
+ // Extract gravestone from objects_pack (approx position in atlas)
+ // Gravestone appears to be around position row 4, column 4-5 in the pack
+ const canvas = document.createElement('canvas');
+ const size = 32; // Approximate sprite size
+ canvas.width = size;
+ canvas.height = size;
+ const ctx = canvas.getContext('2d', { willReadFrequently: true });
+
+ const sourceTexture = this.scene.textures.get('objects_pack');
+ const sourceImg = sourceTexture.getSourceImage();
+
+ // Extract gravestone (cross tombstone) - estimated coords
+ // Adjust these values based on actual sprite sheet layout
+ const sourceX = 240; // Approximate X position
+ const sourceY = 160; // Approximate Y position
+
+ ctx.drawImage(sourceImg, sourceX, sourceY, size, size, 0, 0, size, size);
+
+ // Create texture
+ this.scene.textures.addCanvas('gravestone', canvas);
+ console.log('✅ Gravestone sprite extracted!');
+ }
+
+ // Generiraj teren (data only)
generate() {
- console.log(`🌍 Generating terrain: ${this.width}x${this.height}...`);
+ console.log(`🌍 Generating terrain data: ${this.width}x${this.height}...`);
+
+ // Zagotovi teksture
+ this.createTileTextures();
+
+ // Zagotovi decoration teksture - check for custom sprites first
+ if (!this.scene.textures.exists('flower')) {
+ TextureGenerator.createFlowerSprite(this.scene, 'flower');
+ }
+
+ // Bush - use custom stone sprite if available
+ if (this.scene.textures.exists('stone_sprite')) {
+ // Use stone_sprite for bushes (rocks)
+ if (!this.scene.textures.exists('bush')) {
+ this.scene.textures.addImage('bush', this.scene.textures.get('stone_sprite').getSourceImage());
+ }
+ } else if (!this.scene.textures.exists('bush')) {
+ TextureGenerator.createBushSprite(this.scene, 'bush');
+ }
+
+ // Tree - use custom tree sprite if available
+ if (this.scene.textures.exists('tree_sprite')) {
+ if (!this.scene.textures.exists('tree')) {
+ this.scene.textures.addImage('tree', this.scene.textures.get('tree_sprite').getSourceImage());
+ }
+ } else if (!this.scene.textures.exists('tree')) {
+ TextureGenerator.createTreeSprite(this.scene, 'tree');
+ }
+
+ // Gravestone - extract from objects_pack
+ if (this.scene.textures.exists('objects_pack') && !this.scene.textures.exists('gravestone')) {
+ this.createGravestoneSprite();
+ }
+
+ // Crop textures
+ for (let i = 1; i <= 4; i++) {
+ if (!this.scene.textures.exists(`crop_stage_${i}`))
+ TextureGenerator.createCropSprite(this.scene, `crop_stage_${i}`, i);
+ }
+
+ this.decorationsMap.clear();
+ this.decorations = [];
+ this.cropsMap.clear();
// Generiraj tile podatke
for (let y = 0; y < this.height; y++) {
this.tiles[y] = [];
for (let x = 0; x < this.width; x++) {
const noiseValue = this.noise.getNormalized(x, y, 0.05, 4);
- const terrainType = this.getTerrainType(noiseValue);
+
+ // Elevation (druga Perlin noise layer za hribe)
+ let elevation = this.noise.getNormalized(x, y, 0.03, 3);
+
+ // Get terrain type based on BOTH noise and elevation (Y-layer)
+ let terrainType = this.getTerrainTypeByElevation(noiseValue, elevation);
+
+ // === FLOATING ISLAND EDGE ===
+ const edgeDistance = 2; // Tiles from edge (tighter border)
+ const isNearEdge = x < edgeDistance || x >= this.width - edgeDistance ||
+ y < edgeDistance || y >= this.height - edgeDistance;
+
+ const isEdge = x === 0 || x === this.width - 1 ||
+ y === 0 || y === this.height - 1;
+
+ // Override terrain type at edges
+ if (isEdge) {
+ terrainType = this.terrainTypes.STONE; // Cliff wall (stone edge)
+ } else if (isNearEdge) {
+ // Keep Y-layer system active
+ }
+
+ // Flatten edges (cliff drop-off for floating island effect)
+ if (isEdge) {
+ elevation = 0; // Flat cliff wall
+ } else if (isNearEdge) {
+ elevation = Math.max(0, elevation - 0.2); // Slight dip near edge
+ }
this.tiles[y][x] = {
gridX: x,
gridY: y,
type: terrainType.name,
- color: terrainType.color,
- height: noiseValue
+ texture: terrainType.texture,
+ height: noiseValue,
+ elevation: elevation, // 0-1 (0=low, 1=high)
+ yLayer: terrainType.yLayer, // Y-stacking layer
+ hasDecoration: false,
+ hasCrop: false
};
+
+ // Generacija dekoracij (shranimo v data, ne ustvarjamo sprite-ov še)
+ if (x > 5 && x < this.width - 5 && y > 5 && y < this.height - 5) {
+ let decorType = null;
+ let maxHp = 1;
+
+ if (terrainType.name === 'grass') {
+ const rand = Math.random();
+
+ // Na hribih več kamnov
+ if (elevation > 0.6 && rand < 0.05) {
+ decorType = 'bush'; // Kamni (bomo kasneje naredili 'stone' tip)
+ maxHp = 5;
+ } else if (rand < 0.01) {
+ decorType = 'tree';
+ maxHp = 5;
+ } else if (rand < 0.015) {
+ decorType = 'gravestone'; // 💀 Nagrobniki
+ maxHp = 10; // Težje uničiti
+ } else if (rand < 0.1) {
+ decorType = 'flower';
+ maxHp = 1;
+ }
+ } else if (terrainType.name === 'dirt' && Math.random() < 0.02) {
+ decorType = 'bush';
+ maxHp = 3;
+ }
+
+ if (decorType) {
+ const key = `${x},${y}`;
+ const decorData = {
+ gridX: x,
+ gridY: y,
+ type: decorType,
+ id: key,
+ maxHp: maxHp,
+ hp: maxHp
+ };
+
+ this.decorations.push(decorData);
+ this.decorationsMap.set(key, decorData);
+
+ this.tiles[y][x].hasDecoration = true;
+ }
+ }
}
}
- console.log('✅ Terrain data generated!');
+ console.log('✅ Terrain and decorations data generated!');
}
- // Določi tip terena glede na noise vrednost
+ // DAMAGE / INTERACTION LOGIC
+ damageDecoration(x, y, amount) {
+ const key = `${x},${y}`;
+ const decor = this.decorationsMap.get(key);
+
+ if (!decor) return false;
+
+ decor.hp -= amount;
+
+ // Visual feedback (flash red)
+ if (this.visibleDecorations.has(key)) {
+ const sprite = this.visibleDecorations.get(key);
+ sprite.setTint(0xff0000);
+ this.scene.time.delayedCall(100, () => sprite.clearTint());
+
+ // Shake effect?
+ this.scene.tweens.add({
+ targets: sprite,
+ x: sprite.x + 2,
+ duration: 50,
+ yoyo: true,
+ repeat: 1
+ });
+ }
+
+ if (decor.hp <= 0) {
+ this.removeDecoration(x, y);
+ return 'destroyed';
+ }
+
+ return 'hit';
+ }
+
+ removeDecoration(x, y) {
+ const key = `${x},${y}`;
+ const decor = this.decorationsMap.get(key);
+
+ if (!decor) return;
+
+ // Remove visual
+ if (this.visibleDecorations.has(key)) {
+ const sprite = this.visibleDecorations.get(key);
+ sprite.setVisible(false);
+ this.decorationPool.release(sprite);
+ this.visibleDecorations.delete(key);
+ }
+
+ // Remove data
+ this.decorationsMap.delete(key);
+
+ // Remove from array (slow but needed for save compat for now)
+ const index = this.decorations.indexOf(decor);
+ if (index > -1) this.decorations.splice(index, 1);
+
+ // Update tile flag
+ if (this.tiles[y] && this.tiles[y][x]) {
+ this.tiles[y][x].hasDecoration = false;
+ }
+
+ return decor.type; // Return type for dropping loot
+ }
+
+ placeStructure(x, y, structureType) {
+ if (this.decorationsMap.has(`${x},${y}`)) return false;
+
+ const decorData = {
+ gridX: x,
+ gridY: y,
+ type: structureType, // 'struct_fence', etc.
+ id: `${x},${y}`,
+ maxHp: 5,
+ hp: 5
+ };
+
+ // Add to data
+ this.decorations.push(decorData);
+ this.decorationsMap.set(decorData.id, decorData);
+
+ // Update tile
+ const tile = this.getTile(x, y);
+ if (tile) tile.hasDecoration = true;
+
+ // Force Visual Update immediately?
+ // updateCulling will catch it on next frame, but to be safe:
+ // Or leave it to update loop.
+
+ return true;
+ }
+
+ // --- Dynamic Tile Modification ---
+
+ setTileType(x, y, typeName) {
+ if (!this.tiles[y] || !this.tiles[y][x]) return;
+
+ const typeDef = Object.values(this.terrainTypes).find(t => t.name === typeName);
+ if (!typeDef) return;
+
+ this.tiles[y][x].type = typeName;
+ this.tiles[y][x].texture = typeDef.texture;
+
+ // Force visual update if visible
+ const key = `${x},${y}`;
+ if (this.visibleTiles.has(key)) {
+ const sprite = this.visibleTiles.get(key);
+ sprite.setTexture(typeDef.texture);
+ }
+ }
+
+ addCrop(x, y, cropData) {
+ const key = `${x},${y}`;
+ this.cropsMap.set(key, cropData);
+ this.tiles[y][x].hasCrop = true;
+ // updateCulling loop will pick it up on next frame
+ }
+
+ removeCrop(x, y) {
+ const key = `${x},${y}`;
+ if (this.cropsMap.has(key)) {
+ // Remove visual
+ if (this.visibleCrops.has(key)) {
+ const sprite = this.visibleCrops.get(key);
+ sprite.setVisible(false);
+ this.cropPool.release(sprite);
+ this.visibleCrops.delete(key);
+ }
+ this.cropsMap.delete(key);
+ this.tiles[y][x].hasCrop = false;
+ }
+ }
+
+ updateCropVisual(x, y, stage) {
+ const key = `${x},${y}`;
+ if (this.visibleCrops.has(key)) {
+ const sprite = this.visibleCrops.get(key);
+ sprite.setTexture(`crop_stage_${stage}`);
+ }
+ }
+
+ // Initialize rendering (called once)
+ init(offsetX, offsetY) {
+ this.offsetX = offsetX;
+ this.offsetY = offsetY;
+ }
+
+ // Update culling (called every frame)
+ updateCulling(camera) {
+ const view = camera.worldView;
+ const buffer = 200;
+ const left = view.x - buffer - this.offsetX;
+ const top = view.y - buffer - this.offsetY;
+ const right = view.x + view.width + buffer - this.offsetX;
+ const bottom = view.y + view.height + buffer - this.offsetY;
+
+ // Calculate visible bounding box (rough)
+ const p1 = this.iso.toGrid(left, top);
+ const p2 = this.iso.toGrid(right, top);
+ const p3 = this.iso.toGrid(left, bottom);
+ const p4 = this.iso.toGrid(right, bottom);
+
+ const minGridX = Math.floor(Math.min(p1.x, p2.x, p3.x, p4.x));
+ const maxGridX = Math.ceil(Math.max(p1.x, p2.x, p3.x, p4.x));
+ const minGridY = Math.floor(Math.min(p1.y, p2.y, p3.y, p4.y));
+ const maxGridY = Math.ceil(Math.max(p1.y, p2.y, p3.y, p4.y));
+
+ const startX = Math.max(0, minGridX);
+ const endX = Math.min(this.width, maxGridX);
+ const startY = Math.max(0, minGridY);
+ const endY = Math.min(this.height, maxGridY);
+
+ const neededKeys = new Set();
+ const neededDecorKeys = new Set();
+ const neededCropKeys = new Set();
+
+ for (let y = startY; y < endY; y++) {
+ for (let x = startX; x < endX; x++) {
+ const key = `${x},${y}`;
+ neededKeys.add(key);
+
+ // Tile Logic
+ if (!this.visibleTiles.has(key)) {
+ const tilePos = this.iso.toScreen(x, y);
+ const tileData = this.tiles[y][x];
+
+ const sprite = this.tilePool.get();
+ sprite.setTexture(tileData.texture);
+
+ // Elevation effect: MOČAN vertikalni offset za hribe
+ const elevationOffset = tileData.elevation * -25; // Povečano iz -10 na -25
+ sprite.setPosition(
+ tilePos.x + this.offsetX,
+ tilePos.y + this.offsetY + elevationOffset
+ );
+
+ // DRAMATIČNO senčenje glede na višino
+ if (tileData.type === 'grass') {
+ let brightness = 1.0;
+
+ if (tileData.elevation > 0.5) {
+ // Visoko = svetlo (1.0 - 1.5)
+ brightness = 1.0 + (tileData.elevation - 0.5) * 1.0;
+ } else {
+ // Nizko = temno (0.7 - 1.0)
+ brightness = 0.7 + tileData.elevation * 0.6;
+ }
+
+ sprite.setTint(Phaser.Display.Color.GetColor(
+ Math.min(255, Math.floor(92 * brightness)),
+ Math.min(255, Math.floor(184 * brightness)),
+ Math.min(255, Math.floor(92 * brightness))
+ ));
+ }
+
+ sprite.setDepth(this.iso.getDepth(x, y));
+
+ this.visibleTiles.set(key, sprite);
+ }
+
+ // Crop Logic (render before decor or after? Same layer mostly)
+ if (this.tiles[y][x].hasCrop) {
+ neededCropKeys.add(key);
+ if (!this.visibleCrops.has(key)) {
+ const cropData = this.cropsMap.get(key);
+ if (cropData) {
+ const cropPos = this.iso.toScreen(x, y);
+ const sprite = this.cropPool.get();
+ sprite.setTexture(`crop_stage_${cropData.stage}`);
+ sprite.setPosition(
+ cropPos.x + this.offsetX,
+ cropPos.y + this.offsetY + this.iso.tileHeight / 2
+ );
+ const depth = this.iso.getDepth(x, y);
+ sprite.setDepth(depth + 1); // Just slightly above tile
+
+ this.visibleCrops.set(key, sprite);
+ }
+ }
+ }
+
+ // Decoration Logic
+ if (this.tiles[y][x].hasDecoration) {
+ neededDecorKeys.add(key);
+
+ if (!this.visibleDecorations.has(key)) {
+ // Fast lookup from map
+ const decor = this.decorationsMap.get(key);
+
+ if (decor) {
+ const decorPos = this.iso.toScreen(x, y);
+ const sprite = this.decorationPool.get();
+ sprite.setTexture(decor.type);
+ sprite.setPosition(
+ decorPos.x + this.offsetX,
+ decorPos.y + this.offsetY + this.iso.tileHeight / 2
+ );
+
+ const depth = this.iso.getDepth(x, y);
+ if (decor.type === 'flower') sprite.setDepth(depth + 1);
+ else sprite.setDepth(depth + 1000); // Taller objects update depth
+
+ sprite.flipX = (x + y) % 2 === 0;
+ this.visibleDecorations.set(key, sprite);
+ }
+ }
+ }
+ }
+ }
+
+ // Cleanup invisible tiles
+ for (const [key, sprite] of this.visibleTiles) {
+ if (!neededKeys.has(key)) {
+ sprite.setVisible(false);
+ this.tilePool.release(sprite);
+ this.visibleTiles.delete(key);
+ }
+ }
+
+ // Cleanup invisible decorations
+ for (const [key, sprite] of this.visibleDecorations) {
+ if (!neededDecorKeys.has(key)) {
+ sprite.setVisible(false);
+ this.decorationPool.release(sprite);
+ this.visibleDecorations.delete(key);
+ }
+ }
+
+ // Cleanup visible crops
+ for (const [key, sprite] of this.visibleCrops) {
+ if (!neededCropKeys.has(key)) {
+ sprite.setVisible(false);
+ this.cropPool.release(sprite);
+ this.visibleCrops.delete(key);
+ }
+ }
+ }
+
+ // Helper functions
getTerrainType(value) {
for (const type of Object.values(this.terrainTypes)) {
if (value < type.threshold) {
@@ -56,71 +683,10 @@ class TerrainSystem {
return this.terrainTypes.STONE;
}
- // Renderaj teren (visual sprites)
- render(offsetX = 0, offsetY = 300) {
- console.log('🎨 Rendering terrain sprites...');
-
- const container = this.scene.add.container(offsetX, offsetY);
-
- // Renderaj vse tile-e
- for (let y = 0; y < this.height; y++) {
- for (let x = 0; x < this.width; x++) {
- const tile = this.tiles[y][x];
- const screenPos = this.iso.toScreen(x, y);
-
- // Kreira diamond (romb) obliko za isometric tile
- const graphics = this.scene.add.graphics();
-
- // Osnovna barva
- const baseColor = tile.color;
- graphics.fillStyle(baseColor, 1);
-
- // Nariši isometric tile (diamond shape)
- const tileWidth = this.iso.tileWidth;
- const tileHeight = this.iso.tileHeight;
-
- graphics.beginPath();
- graphics.moveTo(screenPos.x, screenPos.y); // Top
- graphics.lineTo(screenPos.x + tileWidth / 2, screenPos.y + tileHeight / 2); // Right
- graphics.lineTo(screenPos.x, screenPos.y + tileHeight); // Bottom
- graphics.lineTo(screenPos.x - tileWidth / 2, screenPos.y + tileHeight / 2); // Left
- graphics.closePath();
- graphics.fillPath();
-
- // Outline za boljšo vidljivost
- graphics.lineStyle(1, 0x000000, 0.2);
- graphics.strokePath();
-
- // Dodaj v container
- container.add(graphics);
-
- // Shrani referenco
- this.tileSprites.push({
- graphics: graphics,
- tile: tile,
- depth: this.iso.getDepth(x, y)
- });
- }
- }
-
- // Sortiraj po depth
- container.setDepth(0);
-
- console.log(`✅ Rendered ${this.tileSprites.length} tiles!`);
- return container;
- }
-
- // Pridobi tile na določenih grid koordinatah
getTile(gridX, gridY) {
if (gridX >= 0 && gridX < this.width && gridY >= 0 && gridY < this.height) {
return this.tiles[gridY][gridX];
}
return null;
}
-
- // Screen koordinate -> tile
- getTileAtScreen(screenX, screenY) {
- const grid = this.iso.toGrid(screenX, screenY);
- return this.getTile(grid.x, grid.y);
- }
}
diff --git a/src/systems/TimeSystem.js b/src/systems/TimeSystem.js
new file mode 100644
index 0000000..99a7282
--- /dev/null
+++ b/src/systems/TimeSystem.js
@@ -0,0 +1,107 @@
+class TimeSystem {
+ constructor(scene) {
+ this.scene = scene;
+
+ // Konfiguracija
+ this.fullDaySeconds = 300; // 5 minut za cel dan (real-time)
+ this.startTime = 8; // Začetek ob 8:00
+
+ // Stanje
+ this.gameTime = this.startTime; // 0 - 24
+ this.dayCount = 1;
+
+ // Lighting overlay
+ this.lightOverlay = null; // Ustvarjen bo v GameScene ali tu? Bolje tu če imamo dostop.
+ }
+
+ create() {
+ // Overlay za temo
+ // Uporabimo velik pravokotnik čez cel ekran, ki je fixiran na kamero
+ // Ampak ker imamo UIScene, je bolje da je overlay v GameScene, ampak nad vsem razen UI.
+ // Najlažje: canvas tinting ali graphics overlay.
+
+ // Za preprostost: Modificiramo ambient light ali tintamo igralca/teren?
+ // Phaser 3 ima setTint.
+ // Najboljši efekt za 2D: Temno moder rectangle z 'MULTIPLY' blend mode čez GameScene.
+
+ const width = this.scene.cameras.main.width * 2; // Malo večji za varnost
+ const height = this.scene.cameras.main.height * 2;
+
+ this.lightOverlay = this.scene.add.rectangle(0, 0, width, height, 0x000022);
+ this.lightOverlay.setScrollFactor(0);
+ this.lightOverlay.setDepth(9000); // Pod UI (UI je v drugi sceni), ampak nad igro
+ this.lightOverlay.setBlendMode(Phaser.BlendModes.MULTIPLY);
+ this.lightOverlay.setAlpha(0); // Začetek dan (0 alpha)
+ }
+
+ update(delta) {
+ // Povečaj čas
+ // delta je v ms.
+ // fullDaySeconds = 24 game hours.
+ // 1 game hour = fullDaySeconds / 24 seconds.
+ const seconds = delta / 1000;
+ const gameHoursPerRealSecond = 24 / this.fullDaySeconds;
+
+ this.gameTime += seconds * gameHoursPerRealSecond;
+
+ if (this.gameTime >= 24) {
+ this.gameTime -= 24;
+ this.dayCount++;
+ console.log(`🌞 Day ${this.dayCount} started!`);
+ }
+
+ this.updateLighting();
+ this.updateUI();
+ }
+
+ updateLighting() {
+ if (!this.lightOverlay) return;
+
+ // Izračunaj svetlobo (Alpha vrednost senc)
+ // 0 = Dan (Prozoren overlay)
+ // 0.8 = Polnoč (Temen overlay)
+
+ let alpha = 0;
+ const t = this.gameTime;
+
+ // Preprosta logika:
+ // 6:00 - 18:00 = Dan (0 alpha)
+ // 18:00 - 20:00 = Mrak (prehod 0 -> 0.7)
+ // 20:00 - 4:00 = Noč (0.7 alpha)
+ // 4:00 - 6:00 = Jutro (prehod 0.7 -> 0)
+
+ if (t >= 6 && t < 18) {
+ alpha = 0; // Dan
+ } else if (t >= 18 && t < 20) {
+ alpha = ((t - 18) / 2) * 0.7; // Mrak
+ } else if (t >= 20 || t < 4) {
+ alpha = 0.7; // Noč
+ } else if (t >= 4 && t < 6) {
+ alpha = 0.7 - ((t - 4) / 2) * 0.7; // Jutro
+ }
+
+ this.lightOverlay.setAlpha(alpha);
+ }
+
+ updateUI() {
+ const uiScene = this.scene.scene.get('UIScene');
+ if (uiScene && uiScene.clockText) {
+ const hours = Math.floor(this.gameTime);
+ const minutes = Math.floor((this.gameTime - hours) * 60);
+ const timeString = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
+ uiScene.clockText.setText(`Day ${this.dayCount} - ${timeString}`);
+ }
+ }
+
+ getCurrentHour() {
+ return Math.floor(this.gameTime);
+ }
+
+ getGameTime() {
+ return this.gameTime;
+ }
+
+ getDayCount() {
+ return this.dayCount;
+ }
+}
diff --git a/src/systems/WeatherSystem.js b/src/systems/WeatherSystem.js
new file mode 100644
index 0000000..f1febe8
--- /dev/null
+++ b/src/systems/WeatherSystem.js
@@ -0,0 +1,159 @@
+class WeatherSystem {
+ constructor(scene) {
+ this.scene = scene;
+ this.currentWeather = 'clear'; // clear, rain, storm, fog
+ this.weatherDuration = 0;
+ this.maxWeatherDuration = 30000; // 30s per weather cycle
+
+ // Weather effects containers
+ this.rainParticles = [];
+ this.overlay = null;
+
+ this.init();
+ }
+
+ init() {
+ // Create weather overlay
+ this.overlay = this.scene.add.graphics();
+ this.overlay.setDepth(5000); // Above everything but UI
+ this.overlay.setScrollFactor(0); // Fixed to camera
+
+ // Start with random weather
+ this.changeWeather();
+ }
+
+ changeWeather() {
+ // Clean up old weather
+ this.clearWeather();
+
+ // Random new weather
+ const weathers = ['clear', 'clear', 'rain', 'fog']; // Clear more common
+ this.currentWeather = weathers[Math.floor(Math.random() * weathers.length)];
+ this.weatherDuration = 0;
+
+ console.log(`🌦️ Weather changed to: ${this.currentWeather}`);
+
+ // Apply new weather effects
+ this.applyWeather();
+ }
+
+ applyWeather() {
+ if (this.currentWeather === 'rain') {
+ this.startRain();
+ // Play rain sound
+ if (this.scene.soundManager) {
+ this.scene.soundManager.playRainSound();
+ }
+ } else if (this.currentWeather === 'fog') {
+ this.applyFog();
+ } else if (this.currentWeather === 'storm') {
+ this.startRain(true);
+ // Play rain sound (louder?)
+ if (this.scene.soundManager) {
+ this.scene.soundManager.playRainSound();
+ }
+ } else {
+ // Clear weather - stop ambient sounds
+ if (this.scene.soundManager) {
+ this.scene.soundManager.stopRainSound();
+ }
+ }
+ }
+
+ startRain(heavy = false) {
+ const width = this.scene.cameras.main.width;
+ const height = this.scene.cameras.main.height;
+
+ // Create rain drops
+ const dropCount = heavy ? 150 : 100;
+
+ for (let i = 0; i < dropCount; i++) {
+ const drop = {
+ x: Math.random() * width,
+ y: Math.random() * height,
+ speed: heavy ? Phaser.Math.Between(400, 600) : Phaser.Math.Between(200, 400),
+ length: heavy ? Phaser.Math.Between(10, 15) : Phaser.Math.Between(5, 10)
+ };
+ this.rainParticles.push(drop);
+ }
+
+ // Darken screen slightly
+ this.overlay.clear();
+ this.overlay.fillStyle(0x000033, heavy ? 0.3 : 0.2);
+ this.overlay.fillRect(0, 0, width, height);
+ }
+
+ applyFog() {
+ const width = this.scene.cameras.main.width;
+ const height = this.scene.cameras.main.height;
+
+ this.overlay.clear();
+ this.overlay.fillStyle(0xcccccc, 0.4);
+ this.overlay.fillRect(0, 0, width, height);
+ }
+
+ clearWeather() {
+ // Remove all weather effects
+ this.rainParticles = [];
+ if (this.overlay) {
+ this.overlay.clear();
+ }
+ }
+
+ update(delta) {
+ this.weatherDuration += delta;
+
+ // Change weather periodically
+ if (this.weatherDuration > this.maxWeatherDuration) {
+ this.changeWeather();
+ }
+
+ // Update rain
+ if (this.currentWeather === 'rain' || this.currentWeather === 'storm') {
+ this.updateRain(delta);
+ }
+ }
+
+ updateRain(delta) {
+ const height = this.scene.cameras.main.height;
+ const width = this.scene.cameras.main.width;
+
+ // Update drop positions
+ for (const drop of this.rainParticles) {
+ drop.y += (drop.speed * delta) / 1000;
+
+ // Reset if off screen
+ if (drop.y > height) {
+ drop.y = -10;
+ drop.x = Math.random() * width;
+ }
+ }
+
+ // Render rain
+ this.overlay.clear();
+
+ // Background darkening
+ const isDark = this.currentWeather === 'storm' ? 0.3 : 0.2;
+ this.overlay.fillStyle(0x000033, isDark);
+ this.overlay.fillRect(0, 0, width, height);
+
+ // Draw drops
+ this.overlay.lineStyle(1, 0x88aaff, 0.5);
+ for (const drop of this.rainParticles) {
+ this.overlay.beginPath();
+ this.overlay.moveTo(drop.x, drop.y);
+ this.overlay.lineTo(drop.x - 2, drop.y + drop.length);
+ this.overlay.strokePath();
+ }
+ }
+
+ getCurrentWeather() {
+ return this.currentWeather;
+ }
+
+ setWeather(weather) {
+ this.currentWeather = weather;
+ this.weatherDuration = 0;
+ this.applyWeather();
+ }
+}
diff --git a/src/utils/ObjectPool.js b/src/utils/ObjectPool.js
new file mode 100644
index 0000000..374698e
--- /dev/null
+++ b/src/utils/ObjectPool.js
@@ -0,0 +1,61 @@
+class ObjectPool {
+ constructor(createFn, resetFn) {
+ this.createFn = createFn; // Funkcija za kreiranje novega objekta
+ this.resetFn = resetFn; // Funkcija za resetiranje objekta pred ponovno uporabo
+ this.active = []; // Aktivni objekti
+ this.inactive = []; // Neaktivni objekti (v poolu)
+ }
+
+ // Dobi objekt iz poola
+ get() {
+ let item;
+ if (this.inactive.length > 0) {
+ item = this.inactive.pop();
+ } else {
+ item = this.createFn();
+ }
+
+ // Resetiraj objekt (npr. nastavi visible = true)
+ if (this.resetFn) {
+ this.resetFn(item);
+ }
+
+ this.active.push(item);
+ return item;
+ }
+
+ // Vrni objekt v pool
+ release(item) {
+ const index = this.active.indexOf(item);
+ if (index > -1) {
+ this.active.splice(index, 1);
+ this.inactive.push(item);
+ }
+ }
+
+ // Vrni vse aktivne v pool
+ releaseAll() {
+ while (this.active.length > 0) {
+ const item = this.active.pop();
+ this.inactive.push(item);
+ }
+ }
+
+ // Počisti vse
+ clear() {
+ this.active = [];
+ this.inactive = [];
+ }
+
+ // Info
+ getStats() {
+ return {
+ active: this.active.length,
+ inactive: this.inactive.length,
+ total: this.active.length + this.inactive.length
+ };
+ }
+}
+
+// Ensure global access
+window.ObjectPool = ObjectPool;
diff --git a/src/utils/TextureGenerator.js b/src/utils/TextureGenerator.js
index 8eac0de..e1d0296 100644
--- a/src/utils/TextureGenerator.js
+++ b/src/utils/TextureGenerator.js
@@ -4,6 +4,7 @@ class TextureGenerator {
// Generiraj player sprite (32x32px pixel art)
static createPlayerSprite(scene, key = 'player') {
+ if (scene.textures.exists(key)) return;
const size = 32;
const canvas = scene.textures.createCanvas(key, size, size);
const ctx = canvas.getContext();
@@ -94,6 +95,7 @@ class TextureGenerator {
// Generiraj walking animacijo (4 frame-i)
static createPlayerWalkSprite(scene, key = 'player_walk') {
+ if (scene.textures.exists(key)) return;
const frameWidth = 32;
const frameHeight = 32;
const frameCount = 4;
@@ -177,9 +179,6 @@ class TextureGenerator {
if (frame === 2) legOffset = 1; // Right foot forward
for (let y = 11; y < 16; y++) {
- const leftShift = (frame === 1) ? 0 : 0;
- const rightShift = (frame === 2) ? 0 : 0;
-
// Leva noga
pixel(ox + 0, oy + y, outlineColor);
pixel(ox + 1, oy + y, pantsColor);
@@ -200,6 +199,7 @@ class TextureGenerator {
// Generiraj NPC sprite (32x32px pixel art)
static createNPCSprite(scene, key = 'npc', type = 'zombie') {
+ if (scene.textures.exists(key)) return;
const size = 32;
const canvas = scene.textures.createCanvas(key, size, size);
const ctx = canvas.getContext();
@@ -253,6 +253,19 @@ class TextureGenerator {
pixel(ox + 2, oy + 4, outlineColor);
pixel(ox + 5, oy + 4, outlineColor);
+ // Dreads (if Zombie)
+ if (type === 'zombie') {
+ const hairColor = '#3e2723'; // Dark Brown
+ // Top
+ for (let x = 1; x < 7; x++) pixel(ox + x, oy + 1, hairColor);
+ // Side Dreads
+ pixel(ox, oy + 2, hairColor); pixel(ox - 1, oy + 3, hairColor); pixel(ox - 1, oy + 4, hairColor);
+ pixel(ox + 7, oy + 2, hairColor); pixel(ox + 8, oy + 3, hairColor); pixel(ox + 8, oy + 4, hairColor);
+ // Back Dreads
+ pixel(ox, oy + 5, hairColor);
+ pixel(ox + 7, oy + 5, hairColor);
+ }
+
// Telo - srajca
for (let y = 6; y < 11; y++) {
pixel(ox + 0, oy + y, outlineColor);
@@ -290,4 +303,466 @@ class TextureGenerator {
canvas.refresh();
return canvas;
}
+
+ // Generiraj Flower sprite (16x16px)
+ static createFlowerSprite(scene, key = 'flower') {
+ if (scene.textures.exists(key)) return;
+ const size = 16;
+ const canvas = scene.textures.createCanvas(key, size, size);
+ const ctx = canvas.getContext();
+
+ ctx.clearRect(0, 0, size, size);
+
+ // Steblo
+ ctx.fillStyle = '#228B22';
+ ctx.fillRect(7, 8, 2, 8);
+ ctx.fillRect(5, 12, 2, 1); // List levo
+ ctx.fillRect(9, 10, 2, 1); // List desno
+
+ // Cvet (random barva vsakič ko kličemo? Ne, tekstura je statična, ampak lahko naredimo več variant)
+ // Za zdaj rdeča roža
+ ctx.fillStyle = '#FF0000';
+ ctx.fillRect(6, 4, 4, 4); // Center
+ ctx.fillStyle = '#FF69B4'; // Petals
+ ctx.fillRect(6, 2, 4, 2); // Top
+ ctx.fillRect(6, 8, 4, 2); // Bottom
+ ctx.fillRect(4, 4, 2, 4); // Left
+ ctx.fillRect(10, 4, 2, 4); // Right
+
+ canvas.refresh();
+ return canvas;
+ }
+
+ // Generiraj Bush sprite (32x32px)
+ static createBushSprite(scene, key = 'bush') {
+ if (scene.textures.exists(key)) return;
+ const size = 32;
+ const canvas = scene.textures.createCanvas(key, size, size);
+ const ctx = canvas.getContext();
+
+ ctx.clearRect(0, 0, size, size);
+
+ // Grm
+ ctx.fillStyle = '#006400'; // DarkGreen
+
+ // Risemo kroge/elipse pikslov za grm
+ // Base
+ ctx.fillRect(4, 16, 24, 14);
+ ctx.fillRect(2, 20, 28, 6);
+
+ // Highlights
+ ctx.fillStyle = '#228B22'; // ForestGreen
+ ctx.fillRect(6, 18, 10, 6);
+ ctx.fillRect(18, 14, 8, 8);
+
+ // Berries (rdeče pike)
+ ctx.fillStyle = '#FF0000';
+ ctx.fillRect(10, 20, 2, 2);
+ ctx.fillRect(20, 18, 2, 2);
+ ctx.fillRect(15, 24, 2, 2);
+
+ canvas.refresh();
+ return canvas;
+ }
+
+ // Generiraj Tree sprite (64x64px) - Blue Magical Tree
+ static createTreeSprite(scene, key = 'tree') {
+ if (scene.textures.exists(key)) return;
+ const width = 64;
+ const height = 64;
+ const canvas = scene.textures.createCanvas(key, width, height);
+ const ctx = canvas.getContext();
+
+ ctx.clearRect(0, 0, width, height);
+
+ // Trunk
+ ctx.fillStyle = '#8B4513'; // SaddleBrown
+ ctx.fillRect(28, 40, 8, 24); // Main trunk
+ ctx.fillRect(24, 58, 4, 6); // Root L
+ ctx.fillRect(36, 58, 4, 6); // Root R
+
+ // Branches
+ ctx.beginPath();
+ ctx.moveTo(32, 40);
+ ctx.lineTo(20, 30); // L
+ ctx.lineTo(24, 28);
+ ctx.lineTo(32, 35);
+ ctx.fill();
+
+ ctx.beginPath();
+ ctx.moveTo(32, 40);
+ ctx.lineTo(44, 30); // R
+ ctx.lineTo(40, 28);
+ ctx.lineTo(32, 35);
+ ctx.fill();
+
+ // Foliage (Blue/Teal/Cyan)
+ const cols = ['#008B8B', '#20B2AA', '#48D1CC', '#00CED1'];
+
+ const drawCluster = (cx, cy, r) => {
+ const col = cols[Math.floor(Math.random() * cols.length)];
+ ctx.fillStyle = col;
+ for (let y = -r; y <= r; y++) {
+ for (let x = -r; x <= r; x++) {
+ if (x * x + y * y <= r * r) {
+ ctx.fillRect(cx + x * 2, cy + y * 2, 2, 2);
+ }
+ }
+ }
+ };
+
+ // Main Canopy
+ drawCluster(32, 20, 10);
+ drawCluster(20, 25, 6);
+ drawCluster(44, 25, 6);
+ drawCluster(32, 10, 5);
+
+ // Magic sparkels
+ ctx.fillStyle = '#E0FFFF'; // LightCyan
+ for (let i = 0; i < 10; i++) {
+ ctx.fillRect(10 + Math.random() * 44, 5 + Math.random() * 30, 2, 2);
+ }
+
+ canvas.refresh();
+ return canvas;
+ }
+
+ // Generiraj Cloud sprite (64x32px)
+ static createCloudSprite(scene, key = 'cloud') {
+ if (scene.textures.exists(key)) return;
+ const width = 64;
+ const height = 32;
+ const canvas = scene.textures.createCanvas(key, width, height);
+ const ctx = canvas.getContext();
+
+ ctx.clearRect(0, 0, width, height);
+
+ ctx.fillStyle = '#FFFFFF';
+
+ // Simple pixel art cloud shape
+ // Three circles/blobs
+ ctx.fillRect(10, 10, 20, 15);
+ ctx.fillRect(25, 5, 20, 20);
+ ctx.fillRect(40, 10, 15, 12);
+
+ canvas.refresh();
+ return canvas;
+ }
+ // Generiraj Crop sprite (32x32px) - stages 1-4
+ static createCropSprite(scene, key, stage = 4) {
+ if (scene.textures.exists(key)) return;
+ const size = 32;
+ const canvas = scene.textures.createCanvas(key, size, size);
+ const ctx = canvas.getContext();
+
+ ctx.clearRect(0, 0, size, size);
+
+ const cx = 16;
+ const cy = 24; // Base position
+
+ if (stage === 1) {
+ // Seeds
+ ctx.fillStyle = '#D2B48C';
+ ctx.fillRect(cx - 2, cy, 2, 2);
+ ctx.fillRect(cx + 2, cy - 2, 2, 2);
+ ctx.fillRect(cx, cy + 2, 2, 2);
+ } else if (stage === 2) {
+ // Sprout
+ ctx.fillStyle = '#32CD32'; // LimeGreen
+ ctx.fillRect(cx - 1, cy, 2, 4); // Stem
+ ctx.fillRect(cx - 3, cy - 2, 2, 2); // Leaf left
+ ctx.fillRect(cx + 1, cy - 2, 2, 2); // Leaf right
+ } else if (stage === 3) {
+ // Growing
+ ctx.fillStyle = '#228B22'; // ForestGreen
+ ctx.fillRect(cx - 1, cy - 4, 3, 8); // Stem
+ ctx.fillRect(cx - 5, cy - 4, 4, 3); // Leaf L
+ ctx.fillRect(cx + 2, cy - 6, 4, 3); // Leaf R
+ } else if (stage === 4) {
+ // Ripe
+ ctx.fillStyle = '#006400'; // DarkGreen
+ ctx.fillRect(cx - 2, cy - 8, 4, 12); // Stem
+
+ // Leaves
+ ctx.fillStyle = '#228B22';
+ ctx.fillRect(cx - 6, cy - 2, 4, 4);
+ ctx.fillRect(cx + 2, cy - 4, 4, 4);
+
+ // Fruit (Corn/Wheat/Generic yellow/orange)
+ ctx.fillStyle = '#FFD700'; // Gold
+ ctx.fillRect(cx - 2, cy - 12, 4, 6);
+ }
+
+ canvas.refresh();
+ return canvas;
+ }
+ // Generiraj Structure sprite (Fence, Wall, House)
+ static createStructureSprite(scene, key, type) {
+ if (scene.textures.exists(key)) return;
+ const size = 32;
+ const width = (type === 'house' || type === 'ruin') ? 64 : 32;
+ const height = (type === 'house' || type === 'ruin') ? 64 : 32;
+
+ const canvas = scene.textures.createCanvas(key, width, height);
+ const ctx = canvas.getContext();
+
+ ctx.clearRect(0, 0, width, height);
+
+ if (type === 'fence') {
+ // Brown Fence
+ ctx.fillStyle = '#8B4513';
+ ctx.fillRect(8, 8, 4, 24); // Post L
+ ctx.fillRect(20, 8, 4, 24); // Post R
+ ctx.fillRect(8, 12, 16, 4); // Rail Top
+ ctx.fillRect(8, 20, 16, 4); // Rail Bot
+ } else if (type === 'wall') {
+ // Grey Wall
+ ctx.fillStyle = '#808080';
+ ctx.fillRect(0, 8, 32, 24);
+ ctx.fillStyle = '#696969'; // Bricks
+ ctx.fillRect(4, 12, 10, 6);
+ ctx.fillRect(18, 12, 10, 6);
+ ctx.fillRect(2, 22, 10, 6);
+ ctx.fillRect(16, 22, 10, 6);
+ } else if (type === 'house') {
+ // Isometric House
+ // Left Wall (Darker)
+ ctx.fillStyle = '#C2B280'; // Sand/Wheat dark
+ ctx.beginPath();
+ ctx.moveTo(32, 60); // Bottom Center
+ ctx.lineTo(10, 50); // Bottom Left corner
+ ctx.lineTo(10, 30); // Top Left corner
+ ctx.lineTo(32, 40); // Top Center (Roof start)
+ ctx.fill();
+
+ // Right Wall (Lighter)
+ ctx.fillStyle = '#F5DEB3'; // Wheat light
+ ctx.beginPath();
+ ctx.moveTo(32, 60); // Bottom Center
+ ctx.lineTo(54, 50); // Bottom Right corner
+ ctx.lineTo(54, 30); // Top Right corner
+ ctx.lineTo(32, 40); // Top Center
+ ctx.fill();
+
+ // Door (Right Wall)
+ ctx.fillStyle = '#8B4513';
+ ctx.beginPath();
+ ctx.moveTo(38, 56);
+ ctx.lineTo(48, 52);
+ ctx.lineTo(48, 38);
+ ctx.lineTo(38, 42);
+ ctx.fill();
+
+ // Roof (Left Slope)
+ ctx.fillStyle = '#8B0000'; // Dark Red
+ ctx.beginPath();
+ ctx.moveTo(32, 40);
+ ctx.lineTo(10, 30);
+ ctx.lineTo(32, 10); // Peak
+ ctx.lineTo(32, 40);
+ ctx.fill();
+
+ // Roof (Right Slope)
+ ctx.fillStyle = '#FF0000'; // Red
+ ctx.beginPath();
+ ctx.moveTo(32, 40);
+ ctx.lineTo(54, 30);
+ ctx.lineTo(32, 10); // Peak
+ ctx.lineTo(32, 40);
+ ctx.fill();
+
+ } else if (type === 'ruin') {
+ // Isometric Ruin
+
+ // Left Wall (Broken)
+ ctx.fillStyle = '#555555'; // Dark Grey
+ ctx.beginPath();
+ ctx.moveTo(32, 60);
+ ctx.lineTo(10, 50);
+ ctx.lineTo(10, 40); // Lower than house
+ ctx.lineTo(20, 45); // Jagged
+ ctx.lineTo(25, 38);
+ ctx.lineTo(32, 45);
+ ctx.fill();
+
+ // Right Wall (Broken)
+ ctx.fillStyle = '#777777'; // Light Grey
+ ctx.beginPath();
+ ctx.moveTo(32, 60);
+ ctx.lineTo(54, 50);
+ ctx.lineTo(54, 35);
+ ctx.lineTo(45, 30);
+ ctx.lineTo(40, 35);
+ ctx.lineTo(32, 25); // Exposed interior?
+ ctx.lineTo(32, 60);
+ ctx.fill();
+
+ // Debris piles
+ ctx.fillStyle = '#333333';
+ ctx.beginPath(); // Pile 1
+ ctx.arc(20, 55, 5, 0, Math.PI * 2);
+ ctx.fill();
+
+ ctx.beginPath(); // Pile 2
+ ctx.arc(45, 55, 4, 0, Math.PI * 2);
+ ctx.fill();
+
+ // Overgrowth
+ ctx.fillStyle = '#228B22';
+ ctx.fillRect(10, 48, 4, 8); // Vines Left
+ ctx.fillRect(50, 45, 5, 10); // Vines Right
+
+ // Random Bricks
+ ctx.fillStyle = '#444444';
+ ctx.fillRect(15, 60, 4, 2);
+ ctx.fillRect(35, 62, 3, 2);
+ }
+
+ canvas.refresh();
+ return canvas;
+ }
+
+ // ========== 2.5D VOLUMETRIC GENERATORS ==========
+
+ // Generiraj 3D volumetric tree (Minecraft-style)
+ static createTreeSprite(scene, key = 'tree') {
+ if (scene.textures.exists(key)) return;
+
+ const size = 64;
+ const canvas = scene.textures.createCanvas(key, size, size);
+ const ctx = canvas.getContext();
+
+ ctx.clearRect(0, 0, size, size);
+
+ // Tree trunk (3D block)
+ const trunkW = 12;
+ const trunkH = 24;
+ const trunkX = size / 2 - trunkW / 2;
+ const trunkY = size - trunkH - 8;
+
+ // Trunk - left side (darker)
+ ctx.fillStyle = '#8B6F47';
+ ctx.fillRect(trunkX, trunkY, trunkW / 2, trunkH);
+
+ // Trunk - right side (darkest)
+ ctx.fillStyle = '#654321';
+ ctx.fillRect(trunkX + trunkW / 2, trunkY, trunkW / 2, trunkH);
+
+ // Trunk - top (brightest)
+ ctx.fillStyle = '#A0826D';
+ ctx.fillRect(trunkX + 2, trunkY - 2, trunkW - 4, 2);
+
+ // Foliage (3D spherical)
+ const foliageX = size / 2;
+ const foliageY = trunkY - 8;
+ const radius = 20;
+
+ // Back shadow
+ ctx.fillStyle = '#228B22';
+ ctx.beginPath();
+ ctx.arc(foliageX - 2, foliageY + 2, radius, 0, Math.PI * 2);
+ ctx.fill();
+
+ // Main foliage
+ ctx.fillStyle = '#32CD32';
+ ctx.beginPath();
+ ctx.arc(foliageX, foliageY, radius, 0, Math.PI * 2);
+ ctx.fill();
+
+ // Highlight
+ ctx.fillStyle = '#90EE90';
+ ctx.beginPath();
+ ctx.arc(foliageX + 5, foliageY - 5, 8, 0, Math.PI * 2);
+ ctx.fill();
+
+ canvas.refresh();
+ return canvas;
+ }
+
+ // Generiraj 3D volumetric bush/rock (Minecraft-style)
+ static createBushSprite(scene, key = 'bush') {
+ if (scene.textures.exists(key)) return;
+
+ const size = 48;
+ const canvas = scene.textures.createCanvas(key, size, size);
+ const ctx = canvas.getContext();
+
+ ctx.clearRect(0, 0, size, size);
+
+ // Rock/Bush as 3D isometric block
+ const w = 24;
+ const h = 16;
+ const x = size / 2 - w / 2;
+ const y = size - h - 4;
+
+ // Left face (darker)
+ ctx.fillStyle = '#7d7d7d';
+ ctx.beginPath();
+ ctx.moveTo(x, y + h / 2);
+ ctx.lineTo(x + w / 2, y + h);
+ ctx.lineTo(x + w / 2, y);
+ ctx.lineTo(x, y + h / 2);
+ ctx.closePath();
+ ctx.fill();
+
+ // Right face (darkest)
+ ctx.fillStyle = '#5a5a5a';
+ ctx.beginPath();
+ ctx.moveTo(x + w / 2, y + h);
+ ctx.lineTo(x + w, y + h / 2);
+ ctx.lineTo(x + w, y - h / 2);
+ ctx.lineTo(x + w / 2, y);
+ ctx.closePath();
+ ctx.fill();
+
+ // Top face (brightest)
+ ctx.fillStyle = '#a0a0a0';
+ ctx.beginPath();
+ ctx.moveTo(x, y + h / 2);
+ ctx.lineTo(x + w / 2, y);
+ ctx.lineTo(x + w, y - h / 2);
+ ctx.lineTo(x + w / 2, y);
+ ctx.closePath();
+ ctx.fill();
+
+ // Black outline
+ ctx.strokeStyle = '#000000';
+ ctx.lineWidth = 1;
+ ctx.stroke();
+
+ canvas.refresh();
+ return canvas;
+ }
+
+ // Generiraj 3D flower (simple volumetric)
+ static createFlowerSprite(scene, key = 'flower') {
+ if (scene.textures.exists(key)) return;
+
+ const size = 32;
+ const canvas = scene.textures.createCanvas(key, size, size);
+ const ctx = canvas.getContext();
+
+ ctx.clearRect(0, 0, size, size);
+
+ // Stem
+ ctx.fillStyle = '#228B22';
+ ctx.fillRect(size / 2 - 1, size / 2, 2, size / 2 - 4);
+
+ // Flower petals (simple 2D for flowers)
+ const colors = ['#FF69B4', '#FFD700', '#FF4500', '#9370DB'];
+ const color = colors[Math.floor(Math.random() * colors.length)];
+
+ ctx.fillStyle = color;
+ ctx.beginPath();
+ ctx.arc(size / 2, size / 2 - 4, 6, 0, Math.PI * 2);
+ ctx.fill();
+
+ ctx.fillStyle = '#FFFF00';
+ ctx.beginPath();
+ ctx.arc(size / 2, size / 2 - 4, 3, 0, Math.PI * 2);
+ ctx.fill();
+
+ canvas.refresh();
+ return canvas;
+ }
}