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; + } }