FAZA 17: 2.5D Minecraft-Style Terrain + Y-Layer Stacking + Custom Sprites

COMPLETED FEATURES:

 Custom Sprite Integration:
- Player, Zombie, Merchant sprites (0.2 scale)
- 11 custom sprites + 5 asset packs loaded
- Auto-transparency processing (white/brown removal)
- Gravestone system with atlas extraction

 2.5D Minecraft-Style Terrain:
- Volumetric blocks with 25px thickness
- Strong left/right side shading (30%/50% darker)
- Minecraft-style texture patterns (grass, dirt, stone)
- Crisp black outlines for definition

 Y-Layer Stacking System:
- GRASS_FULL: All green (elevation > 0.7)
- GRASS_TOP: Green top + brown sides (elevation 0.4-0.7)
- DIRT: All brown (elevation < 0.4)
- Dynamic terrain depth based on height

 Floating Island World Edge:
- Stone cliff walls at map borders
- 2-tile transition zone
- Elevation flattening for cliff drop-off effect
- 100x100 world with defined boundaries

 Performance & Polish:
- Canvas renderer for pixel-perfect sharpness
- CSS image-rendering: crisp-edges
- willReadFrequently optimization
- No Canvas2D warnings

 Technical:
- 3D volumetric trees and rocks
- Hybrid rendering (2.5D terrain + 2D characters)
- Procedural texture generation
- Y-layer aware terrain type selection
This commit is contained in:
2025-12-07 01:44:16 +01:00
parent 34a2d07538
commit 9eb57ed117
60 changed files with 5082 additions and 195 deletions

64
FAZA_10_CHECKLIST.md Normal file
View File

@@ -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.

68
FAZA_11_CHECKLIST.md Normal file
View File

@@ -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).

54
FAZA_12_CHECKLIST.md Normal file
View File

@@ -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).

58
FAZA_13_CHECKLIST.md Normal file
View File

@@ -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).

62
FAZA_14_CHECKLIST.md Normal file
View File

@@ -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).

379
FAZA_15_CHECKLIST.md Normal file
View File

@@ -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

69
FAZA_16_CHECKLIST.md Normal file
View File

@@ -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)

121
FAZA_3_CHECKLIST.md Normal file
View File

@@ -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

68
FAZA_4_CHECKLIST.md Normal file
View File

@@ -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)

73
FAZA_5_CHECKLIST.md Normal file
View File

@@ -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

58
FAZA_6_CHECKLIST.md Normal file
View File

@@ -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.

71
FAZA_7_CHECKLIST.md Normal file
View File

@@ -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.

67
FAZA_8_CHECKLIST.md Normal file
View File

@@ -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).

70
FAZA_9_CHECKLIST.md Normal file
View File

@@ -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).

59
GDD.md Normal file
View File

@@ -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.*

BIN
assets/decoration_tree.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 894 KiB

BIN
assets/grass_sprite.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
assets/grass_tile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

BIN
assets/ground_tiles.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
assets/house_sprite.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

BIN
assets/leaf_sprite.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
assets/merchant_sprite.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 865 KiB

BIN
assets/npc_merchant.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 KiB

BIN
assets/npc_zombie.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 810 KiB

BIN
assets/objects_pack.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

BIN
assets/objects_pack2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

BIN
assets/player.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 KiB

BIN
assets/player_sprite.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

BIN
assets/stone_sprite.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
assets/stone_texture.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
assets/structure_house.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 556 KiB

BIN
assets/tree_sprite.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

BIN
assets/trees_vegetation.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

BIN
assets/walls_pack.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
assets/wheat_sprite.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

BIN
assets/zombie_sprite.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

View File

@@ -181,5 +181,5 @@ Format potrditve:
FAZA [N]: [STATUS] FAZA [N]: [STATUS]
- Testirano: [DA/NE] - Testirano: [DA/NE]
- Opombe: [opombe naročnika] - Opombe: [opombe naročnika]
- Odobreno: [DA/NE] - Odobreno: [DA/NE]a
``` ```

View File

@@ -4,6 +4,9 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Suppress Electron Security Warning for Dev -->
<meta http-equiv="Content-Security-Policy"
content="script-src 'self' 'unsafe-inline' 'unsafe-eval' blob: data:; object-src 'self';">
<title>NovaFarma - 2.5D Survival Game</title> <title>NovaFarma - 2.5D Survival Game</title>
<style> <style>
* { * {
@@ -24,12 +27,37 @@
align-items: center; align-items: center;
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
image-rendering: -moz-crisp-edges;
image-rendering: -webkit-crisp-edges;
image-rendering: pixelated;
image-rendering: crisp-edges;
}
canvas {
image-rendering: -moz-crisp-edges;
image-rendering: -webkit-crisp-edges;
image-rendering: pixelated;
image-rendering: crisp-edges;
} }
</style> </style>
</head> </head>
<body> <body>
<div id="game-container"></div> <div id="game-container"></div>
<div id="debug-console"
style="position: fixed; top: 0; left: 0; background: rgba(0,0,0,0.8); color: red; pointer-events: none; z-index: 9999; white-space: pre-wrap;">
</div>
<script>
window.onerror = function (msg, url, lineNo, columnNo, error) {
const container = document.getElementById('debug-console');
if (container) {
container.innerHTML += `ERROR: ${msg}\nAt: ${url}:${lineNo}:${columnNo}\n\n`;
}
console.error('Global Error:', msg, url, lineNo, error);
return false;
};
</script>
<!-- Phaser 3 --> <!-- Phaser 3 -->
<script src="node_modules/phaser/dist/phaser.js"></script> <script src="node_modules/phaser/dist/phaser.js"></script>
@@ -38,9 +66,21 @@
<script src="src/utils/PerlinNoise.js"></script> <script src="src/utils/PerlinNoise.js"></script>
<script src="src/utils/IsometricUtils.js"></script> <script src="src/utils/IsometricUtils.js"></script>
<script src="src/utils/TextureGenerator.js"></script> <script src="src/utils/TextureGenerator.js"></script>
<script src="src/utils/ObjectPool.js"></script>
<!-- Systems --> <!-- Systems -->
<script src="src/systems/TerrainSystem.js"></script> <script src="src/systems/TerrainSystem.js"></script>
<script src="src/systems/SaveSystem.js"></script>
<script src="src/systems/TimeSystem.js"></script>
<script src="src/systems/StatsSystem.js"></script>
<script src="src/systems/InventorySystem.js"></script>
<script src="src/systems/InteractionSystem.js"></script>
<script src="src/systems/FarmingSystem.js"></script>
<script src="src/systems/BuildingSystem.js"></script>
<script src="src/systems/WeatherSystem.js"></script>
<script src="src/systems/DayNightSystem.js"></script>
<script src="src/systems/SoundManager.js"></script>
<script src="src/systems/ParallaxSystem.js"></script>
<!-- Entities --> <!-- Entities -->
<script src="src/entities/Player.js"></script> <script src="src/entities/Player.js"></script>
@@ -49,6 +89,8 @@
<!-- Game Files --> <!-- Game Files -->
<script src="src/scenes/BootScene.js"></script> <script src="src/scenes/BootScene.js"></script>
<script src="src/scenes/PreloadScene.js"></script> <script src="src/scenes/PreloadScene.js"></script>
<script src="src/scenes/UIScene.js"></script>
<script src="src/scenes/StoryScene.js"></script>
<script src="src/scenes/GameScene.js"></script> <script src="src/scenes/GameScene.js"></script>
<script src="src/game.js"></script> <script src="src/game.js"></script>
</body> </body>

8
novafarma.code-workspace Normal file
View File

@@ -0,0 +1,8 @@
{
"folders": [
{
"path": "."
}
],
"settings": {}
}

View File

@@ -33,10 +33,14 @@ class NPC {
} }
createSprite() { createSprite() {
// Generiraj NPC teksturo glede na tip // Check for custom sprites first
const texKey = `npc_${this.type}`; 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); TextureGenerator.createNPCSprite(this.scene, texKey, this.type);
} }
@@ -47,7 +51,8 @@ class NPC {
screenPos.y + this.offsetY, screenPos.y + this.offsetY,
texKey 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 // Depth sorting
this.updateDepth(); this.updateDepth();

View File

@@ -31,23 +31,27 @@ class Player {
} }
createSprite() { createSprite() {
// Generiraj player teksturo (static sprite) // Use custom sprite if available, otherwise procedural
TextureGenerator.createPlayerSprite(this.scene, 'player'); 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 // Kreira sprite
const screenPos = this.iso.toScreen(this.gridX, this.gridY); const screenPos = this.iso.toScreen(this.gridX, this.gridY);
this.sprite = this.scene.add.sprite( this.sprite = this.scene.add.sprite(
screenPos.x + this.offsetX, screenPos.x + this.offsetX,
screenPos.y + this.offsetY, 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 // Depth sorting
this.updateDepth(); 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() { setupControls() {

View File

@@ -1,13 +1,21 @@
// Phaser Game Configuration // Phaser Game Configuration
const config = { const config = {
type: Phaser.AUTO, type: Phaser.CANVAS, // Canvas renderer za pixel-perfect ostrino
width: 1280, width: 640, // Pixel Art Viewport
height: 720, height: 360, // Pixel Art Viewport (16:9)
parent: 'game-container', parent: 'game-container',
backgroundColor: '#1a1a2e', backgroundColor: '#1a1a2e',
pixelArt: true, pixelArt: true,
antialias: false, antialias: false,
roundPixels: true, roundPixels: true,
render: {
pixelArt: true,
antialias: false,
roundPixels: true,
transparent: false,
clearBeforeRender: true,
powerPreference: 'high-performance'
},
physics: { physics: {
default: 'arcade', default: 'arcade',
arcade: { arcade: {
@@ -15,10 +23,13 @@ const config = {
debug: false debug: false
} }
}, },
scene: [BootScene, PreloadScene, GameScene], scene: [BootScene, PreloadScene, StoryScene, GameScene, UIScene],
scale: { scale: {
mode: Phaser.Scale.FIT, mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH autoCenter: Phaser.Scale.CENTER_BOTH
},
input: {
gamepad: true
} }
}; };

View File

@@ -18,21 +18,37 @@ class GameScene extends Phaser.Scene {
// Setup kamere // Setup kamere
this.cameras.main.setBackgroundColor('#1a1a2e'); this.cameras.main.setBackgroundColor('#1a1a2e');
// Initialize Isometric Utils
this.iso = new IsometricUtils();
// Inicializiraj terrain sistem - 100x100 mapa // Inicializiraj terrain sistem - 100x100 mapa
console.log('🌍 Initializing terrain...'); console.log('🌍 Initializing terrain...');
this.terrainSystem = new TerrainSystem(this, 100, 100); try {
this.terrainSystem.generate(); this.terrainSystem = new TerrainSystem(this, 100, 100);
this.terrainSystem.generate();
// Terrain offset // Terrain offset
this.terrainOffsetX = width / 2; this.terrainOffsetX = width / 2;
this.terrainOffsetY = 100; this.terrainOffsetY = 100;
this.terrainContainer = this.terrainSystem.render(this.terrainOffsetX, this.terrainOffsetY);
// 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...'); console.log('👤 Initializing player...');
this.player = new Player(this, 50, 50, this.terrainOffsetX, this.terrainOffsetY); this.player = new Player(this, 50, 50, this.terrainOffsetX, this.terrainOffsetY);
// Dodaj 3 NPCje - random pozicije // Dodaj 3 NPCje
console.log('🧟 Initializing NPCs...'); console.log('🧟 Initializing NPCs...');
const npcTypes = ['zombie', 'villager', 'merchant']; const npcTypes = ['zombie', 'villager', 'merchant'];
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
@@ -42,36 +58,59 @@ class GameScene extends Phaser.Scene {
this.npcs.push(npc); this.npcs.push(npc);
} }
// Kamera sledi igralcu // Kamera sledi igralcu z izboljšanimi nastavitvami
this.cameras.main.startFollow(this.player.sprite, true, 0.1, 0.1); 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 // Kamera kontrole
this.setupCamera(); this.setupCamera();
// UI elementi // Initialize Time & Stats
this.createUI(); console.log('⏳ Initializing Time & Stats...');
this.timeSystem = new TimeSystem(this);
this.timeSystem.create();
// Debug info this.statsSystem = new StatsSystem(this);
this.debugText = this.add.text(10, 10, '', { this.inventorySystem = new InventorySystem(this);
fontFamily: 'Courier New', this.interactionSystem = new InteractionSystem(this);
fontSize: '12px', this.farmingSystem = new FarmingSystem(this);
fill: '#ffffff', this.buildingSystem = new BuildingSystem(this);
backgroundColor: '#000000',
padding: { x: 5, y: 3 }
});
this.debugText.setScrollFactor(0);
this.debugText.setDepth(1000);
// FPS counter // Initialize Weather System
this.fpsText = this.add.text(10, height - 30, 'FPS: 60', { console.log('🌦️ Initializing Weather System...');
fontFamily: 'Courier New', this.weatherSystem = new WeatherSystem(this);
fontSize: '14px',
fill: '#00ff41'
});
this.fpsText.setScrollFactor(0);
this.fpsText.setDepth(1000);
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() { setupCamera() {
@@ -88,51 +127,81 @@ class GameScene extends Phaser.Scene {
cam.setZoom(newZoom); cam.setZoom(newZoom);
}); });
// Pan kontrole (Right click + drag) - DISABLED za FAZA 2
// Player movement sedaj uporablja WASD
// Q/E za zoom // Q/E za zoom
this.zoomKeys = this.input.keyboard.addKeys({ this.zoomKeys = this.input.keyboard.addKeys({
zoomIn: Phaser.Input.Keyboard.KeyCodes.Q, zoomIn: Phaser.Input.Keyboard.KeyCodes.Q,
zoomOut: Phaser.Input.Keyboard.KeyCodes.E zoomOut: Phaser.Input.Keyboard.KeyCodes.E
}); });
}
createUI() { // Save/Load Keys
const width = this.cameras.main.width; this.input.keyboard.on('keydown-F8', () => {
// Save
// Naslov if (this.saveSystem) {
const title = this.add.text(width / 2, 20, 'FAZA 3: NPC-ji in Dekoracije', { this.saveSystem.saveGame();
fontFamily: 'Courier New', console.log('💾 Game Saved! (F8)');
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'
} }
); });
controlsText.setOrigin(1, 0);
controlsText.setScrollFactor(0); this.input.keyboard.on('keydown-F9', () => {
controlsText.setDepth(1000); // 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(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 // Update player
if (this.player) { if (this.player) {
this.player.update(delta); this.player.update(delta);
@@ -143,32 +212,61 @@ class GameScene extends Phaser.Scene {
npc.update(delta); npc.update(delta);
} }
// Update FPS // Update Terrain Culling
if (this.fpsText) { if (this.terrainSystem) {
this.fpsText.setText(`FPS: ${Math.round(this.game.loop.actualFps)}`); this.terrainSystem.updateCulling(this.cameras.main);
} }
// Zoom controls // Update clouds
const cam = this.cameras.main; if (this.clouds) {
if (this.zoomKeys) { for (const cloud of this.clouds) {
if (this.zoomKeys.zoomIn.isDown) { cloud.sprite.x += cloud.speed * (delta / 1000);
cam.setZoom(Phaser.Math.Clamp(cam.zoom + 0.01, 0.3, 2.0)); if (cloud.sprite.x > this.terrainOffsetX + 2000) { // Reset far right
} cloud.sprite.x = this.terrainOffsetX - 2000;
if (this.zoomKeys.zoomOut.isDown) { cloud.sprite.y = Phaser.Math.Between(0, 1000);
cam.setZoom(Phaser.Math.Clamp(cam.zoom - 0.01, 0.3, 2.0)); }
} }
} }
// Debug info update // Send debug info to UI Scene
if (this.debugText && this.player) { if (this.player) {
const playerPos = this.player.getPosition(); const playerPos = this.player.getPosition();
const cam = this.cameras.main;
const visibleTiles = this.terrainSystem ? this.terrainSystem.visibleTiles.size : 0;
this.debugText.setText( const uiScene = this.scene.get('UIScene');
`FAZA 3 - NPCs & Decorations\n` + if (uiScene && uiScene.debugText) {
`Zoom: ${cam.zoom.toFixed(2)}\n` + const activeCrops = this.terrainSystem && this.terrainSystem.cropsMap ? this.terrainSystem.cropsMap.size : 0;
`Player: (${playerPos.x}, ${playerPos.y})\n` + const dropsCount = this.interactionSystem && this.interactionSystem.drops ? this.interactionSystem.drops.length : 0;
`NPCs: ${this.npcs.length}`
); 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) });
} }
} }
} }

View File

@@ -5,32 +5,125 @@ class PreloadScene extends Phaser.Scene {
} }
preload() { preload() {
console.log('📦 PreloadScene: Loading assets...'); console.log(' PreloadScene: Loading assets...');
// TODO: Tu bomo nalagali sprite-e, tile-e, audio, itd. // Load ALL custom sprites
// Za fazo 0 pustimo prazno - samo testiramo osnovni setup 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() { create() {
console.log('✅ PreloadScene: Assets loaded!'); console.log('✅ PreloadScene: Assets loaded!');
window.gameState.currentScene = 'PreloadScene'; window.gameState.currentScene = 'PreloadScene';
// Prikaz začetnega sporočila
const width = this.cameras.main.width; const width = this.cameras.main.width;
const height = this.cameras.main.height; 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', fontFamily: 'Courier New',
fontSize: '48px', fontSize: '48px',
fill: '#00ff41', fill: '#ff0000',
fontStyle: 'bold' fontStyle: 'bold',
stroke: '#000000',
strokeThickness: 6
}); });
title.setOrigin(0.5); 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', fontFamily: 'Courier New',
fontSize: '20px', fontSize: '24px',
fill: '#ffffff' fill: '#00ff41'
}); });
subtitle.setOrigin(0.5); subtitle.setOrigin(0.5);
@@ -41,7 +134,6 @@ class PreloadScene extends Phaser.Scene {
}); });
instruction.setOrigin(0.5); instruction.setOrigin(0.5);
// Blinking effect
this.tweens.add({ this.tweens.add({
targets: instruction, targets: instruction,
alpha: 0.3, alpha: 0.3,
@@ -50,10 +142,25 @@ class PreloadScene extends Phaser.Scene {
repeat: -1 repeat: -1
}); });
// Pritisk SPACE za začetek igre const startGame = () => {
this.input.keyboard.once('keydown-SPACE', () => { console.log('🎮 Starting StoryScene...');
console.log('🎮 Starting GameScene...'); this.input.keyboard.off('keydown');
this.scene.start('GameScene'); 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();
}); });
} }
} }

70
src/scenes/StoryScene.js Normal file
View File

@@ -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');
});
}
}

451
src/scenes/UIScene.js Normal file
View File

@@ -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);
}
}

View File

@@ -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() });
}
}

View File

@@ -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';
}
}

View File

@@ -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?
}
}
}
}
}

View File

@@ -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);
}
}
}
}

View File

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

View File

@@ -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 = [];
}
}

246
src/systems/SaveSystem.js Normal file
View File

@@ -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();
}
});
}
}
}

148
src/systems/SoundManager.js Normal file
View File

@@ -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(); }
}

135
src/systems/StatsSystem.js Normal file
View File

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

View File

@@ -1,5 +1,5 @@
// Terrain Generator System // Terrain Generator System
// Generira proceduralni isometrični teren // Generira proceduralni isometrični teren in skrbi za optimizacijo (Culling, Object Pooling)
class TerrainSystem { class TerrainSystem {
constructor(scene, width = 100, height = 100) { constructor(scene, width = 100, height = 100) {
this.scene = scene; this.scene = scene;
@@ -10,43 +10,670 @@ class TerrainSystem {
this.noise = new PerlinNoise(Date.now()); this.noise = new PerlinNoise(Date.now());
this.tiles = []; 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 = { this.terrainTypes = {
WATER: { threshold: 0.3, color: 0x2166aa, name: 'water' }, WATER: { threshold: 0.3, color: 0x2166aa, name: 'water', texture: 'tile_water', yLayer: -1 },
SAND: { threshold: 0.4, color: 0xf4e7c6, name: 'sand' }, SAND: { threshold: 0.4, color: 0xf4e7c6, name: 'sand', texture: 'tile_sand', yLayer: 0 },
GRASS: { threshold: 0.65, color: 0x5cb85c, name: 'grass' },
DIRT: { threshold: 0.75, color: 0x8b6f47, name: 'dirt' }, // Y-LAYER GRASS VARIANTS (A, B, C systém)
STONE: { threshold: 1.0, color: 0x7d7d7d, name: 'stone' } 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() { 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 // Generiraj tile podatke
for (let y = 0; y < this.height; y++) { for (let y = 0; y < this.height; y++) {
this.tiles[y] = []; this.tiles[y] = [];
for (let x = 0; x < this.width; x++) { for (let x = 0; x < this.width; x++) {
const noiseValue = this.noise.getNormalized(x, y, 0.05, 4); 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] = { this.tiles[y][x] = {
gridX: x, gridX: x,
gridY: y, gridY: y,
type: terrainType.name, type: terrainType.name,
color: terrainType.color, texture: terrainType.texture,
height: noiseValue 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) { getTerrainType(value) {
for (const type of Object.values(this.terrainTypes)) { for (const type of Object.values(this.terrainTypes)) {
if (value < type.threshold) { if (value < type.threshold) {
@@ -56,71 +683,10 @@ class TerrainSystem {
return this.terrainTypes.STONE; 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) { getTile(gridX, gridY) {
if (gridX >= 0 && gridX < this.width && gridY >= 0 && gridY < this.height) { if (gridX >= 0 && gridX < this.width && gridY >= 0 && gridY < this.height) {
return this.tiles[gridY][gridX]; return this.tiles[gridY][gridX];
} }
return null; return null;
} }
// Screen koordinate -> tile
getTileAtScreen(screenX, screenY) {
const grid = this.iso.toGrid(screenX, screenY);
return this.getTile(grid.x, grid.y);
}
} }

107
src/systems/TimeSystem.js Normal file
View File

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

View File

@@ -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();
}
}

61
src/utils/ObjectPool.js Normal file
View File

@@ -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;

View File

@@ -4,6 +4,7 @@ class TextureGenerator {
// Generiraj player sprite (32x32px pixel art) // Generiraj player sprite (32x32px pixel art)
static createPlayerSprite(scene, key = 'player') { static createPlayerSprite(scene, key = 'player') {
if (scene.textures.exists(key)) return;
const size = 32; const size = 32;
const canvas = scene.textures.createCanvas(key, size, size); const canvas = scene.textures.createCanvas(key, size, size);
const ctx = canvas.getContext(); const ctx = canvas.getContext();
@@ -94,6 +95,7 @@ class TextureGenerator {
// Generiraj walking animacijo (4 frame-i) // Generiraj walking animacijo (4 frame-i)
static createPlayerWalkSprite(scene, key = 'player_walk') { static createPlayerWalkSprite(scene, key = 'player_walk') {
if (scene.textures.exists(key)) return;
const frameWidth = 32; const frameWidth = 32;
const frameHeight = 32; const frameHeight = 32;
const frameCount = 4; const frameCount = 4;
@@ -177,9 +179,6 @@ class TextureGenerator {
if (frame === 2) legOffset = 1; // Right foot forward if (frame === 2) legOffset = 1; // Right foot forward
for (let y = 11; y < 16; y++) { for (let y = 11; y < 16; y++) {
const leftShift = (frame === 1) ? 0 : 0;
const rightShift = (frame === 2) ? 0 : 0;
// Leva noga // Leva noga
pixel(ox + 0, oy + y, outlineColor); pixel(ox + 0, oy + y, outlineColor);
pixel(ox + 1, oy + y, pantsColor); pixel(ox + 1, oy + y, pantsColor);
@@ -200,6 +199,7 @@ class TextureGenerator {
// Generiraj NPC sprite (32x32px pixel art) // Generiraj NPC sprite (32x32px pixel art)
static createNPCSprite(scene, key = 'npc', type = 'zombie') { static createNPCSprite(scene, key = 'npc', type = 'zombie') {
if (scene.textures.exists(key)) return;
const size = 32; const size = 32;
const canvas = scene.textures.createCanvas(key, size, size); const canvas = scene.textures.createCanvas(key, size, size);
const ctx = canvas.getContext(); const ctx = canvas.getContext();
@@ -253,6 +253,19 @@ class TextureGenerator {
pixel(ox + 2, oy + 4, outlineColor); pixel(ox + 2, oy + 4, outlineColor);
pixel(ox + 5, 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 // Telo - srajca
for (let y = 6; y < 11; y++) { for (let y = 6; y < 11; y++) {
pixel(ox + 0, oy + y, outlineColor); pixel(ox + 0, oy + y, outlineColor);
@@ -290,4 +303,466 @@ class TextureGenerator {
canvas.refresh(); canvas.refresh();
return canvas; 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;
}
} }