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
64
FAZA_10_CHECKLIST.md
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
After Width: | Height: | Size: 894 KiB |
BIN
assets/grass_sprite.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
assets/grass_tile.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
assets/ground_tiles.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
assets/house_sprite.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
assets/leaf_sprite.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
assets/merchant_sprite.png
Normal file
|
After Width: | Height: | Size: 865 KiB |
BIN
assets/npc_merchant.png
Normal file
|
After Width: | Height: | Size: 492 KiB |
BIN
assets/npc_zombie.png
Normal file
|
After Width: | Height: | Size: 810 KiB |
BIN
assets/objects_pack.png
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
assets/objects_pack2.png
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
assets/player.png
Normal file
|
After Width: | Height: | Size: 572 KiB |
BIN
assets/player_sprite.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
assets/stone_sprite.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
assets/stone_texture.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
assets/structure_house.png
Normal file
|
After Width: | Height: | Size: 556 KiB |
BIN
assets/tree_sprite.png
Normal file
|
After Width: | Height: | Size: 150 KiB |
BIN
assets/trees_vegetation.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
assets/walls_pack.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
assets/wheat_sprite.png
Normal file
|
After Width: | Height: | Size: 110 KiB |
BIN
assets/zombie_sprite.png
Normal file
|
After Width: | Height: | Size: 111 KiB |
@@ -181,5 +181,5 @@ Format potrditve:
|
||||
FAZA [N]: [STATUS]
|
||||
- Testirano: [DA/NE]
|
||||
- Opombe: [opombe naročnika]
|
||||
- Odobreno: [DA/NE]
|
||||
- Odobreno: [DA/NE]a
|
||||
```
|
||||
|
||||
42
index.html
@@ -4,6 +4,9 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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>
|
||||
<style>
|
||||
* {
|
||||
@@ -24,12 +27,37 @@
|
||||
align-items: center;
|
||||
width: 100vw;
|
||||
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>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<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 -->
|
||||
<script src="node_modules/phaser/dist/phaser.js"></script>
|
||||
@@ -38,9 +66,21 @@
|
||||
<script src="src/utils/PerlinNoise.js"></script>
|
||||
<script src="src/utils/IsometricUtils.js"></script>
|
||||
<script src="src/utils/TextureGenerator.js"></script>
|
||||
<script src="src/utils/ObjectPool.js"></script>
|
||||
|
||||
<!-- Systems -->
|
||||
<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 -->
|
||||
<script src="src/entities/Player.js"></script>
|
||||
@@ -49,6 +89,8 @@
|
||||
<!-- Game Files -->
|
||||
<script src="src/scenes/BootScene.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/game.js"></script>
|
||||
</body>
|
||||
|
||||
8
novafarma.code-workspace
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
||||
@@ -33,10 +33,14 @@ class NPC {
|
||||
}
|
||||
|
||||
createSprite() {
|
||||
// Generiraj NPC teksturo glede na tip
|
||||
const texKey = `npc_${this.type}`;
|
||||
// Check for custom sprites first
|
||||
let texKey = `npc_${this.type}`;
|
||||
|
||||
if (!this.scene.textures.exists(texKey)) {
|
||||
if (this.type === 'zombie' && this.scene.textures.exists('zombie_sprite')) {
|
||||
texKey = 'zombie_sprite';
|
||||
} else if (this.type === 'merchant' && this.scene.textures.exists('merchant_sprite')) {
|
||||
texKey = 'merchant_sprite';
|
||||
} else if (!this.scene.textures.exists(texKey)) {
|
||||
TextureGenerator.createNPCSprite(this.scene, texKey, this.type);
|
||||
}
|
||||
|
||||
@@ -47,7 +51,8 @@ class NPC {
|
||||
screenPos.y + this.offsetY,
|
||||
texKey
|
||||
);
|
||||
this.sprite.setOrigin(0.5, 1); // Anchor na dnu sprite-a
|
||||
this.sprite.setOrigin(0.5, 1);
|
||||
this.sprite.setScale(0.2); // Mali, detajlni sprite
|
||||
|
||||
// Depth sorting
|
||||
this.updateDepth();
|
||||
|
||||
@@ -31,23 +31,27 @@ class Player {
|
||||
}
|
||||
|
||||
createSprite() {
|
||||
// Generiraj player teksturo (static sprite)
|
||||
TextureGenerator.createPlayerSprite(this.scene, 'player');
|
||||
// Use custom sprite if available, otherwise procedural
|
||||
let texKey = 'player';
|
||||
|
||||
if (this.scene.textures.exists('player_sprite')) {
|
||||
texKey = 'player_sprite';
|
||||
} else if (!this.scene.textures.exists(texKey)) {
|
||||
TextureGenerator.createPlayerSprite(this.scene, texKey);
|
||||
}
|
||||
|
||||
// Kreira sprite
|
||||
const screenPos = this.iso.toScreen(this.gridX, this.gridY);
|
||||
this.sprite = this.scene.add.sprite(
|
||||
screenPos.x + this.offsetX,
|
||||
screenPos.y + this.offsetY,
|
||||
'player'
|
||||
texKey
|
||||
);
|
||||
this.sprite.setOrigin(0.5, 1); // Anchor na dnu sprite-a
|
||||
this.sprite.setOrigin(0.5, 1);
|
||||
this.sprite.setScale(0.2); // Mali, detajlni sprite
|
||||
|
||||
// Depth sorting
|
||||
this.updateDepth();
|
||||
|
||||
// Walking animacija je onemogočena za FAZA 2 (fix za canvas texture issue)
|
||||
// TODO: Dodaj proper sprite sheet animacijo v FAZA 3
|
||||
}
|
||||
|
||||
setupControls() {
|
||||
|
||||
19
src/game.js
@@ -1,13 +1,21 @@
|
||||
// Phaser Game Configuration
|
||||
const config = {
|
||||
type: Phaser.AUTO,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
type: Phaser.CANVAS, // Canvas renderer za pixel-perfect ostrino
|
||||
width: 640, // Pixel Art Viewport
|
||||
height: 360, // Pixel Art Viewport (16:9)
|
||||
parent: 'game-container',
|
||||
backgroundColor: '#1a1a2e',
|
||||
pixelArt: true,
|
||||
antialias: false,
|
||||
roundPixels: true,
|
||||
render: {
|
||||
pixelArt: true,
|
||||
antialias: false,
|
||||
roundPixels: true,
|
||||
transparent: false,
|
||||
clearBeforeRender: true,
|
||||
powerPreference: 'high-performance'
|
||||
},
|
||||
physics: {
|
||||
default: 'arcade',
|
||||
arcade: {
|
||||
@@ -15,10 +23,13 @@ const config = {
|
||||
debug: false
|
||||
}
|
||||
},
|
||||
scene: [BootScene, PreloadScene, GameScene],
|
||||
scene: [BootScene, PreloadScene, StoryScene, GameScene, UIScene],
|
||||
scale: {
|
||||
mode: Phaser.Scale.FIT,
|
||||
autoCenter: Phaser.Scale.CENTER_BOTH
|
||||
},
|
||||
input: {
|
||||
gamepad: true
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -18,21 +18,37 @@ class GameScene extends Phaser.Scene {
|
||||
// Setup kamere
|
||||
this.cameras.main.setBackgroundColor('#1a1a2e');
|
||||
|
||||
// Initialize Isometric Utils
|
||||
this.iso = new IsometricUtils();
|
||||
|
||||
// Inicializiraj terrain sistem - 100x100 mapa
|
||||
console.log('🌍 Initializing terrain...');
|
||||
this.terrainSystem = new TerrainSystem(this, 100, 100);
|
||||
this.terrainSystem.generate();
|
||||
try {
|
||||
this.terrainSystem = new TerrainSystem(this, 100, 100);
|
||||
this.terrainSystem.generate();
|
||||
|
||||
// Terrain offset
|
||||
this.terrainOffsetX = width / 2;
|
||||
this.terrainOffsetY = 100;
|
||||
this.terrainContainer = this.terrainSystem.render(this.terrainOffsetX, this.terrainOffsetY);
|
||||
// Terrain offset
|
||||
this.terrainOffsetX = width / 2;
|
||||
this.terrainOffsetY = 100;
|
||||
|
||||
// Dodaj igralca - spawn na sredini mape S TERRAIN OFFSETOM
|
||||
// Initialization for culling
|
||||
this.terrainSystem.init(this.terrainOffsetX, this.terrainOffsetY);
|
||||
|
||||
// Initial force update to render active tiles before first frame
|
||||
this.terrainSystem.updateCulling(this.cameras.main);
|
||||
|
||||
// FAZA 14: Spawn Ruin (Town Project) at fixed location near player
|
||||
console.log('🏚️ Spawning Ruin...');
|
||||
this.terrainSystem.placeStructure(55, 55, 'ruin');
|
||||
} catch (e) {
|
||||
console.error("Terrain system failed:", e);
|
||||
}
|
||||
|
||||
// Dodaj igralca
|
||||
console.log('👤 Initializing player...');
|
||||
this.player = new Player(this, 50, 50, this.terrainOffsetX, this.terrainOffsetY);
|
||||
|
||||
// Dodaj 3 NPCje - random pozicije
|
||||
// Dodaj 3 NPCje
|
||||
console.log('🧟 Initializing NPCs...');
|
||||
const npcTypes = ['zombie', 'villager', 'merchant'];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
@@ -42,36 +58,59 @@ class GameScene extends Phaser.Scene {
|
||||
this.npcs.push(npc);
|
||||
}
|
||||
|
||||
// Kamera sledi igralcu
|
||||
this.cameras.main.startFollow(this.player.sprite, true, 0.1, 0.1);
|
||||
// Kamera sledi igralcu z izboljšanimi nastavitvami
|
||||
this.cameras.main.startFollow(this.player.sprite, true, 1.0, 1.0); // Instant follow (was 0.1)
|
||||
|
||||
// Nastavi deadzone (100px border)
|
||||
this.cameras.main.setDeadzone(100, 100);
|
||||
|
||||
// Round pixels za crisp pixel art
|
||||
this.cameras.main.roundPixels = true;
|
||||
|
||||
// Parallax oblaki
|
||||
this.createClouds();
|
||||
|
||||
// Kamera kontrole
|
||||
this.setupCamera();
|
||||
|
||||
// UI elementi
|
||||
this.createUI();
|
||||
// Initialize Time & Stats
|
||||
console.log('⏳ Initializing Time & Stats...');
|
||||
this.timeSystem = new TimeSystem(this);
|
||||
this.timeSystem.create();
|
||||
|
||||
// Debug info
|
||||
this.debugText = this.add.text(10, 10, '', {
|
||||
fontFamily: 'Courier New',
|
||||
fontSize: '12px',
|
||||
fill: '#ffffff',
|
||||
backgroundColor: '#000000',
|
||||
padding: { x: 5, y: 3 }
|
||||
});
|
||||
this.debugText.setScrollFactor(0);
|
||||
this.debugText.setDepth(1000);
|
||||
this.statsSystem = new StatsSystem(this);
|
||||
this.inventorySystem = new InventorySystem(this);
|
||||
this.interactionSystem = new InteractionSystem(this);
|
||||
this.farmingSystem = new FarmingSystem(this);
|
||||
this.buildingSystem = new BuildingSystem(this);
|
||||
|
||||
// FPS counter
|
||||
this.fpsText = this.add.text(10, height - 30, 'FPS: 60', {
|
||||
fontFamily: 'Courier New',
|
||||
fontSize: '14px',
|
||||
fill: '#00ff41'
|
||||
});
|
||||
this.fpsText.setScrollFactor(0);
|
||||
this.fpsText.setDepth(1000);
|
||||
// Initialize Weather System
|
||||
console.log('🌦️ Initializing Weather System...');
|
||||
this.weatherSystem = new WeatherSystem(this);
|
||||
|
||||
console.log('✅ GameScene ready - FAZA 3!');
|
||||
// Initialize Day/Night Cycle
|
||||
console.log('🌅 Initializing Day/Night System...');
|
||||
this.dayNightSystem = new DayNightSystem(this, this.timeSystem);
|
||||
|
||||
// Initialize Sound Manager
|
||||
console.log('🎵 Initializing Sound Manager...');
|
||||
this.soundManager = new SoundManager(this);
|
||||
|
||||
// Initialize Parallax System
|
||||
console.log('🌄 Initializing Parallax System...');
|
||||
this.parallaxSystem = new ParallaxSystem(this);
|
||||
|
||||
// Launch UI Scene
|
||||
console.log('🖥️ Launching UI Scene...');
|
||||
this.scene.launch('UIScene');
|
||||
|
||||
// Initialize Save System
|
||||
this.saveSystem = new SaveSystem(this);
|
||||
|
||||
// Auto-load if available (optional, for now manual)
|
||||
// this.saveSystem.loadGame();
|
||||
|
||||
console.log('✅ GameScene ready - FAZA 17!');
|
||||
}
|
||||
|
||||
setupCamera() {
|
||||
@@ -88,51 +127,81 @@ class GameScene extends Phaser.Scene {
|
||||
cam.setZoom(newZoom);
|
||||
});
|
||||
|
||||
// Pan kontrole (Right click + drag) - DISABLED za FAZA 2
|
||||
// Player movement sedaj uporablja WASD
|
||||
|
||||
// Q/E za zoom
|
||||
this.zoomKeys = this.input.keyboard.addKeys({
|
||||
zoomIn: Phaser.Input.Keyboard.KeyCodes.Q,
|
||||
zoomOut: Phaser.Input.Keyboard.KeyCodes.E
|
||||
});
|
||||
}
|
||||
|
||||
createUI() {
|
||||
const width = this.cameras.main.width;
|
||||
|
||||
// Naslov
|
||||
const title = this.add.text(width / 2, 20, 'FAZA 3: NPC-ji in Dekoracije', {
|
||||
fontFamily: 'Courier New',
|
||||
fontSize: '20px',
|
||||
fill: '#00ff41',
|
||||
fontStyle: 'bold'
|
||||
});
|
||||
title.setOrigin(0.5, 0);
|
||||
title.setScrollFactor(0);
|
||||
title.setDepth(1000);
|
||||
|
||||
// Kontrole info
|
||||
const controlsText = this.add.text(width - 10, 10,
|
||||
'Kontrole:\n' +
|
||||
'WASD - Gibanje igralca\n' +
|
||||
'Q/E - Zoom\n' +
|
||||
'Mouse Wheel - Zoom',
|
||||
{
|
||||
fontFamily: 'Courier New',
|
||||
fontSize: '11px',
|
||||
fill: '#888888',
|
||||
backgroundColor: '#000000',
|
||||
padding: { x: 5, y: 3 },
|
||||
align: 'right'
|
||||
// Save/Load Keys
|
||||
this.input.keyboard.on('keydown-F8', () => {
|
||||
// Save
|
||||
if (this.saveSystem) {
|
||||
this.saveSystem.saveGame();
|
||||
console.log('💾 Game Saved! (F8)');
|
||||
}
|
||||
);
|
||||
controlsText.setOrigin(1, 0);
|
||||
controlsText.setScrollFactor(0);
|
||||
controlsText.setDepth(1000);
|
||||
});
|
||||
|
||||
this.input.keyboard.on('keydown-F9', () => {
|
||||
// Load
|
||||
if (this.saveSystem) {
|
||||
this.saveSystem.loadGame();
|
||||
console.log('📂 Game Loaded! (F9)');
|
||||
}
|
||||
});
|
||||
|
||||
// Build Mode Keys
|
||||
this.input.keyboard.on('keydown-B', () => {
|
||||
if (this.buildingSystem) this.buildingSystem.toggleBuildMode();
|
||||
});
|
||||
|
||||
this.input.keyboard.on('keydown-ONE', () => {
|
||||
if (this.buildingSystem && this.buildingSystem.isBuildMode) this.buildingSystem.selectBuilding('fence');
|
||||
else if (this.scene.get('UIScene')) this.scene.get('UIScene').selectSlot(0);
|
||||
});
|
||||
this.input.keyboard.on('keydown-TWO', () => {
|
||||
if (this.buildingSystem && this.buildingSystem.isBuildMode) this.buildingSystem.selectBuilding('wall');
|
||||
else if (this.scene.get('UIScene')) this.scene.get('UIScene').selectSlot(1);
|
||||
});
|
||||
this.input.keyboard.on('keydown-THREE', () => {
|
||||
if (this.buildingSystem && this.buildingSystem.isBuildMode) this.buildingSystem.selectBuilding('house');
|
||||
else if (this.scene.get('UIScene')) this.scene.get('UIScene').selectSlot(2);
|
||||
});
|
||||
|
||||
// Soft Reset (F4) - Force Reload Page
|
||||
this.input.keyboard.on('keydown-F4', () => {
|
||||
console.log('🔄 Soft Reset Initiated (Force Reload)...');
|
||||
window.location.reload();
|
||||
});
|
||||
|
||||
// Mute Toggle (M key)
|
||||
this.input.keyboard.on('keydown-M', () => {
|
||||
if (this.soundManager) {
|
||||
this.soundManager.toggleMute();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
update(time, delta) {
|
||||
// Update Systems
|
||||
if (this.timeSystem) this.timeSystem.update(delta);
|
||||
if (this.statsSystem) this.statsSystem.update(delta);
|
||||
if (this.interactionSystem) this.interactionSystem.update(delta);
|
||||
if (this.farmingSystem) this.farmingSystem.update(delta);
|
||||
if (this.weatherSystem) this.weatherSystem.update(delta);
|
||||
if (this.dayNightSystem) this.dayNightSystem.update();
|
||||
|
||||
// Update Parallax (foreground grass fading)
|
||||
if (this.parallaxSystem && this.player) {
|
||||
const playerPos = this.player.getPosition();
|
||||
const screenPos = this.iso.toScreen(playerPos.x, playerPos.y);
|
||||
this.parallaxSystem.update(
|
||||
screenPos.x + this.terrainOffsetX,
|
||||
screenPos.y + this.terrainOffsetY
|
||||
);
|
||||
}
|
||||
|
||||
// Update player
|
||||
if (this.player) {
|
||||
this.player.update(delta);
|
||||
@@ -143,32 +212,61 @@ class GameScene extends Phaser.Scene {
|
||||
npc.update(delta);
|
||||
}
|
||||
|
||||
// Update FPS
|
||||
if (this.fpsText) {
|
||||
this.fpsText.setText(`FPS: ${Math.round(this.game.loop.actualFps)}`);
|
||||
// Update Terrain Culling
|
||||
if (this.terrainSystem) {
|
||||
this.terrainSystem.updateCulling(this.cameras.main);
|
||||
}
|
||||
|
||||
// Zoom controls
|
||||
const cam = this.cameras.main;
|
||||
if (this.zoomKeys) {
|
||||
if (this.zoomKeys.zoomIn.isDown) {
|
||||
cam.setZoom(Phaser.Math.Clamp(cam.zoom + 0.01, 0.3, 2.0));
|
||||
}
|
||||
if (this.zoomKeys.zoomOut.isDown) {
|
||||
cam.setZoom(Phaser.Math.Clamp(cam.zoom - 0.01, 0.3, 2.0));
|
||||
// Update clouds
|
||||
if (this.clouds) {
|
||||
for (const cloud of this.clouds) {
|
||||
cloud.sprite.x += cloud.speed * (delta / 1000);
|
||||
if (cloud.sprite.x > this.terrainOffsetX + 2000) { // Reset far right
|
||||
cloud.sprite.x = this.terrainOffsetX - 2000;
|
||||
cloud.sprite.y = Phaser.Math.Between(0, 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Debug info update
|
||||
if (this.debugText && this.player) {
|
||||
// Send debug info to UI Scene
|
||||
if (this.player) {
|
||||
const playerPos = this.player.getPosition();
|
||||
const cam = this.cameras.main;
|
||||
const visibleTiles = this.terrainSystem ? this.terrainSystem.visibleTiles.size : 0;
|
||||
|
||||
this.debugText.setText(
|
||||
`FAZA 3 - NPCs & Decorations\n` +
|
||||
`Zoom: ${cam.zoom.toFixed(2)}\n` +
|
||||
`Player: (${playerPos.x}, ${playerPos.y})\n` +
|
||||
`NPCs: ${this.npcs.length}`
|
||||
);
|
||||
const uiScene = this.scene.get('UIScene');
|
||||
if (uiScene && uiScene.debugText) {
|
||||
const activeCrops = this.terrainSystem && this.terrainSystem.cropsMap ? this.terrainSystem.cropsMap.size : 0;
|
||||
const dropsCount = this.interactionSystem && this.interactionSystem.drops ? this.interactionSystem.drops.length : 0;
|
||||
|
||||
uiScene.debugText.setText(
|
||||
`FAZA 11 - Building\n` +
|
||||
`[F5] Save | [F9] Load | [B] Build Mode\n` +
|
||||
`Time: ${this.timeSystem ? this.timeSystem.gameTime.toFixed(1) : '?'}h\n` +
|
||||
`Active Crops: ${activeCrops}\n` +
|
||||
`Loot Drops: ${dropsCount}\n` +
|
||||
`Player: (${playerPos.x}, ${playerPos.y})`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createClouds() {
|
||||
if (!this.textures.exists('cloud')) TextureGenerator.createCloudSprite(this, 'cloud');
|
||||
|
||||
this.clouds = [];
|
||||
console.log('☁️ Creating parallax clouds...');
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const x = Phaser.Math.Between(-1000, 3000);
|
||||
const y = Phaser.Math.Between(-500, 1500);
|
||||
|
||||
const cloud = this.add.sprite(x, y, 'cloud');
|
||||
cloud.setAlpha(0.4);
|
||||
cloud.setScrollFactor(0.2); // Parallax effect
|
||||
cloud.setDepth(2000); // Nad vsem
|
||||
cloud.setScale(Phaser.Math.FloatBetween(2, 4)); // Veliki oblaki
|
||||
|
||||
this.clouds.push({ sprite: cloud, speed: Phaser.Math.FloatBetween(10, 30) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,32 +5,125 @@ class PreloadScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
preload() {
|
||||
console.log('📦 PreloadScene: Loading assets...');
|
||||
console.log('⏳ PreloadScene: Loading assets...');
|
||||
|
||||
// TODO: Tu bomo nalagali sprite-e, tile-e, audio, itd.
|
||||
// Za fazo 0 pustimo prazno - samo testiramo osnovni setup
|
||||
// Load ALL custom sprites
|
||||
this.load.image('player_sprite', 'assets/player_sprite.png');
|
||||
this.load.image('zombie_sprite', 'assets/zombie_sprite.png');
|
||||
this.load.image('merchant_sprite', 'assets/merchant_sprite.png');
|
||||
this.load.image('house_sprite', 'assets/house_sprite.png');
|
||||
this.load.image('stone_sprite', 'assets/stone_sprite.png');
|
||||
this.load.image('tree_sprite', 'assets/tree_sprite.png');
|
||||
this.load.image('grass_sprite', 'assets/grass_sprite.png');
|
||||
this.load.image('grass_tile', 'assets/grass_tile.png');
|
||||
this.load.image('leaf_sprite', 'assets/leaf_sprite.png');
|
||||
this.load.image('wheat_sprite', 'assets/wheat_sprite.png');
|
||||
this.load.image('stone_texture', 'assets/stone_texture.png');
|
||||
|
||||
// New asset packs
|
||||
this.load.image('objects_pack', 'assets/objects_pack.png');
|
||||
this.load.image('walls_pack', 'assets/walls_pack.png');
|
||||
this.load.image('ground_tiles', 'assets/ground_tiles.png');
|
||||
this.load.image('objects_pack2', 'assets/objects_pack2.png');
|
||||
this.load.image('trees_vegetation', 'assets/trees_vegetation.png');
|
||||
|
||||
// Wait for load completion then process transparency
|
||||
this.load.once('complete', () => {
|
||||
this.processAllTransparency();
|
||||
});
|
||||
}
|
||||
|
||||
processAllTransparency() {
|
||||
// Process ALL sprites to remove backgrounds
|
||||
const spritesToProcess = [
|
||||
'player_sprite',
|
||||
'zombie_sprite',
|
||||
'merchant_sprite',
|
||||
'house_sprite',
|
||||
'stone_sprite',
|
||||
'tree_sprite',
|
||||
'grass_sprite',
|
||||
'leaf_sprite',
|
||||
'wheat_sprite',
|
||||
'stone_texture'
|
||||
];
|
||||
|
||||
spritesToProcess.forEach(spriteKey => {
|
||||
this.processSpriteTransparency(spriteKey);
|
||||
});
|
||||
|
||||
console.log('✅ All sprites transparency processed!');
|
||||
}
|
||||
|
||||
processSpriteTransparency(spriteKey) {
|
||||
if (!this.textures.exists(spriteKey)) return;
|
||||
|
||||
const texture = this.textures.get(spriteKey);
|
||||
const source = texture.getSourceImage();
|
||||
|
||||
// Create canvas to process image
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = source.width;
|
||||
canvas.height = source.height;
|
||||
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
||||
|
||||
// Draw original image
|
||||
ctx.drawImage(source, 0, 0);
|
||||
|
||||
// Get image data
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const data = imageData.data;
|
||||
|
||||
// Remove backgrounds
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
const r = data[i];
|
||||
const g = data[i + 1];
|
||||
const b = data[i + 2];
|
||||
|
||||
// Remove white/light gray backgrounds (all sprites)
|
||||
if (r > 200 && g > 200 && b > 200) {
|
||||
data[i + 3] = 0;
|
||||
}
|
||||
|
||||
// Special: Remove brown/tan backgrounds (merchant sprite)
|
||||
if (spriteKey === 'merchant_sprite') {
|
||||
// Brown detection: R > G > B, warm tones
|
||||
const isBrown = r > 100 && r > g && g > b && (r - b) > 40;
|
||||
if (isBrown) {
|
||||
data[i + 3] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Put processed data back
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
|
||||
// Create new texture from processed canvas
|
||||
this.textures.remove(spriteKey);
|
||||
this.textures.addCanvas(spriteKey, canvas);
|
||||
}
|
||||
|
||||
create() {
|
||||
console.log('✅ PreloadScene: Assets loaded!');
|
||||
window.gameState.currentScene = 'PreloadScene';
|
||||
|
||||
// Prikaz začetnega sporočila
|
||||
const width = this.cameras.main.width;
|
||||
const height = this.cameras.main.height;
|
||||
|
||||
const title = this.add.text(width / 2, height / 2 - 50, 'NOVAFARMA', {
|
||||
const title = this.add.text(width / 2, height / 2 - 50, 'KRVAVA ŽETEV', {
|
||||
fontFamily: 'Courier New',
|
||||
fontSize: '48px',
|
||||
fill: '#00ff41',
|
||||
fontStyle: 'bold'
|
||||
fill: '#ff0000',
|
||||
fontStyle: 'bold',
|
||||
stroke: '#000000',
|
||||
strokeThickness: 6
|
||||
});
|
||||
title.setOrigin(0.5);
|
||||
|
||||
const subtitle = this.add.text(width / 2, height / 2 + 10, '2.5D Isometric Survival Game', {
|
||||
const subtitle = this.add.text(width / 2, height / 2 + 10, 'Zombie Roots', {
|
||||
fontFamily: 'Courier New',
|
||||
fontSize: '20px',
|
||||
fill: '#ffffff'
|
||||
fontSize: '24px',
|
||||
fill: '#00ff41'
|
||||
});
|
||||
subtitle.setOrigin(0.5);
|
||||
|
||||
@@ -41,7 +134,6 @@ class PreloadScene extends Phaser.Scene {
|
||||
});
|
||||
instruction.setOrigin(0.5);
|
||||
|
||||
// Blinking effect
|
||||
this.tweens.add({
|
||||
targets: instruction,
|
||||
alpha: 0.3,
|
||||
@@ -50,10 +142,25 @@ class PreloadScene extends Phaser.Scene {
|
||||
repeat: -1
|
||||
});
|
||||
|
||||
// Pritisk SPACE za začetek igre
|
||||
this.input.keyboard.once('keydown-SPACE', () => {
|
||||
console.log('🎮 Starting GameScene...');
|
||||
this.scene.start('GameScene');
|
||||
const startGame = () => {
|
||||
console.log('🎮 Starting StoryScene...');
|
||||
this.input.keyboard.off('keydown');
|
||||
this.input.off('pointerdown');
|
||||
this.scene.start('StoryScene');
|
||||
};
|
||||
|
||||
this.time.delayedCall(3000, () => {
|
||||
startGame();
|
||||
});
|
||||
|
||||
this.input.keyboard.on('keydown', (event) => {
|
||||
if (event.code === 'Space' || event.code === 'Enter') {
|
||||
startGame();
|
||||
}
|
||||
});
|
||||
|
||||
this.input.on('pointerdown', () => {
|
||||
startGame();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
70
src/scenes/StoryScene.js
Normal 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
@@ -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);
|
||||
}
|
||||
}
|
||||
112
src/systems/BuildingSystem.js
Normal 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() });
|
||||
}
|
||||
}
|
||||
91
src/systems/DayNightSystem.js
Normal 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';
|
||||
}
|
||||
}
|
||||
116
src/systems/FarmingSystem.js
Normal 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?
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
251
src/systems/InteractionSystem.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
85
src/systems/InventorySystem.js
Normal 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;
|
||||
}
|
||||
}
|
||||
198
src/systems/ParallaxSystem.js
Normal 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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// Terrain Generator System
|
||||
// Generira proceduralni isometrični teren
|
||||
// Generira proceduralni isometrični teren in skrbi za optimizacijo (Culling, Object Pooling)
|
||||
class TerrainSystem {
|
||||
constructor(scene, width = 100, height = 100) {
|
||||
this.scene = scene;
|
||||
@@ -10,43 +10,670 @@ class TerrainSystem {
|
||||
this.noise = new PerlinNoise(Date.now());
|
||||
|
||||
this.tiles = [];
|
||||
this.tileSprites = [];
|
||||
this.decorations = []; // Array za save/load compat
|
||||
this.decorationsMap = new Map(); // Fast lookup key->decor
|
||||
this.cropsMap = new Map(); // Store dynamic crops separately
|
||||
|
||||
// Tipi terena z threshold vrednostmi
|
||||
// Render state monitoring
|
||||
this.visibleTiles = new Map(); // Key: "x,y", Value: Sprite
|
||||
this.visibleDecorations = new Map(); // Key: "x,y", Value: Sprite
|
||||
this.visibleCrops = new Map(); // Key: "x,y", Value: Sprite
|
||||
|
||||
this.offsetX = 0;
|
||||
this.offsetY = 0;
|
||||
|
||||
// Object Pools
|
||||
this.tilePool = new ObjectPool(
|
||||
() => {
|
||||
const sprite = this.scene.add.image(0, 0, 'tile_grass');
|
||||
sprite.setOrigin(0.5, 0); // Isometrični tiles imajo origin zgoraj/center ali po potrebi
|
||||
return sprite;
|
||||
},
|
||||
(sprite) => {
|
||||
sprite.setVisible(true);
|
||||
sprite.setAlpha(1);
|
||||
sprite.clearTint();
|
||||
}
|
||||
);
|
||||
|
||||
this.decorationPool = new ObjectPool(
|
||||
() => {
|
||||
const sprite = this.scene.add.sprite(0, 0, 'flower');
|
||||
sprite.setOrigin(0.5, 1);
|
||||
return sprite;
|
||||
},
|
||||
(sprite) => {
|
||||
sprite.setVisible(true);
|
||||
sprite.setAlpha(1);
|
||||
sprite.clearTint(); // Reset damage tint
|
||||
}
|
||||
);
|
||||
|
||||
this.cropPool = new ObjectPool(
|
||||
() => {
|
||||
const sprite = this.scene.add.sprite(0, 0, 'crop_stage_1'); // Default texture logic needed
|
||||
sprite.setOrigin(0.5, 1);
|
||||
return sprite;
|
||||
},
|
||||
(sprite) => {
|
||||
sprite.setVisible(true);
|
||||
sprite.setAlpha(1);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
// Tipi terena z threshold vrednostmi + Y-LAYER STACKING
|
||||
this.terrainTypes = {
|
||||
WATER: { threshold: 0.3, color: 0x2166aa, name: 'water' },
|
||||
SAND: { threshold: 0.4, color: 0xf4e7c6, name: 'sand' },
|
||||
GRASS: { threshold: 0.65, color: 0x5cb85c, name: 'grass' },
|
||||
DIRT: { threshold: 0.75, color: 0x8b6f47, name: 'dirt' },
|
||||
STONE: { threshold: 1.0, color: 0x7d7d7d, name: 'stone' }
|
||||
WATER: { threshold: 0.3, color: 0x2166aa, name: 'water', texture: 'tile_water', yLayer: -1 },
|
||||
SAND: { threshold: 0.4, color: 0xf4e7c6, name: 'sand', texture: 'tile_sand', yLayer: 0 },
|
||||
|
||||
// Y-LAYER GRASS VARIANTS (A, B, C systém)
|
||||
GRASS_FULL: { threshold: 0.50, color: 0x5cb85c, name: 'grass_full', texture: 'tile_grass_full', yLayer: 0 }, // A: Full grass
|
||||
GRASS_TOP: { threshold: 0.60, color: 0x5cb85c, name: 'grass_top', texture: 'tile_grass_top', yLayer: 1 }, // B: Grass top, dirt sides
|
||||
DIRT: { threshold: 0.75, color: 0x8b6f47, name: 'dirt', texture: 'tile_dirt', yLayer: 2 }, // C: Full dirt
|
||||
STONE: { threshold: 1.0, color: 0x7d7d7d, name: 'stone', texture: 'tile_stone', yLayer: 3 },
|
||||
|
||||
FARMLAND: { threshold: 999, color: 0x4a3c2a, name: 'farmland', texture: 'tile_farmland', yLayer: 0 }
|
||||
};
|
||||
}
|
||||
|
||||
// Generiraj teren
|
||||
// Helper da dobi terrain type glede na elevation (Y-layer)
|
||||
getTerrainTypeByElevation(noiseValue, elevation) {
|
||||
// Osnovni terrain type iz noise
|
||||
let baseType = this.getTerrainType(noiseValue);
|
||||
|
||||
// Če je grass, določi Y-layer variant glede na elevation
|
||||
if (baseType.name.includes('grass') || baseType === this.terrainTypes.GRASS_FULL ||
|
||||
baseType === this.terrainTypes.GRASS_TOP) {
|
||||
|
||||
if (elevation > 0.7) {
|
||||
return this.terrainTypes.GRASS_FULL; // A: Najvišja plast (full grass)
|
||||
} else if (elevation > 0.4) {
|
||||
return this.terrainTypes.GRASS_TOP; // B: Srednja (grass top, dirt sides)
|
||||
} else {
|
||||
return this.terrainTypes.DIRT; // C: Nizka (full dirt)
|
||||
}
|
||||
}
|
||||
|
||||
return baseType;
|
||||
}
|
||||
|
||||
// Generiraj teksture za tiles (da ne uporabljamo Počasnih Graphics objektov)
|
||||
createTileTextures() {
|
||||
console.log('🎨 Creating tile textures...');
|
||||
|
||||
for (const type of Object.values(this.terrainTypes)) {
|
||||
const key = `tile_${type.name}`;
|
||||
if (this.scene.textures.exists(key)) continue;
|
||||
|
||||
// Check for custom grass tile
|
||||
if (type.name === 'grass' && this.scene.textures.exists('grass_tile')) {
|
||||
this.scene.textures.addImage(key, this.scene.textures.get('grass_tile').getSourceImage());
|
||||
type.texture = key;
|
||||
continue; // Skip procedural generation
|
||||
}
|
||||
|
||||
const tileW = this.iso.tileWidth;
|
||||
const tileH = this.iso.tileHeight;
|
||||
const thickness = 25; // Minecraft-style thickness (increased from 10)
|
||||
|
||||
const graphics = this.scene.make.graphics({ x: 0, y: 0, add: false });
|
||||
|
||||
// Helper for colors
|
||||
const baseColor = Phaser.Display.Color.IntegerToColor(type.color);
|
||||
let darkColor, darkerColor;
|
||||
|
||||
// Y-LAYER STACKING: Different side colors based on layer
|
||||
if (type.name === 'grass_full') {
|
||||
// A: Full Grass Block - všechno zeleno
|
||||
darkColor = 0x5cb85c; // Green (lighter)
|
||||
darkerColor = 0x4a9d3f; // Green (darker)
|
||||
} else if (type.name === 'grass_top') {
|
||||
// B: Grass Top - zgoraj zeleno, stranice zemlja
|
||||
darkColor = 0x8b6f47; // Dirt color (brown)
|
||||
darkerColor = 0x5d4a2e; // Darker Dirt
|
||||
} else if (type.name === 'dirt') {
|
||||
// C: Full Dirt - vse rjavo
|
||||
darkColor = 0x8b6f47; // Dirt
|
||||
darkerColor = 0x654321; // Darker Dirt
|
||||
} else {
|
||||
// Standard block: Darken base color significantly
|
||||
darkColor = Phaser.Display.Color.IntegerToColor(type.color).darken(30).color;
|
||||
darkerColor = Phaser.Display.Color.IntegerToColor(type.color).darken(50).color;
|
||||
}
|
||||
|
||||
// 1. Draw LEFT Side (Darker) - Minecraft volumetric effect
|
||||
graphics.fillStyle(darkColor, 1);
|
||||
graphics.beginPath();
|
||||
graphics.moveTo(0, tileH / 2); // Left Corner
|
||||
graphics.lineTo(tileW / 2, tileH); // Bottom Corner
|
||||
graphics.lineTo(tileW / 2, tileH + thickness); // Bottom Corner (Lowered)
|
||||
graphics.lineTo(0, tileH / 2 + thickness); // Left Corner (Lowered)
|
||||
graphics.closePath();
|
||||
graphics.fillPath();
|
||||
|
||||
// 2. Draw RIGHT Side (Darkest) - Strong shadow
|
||||
graphics.fillStyle(darkerColor, 1);
|
||||
graphics.beginPath();
|
||||
graphics.moveTo(tileW / 2, tileH); // Bottom Corner
|
||||
graphics.lineTo(tileW, tileH / 2); // Right Corner
|
||||
graphics.lineTo(tileW, tileH / 2 + thickness); // Right Corner (Lowered)
|
||||
graphics.lineTo(tileW / 2, tileH + thickness); // Bottom Corner (Lowered)
|
||||
graphics.closePath();
|
||||
graphics.fillPath();
|
||||
|
||||
// 3. Draw TOP Surface (bright)
|
||||
graphics.fillStyle(type.color, 1);
|
||||
graphics.beginPath();
|
||||
graphics.moveTo(tileW / 2, 0); // Top
|
||||
graphics.lineTo(tileW, tileH / 2); // Right
|
||||
graphics.lineTo(tileW / 2, tileH); // Bottom
|
||||
graphics.lineTo(0, tileH / 2); // Left
|
||||
graphics.closePath();
|
||||
graphics.fillPath();
|
||||
|
||||
// 4. Add Minecraft-style texture pattern on top
|
||||
if (type.name === 'grass_full' || type.name === 'grass_top') {
|
||||
// Grass texture: Random pixel pattern
|
||||
graphics.fillStyle(0x4a9d3f, 0.3); // Slightly darker green
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const px = Math.random() * tileW;
|
||||
const py = Math.random() * tileH;
|
||||
graphics.fillRect(px, py, 2, 2);
|
||||
}
|
||||
} else if (type.name === 'dirt') {
|
||||
// Dirt texture: Darker spots
|
||||
graphics.fillStyle(0x6d5838, 0.4);
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const px = Math.random() * tileW;
|
||||
const py = Math.random() * tileH;
|
||||
graphics.fillRect(px, py, 3, 3);
|
||||
}
|
||||
} else if (type.name === 'stone') {
|
||||
// Stone texture: Gray spots
|
||||
graphics.fillStyle(0x666666, 0.3);
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const px = Math.random() * tileW;
|
||||
const py = Math.random() * tileH;
|
||||
graphics.fillRect(px, py, 2, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Crisp black outline for block definition
|
||||
graphics.lineStyle(1, 0x000000, 0.3);
|
||||
graphics.strokePath();
|
||||
|
||||
// Generate texture
|
||||
graphics.generateTexture(key, tileW, tileH + thickness);
|
||||
graphics.destroy();
|
||||
|
||||
// Update texture name in type def
|
||||
type.texture = key;
|
||||
}
|
||||
}
|
||||
|
||||
createGravestoneSprite() {
|
||||
// Extract gravestone from objects_pack (approx position in atlas)
|
||||
// Gravestone appears to be around position row 4, column 4-5 in the pack
|
||||
const canvas = document.createElement('canvas');
|
||||
const size = 32; // Approximate sprite size
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
||||
|
||||
const sourceTexture = this.scene.textures.get('objects_pack');
|
||||
const sourceImg = sourceTexture.getSourceImage();
|
||||
|
||||
// Extract gravestone (cross tombstone) - estimated coords
|
||||
// Adjust these values based on actual sprite sheet layout
|
||||
const sourceX = 240; // Approximate X position
|
||||
const sourceY = 160; // Approximate Y position
|
||||
|
||||
ctx.drawImage(sourceImg, sourceX, sourceY, size, size, 0, 0, size, size);
|
||||
|
||||
// Create texture
|
||||
this.scene.textures.addCanvas('gravestone', canvas);
|
||||
console.log('✅ Gravestone sprite extracted!');
|
||||
}
|
||||
|
||||
// Generiraj teren (data only)
|
||||
generate() {
|
||||
console.log(`🌍 Generating terrain: ${this.width}x${this.height}...`);
|
||||
console.log(`🌍 Generating terrain data: ${this.width}x${this.height}...`);
|
||||
|
||||
// Zagotovi teksture
|
||||
this.createTileTextures();
|
||||
|
||||
// Zagotovi decoration teksture - check for custom sprites first
|
||||
if (!this.scene.textures.exists('flower')) {
|
||||
TextureGenerator.createFlowerSprite(this.scene, 'flower');
|
||||
}
|
||||
|
||||
// Bush - use custom stone sprite if available
|
||||
if (this.scene.textures.exists('stone_sprite')) {
|
||||
// Use stone_sprite for bushes (rocks)
|
||||
if (!this.scene.textures.exists('bush')) {
|
||||
this.scene.textures.addImage('bush', this.scene.textures.get('stone_sprite').getSourceImage());
|
||||
}
|
||||
} else if (!this.scene.textures.exists('bush')) {
|
||||
TextureGenerator.createBushSprite(this.scene, 'bush');
|
||||
}
|
||||
|
||||
// Tree - use custom tree sprite if available
|
||||
if (this.scene.textures.exists('tree_sprite')) {
|
||||
if (!this.scene.textures.exists('tree')) {
|
||||
this.scene.textures.addImage('tree', this.scene.textures.get('tree_sprite').getSourceImage());
|
||||
}
|
||||
} else if (!this.scene.textures.exists('tree')) {
|
||||
TextureGenerator.createTreeSprite(this.scene, 'tree');
|
||||
}
|
||||
|
||||
// Gravestone - extract from objects_pack
|
||||
if (this.scene.textures.exists('objects_pack') && !this.scene.textures.exists('gravestone')) {
|
||||
this.createGravestoneSprite();
|
||||
}
|
||||
|
||||
// Crop textures
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
if (!this.scene.textures.exists(`crop_stage_${i}`))
|
||||
TextureGenerator.createCropSprite(this.scene, `crop_stage_${i}`, i);
|
||||
}
|
||||
|
||||
this.decorationsMap.clear();
|
||||
this.decorations = [];
|
||||
this.cropsMap.clear();
|
||||
|
||||
// Generiraj tile podatke
|
||||
for (let y = 0; y < this.height; y++) {
|
||||
this.tiles[y] = [];
|
||||
for (let x = 0; x < this.width; x++) {
|
||||
const noiseValue = this.noise.getNormalized(x, y, 0.05, 4);
|
||||
const terrainType = this.getTerrainType(noiseValue);
|
||||
|
||||
// Elevation (druga Perlin noise layer za hribe)
|
||||
let elevation = this.noise.getNormalized(x, y, 0.03, 3);
|
||||
|
||||
// Get terrain type based on BOTH noise and elevation (Y-layer)
|
||||
let terrainType = this.getTerrainTypeByElevation(noiseValue, elevation);
|
||||
|
||||
// === FLOATING ISLAND EDGE ===
|
||||
const edgeDistance = 2; // Tiles from edge (tighter border)
|
||||
const isNearEdge = x < edgeDistance || x >= this.width - edgeDistance ||
|
||||
y < edgeDistance || y >= this.height - edgeDistance;
|
||||
|
||||
const isEdge = x === 0 || x === this.width - 1 ||
|
||||
y === 0 || y === this.height - 1;
|
||||
|
||||
// Override terrain type at edges
|
||||
if (isEdge) {
|
||||
terrainType = this.terrainTypes.STONE; // Cliff wall (stone edge)
|
||||
} else if (isNearEdge) {
|
||||
// Keep Y-layer system active
|
||||
}
|
||||
|
||||
// Flatten edges (cliff drop-off for floating island effect)
|
||||
if (isEdge) {
|
||||
elevation = 0; // Flat cliff wall
|
||||
} else if (isNearEdge) {
|
||||
elevation = Math.max(0, elevation - 0.2); // Slight dip near edge
|
||||
}
|
||||
|
||||
this.tiles[y][x] = {
|
||||
gridX: x,
|
||||
gridY: y,
|
||||
type: terrainType.name,
|
||||
color: terrainType.color,
|
||||
height: noiseValue
|
||||
texture: terrainType.texture,
|
||||
height: noiseValue,
|
||||
elevation: elevation, // 0-1 (0=low, 1=high)
|
||||
yLayer: terrainType.yLayer, // Y-stacking layer
|
||||
hasDecoration: false,
|
||||
hasCrop: false
|
||||
};
|
||||
|
||||
// Generacija dekoracij (shranimo v data, ne ustvarjamo sprite-ov še)
|
||||
if (x > 5 && x < this.width - 5 && y > 5 && y < this.height - 5) {
|
||||
let decorType = null;
|
||||
let maxHp = 1;
|
||||
|
||||
if (terrainType.name === 'grass') {
|
||||
const rand = Math.random();
|
||||
|
||||
// Na hribih več kamnov
|
||||
if (elevation > 0.6 && rand < 0.05) {
|
||||
decorType = 'bush'; // Kamni (bomo kasneje naredili 'stone' tip)
|
||||
maxHp = 5;
|
||||
} else if (rand < 0.01) {
|
||||
decorType = 'tree';
|
||||
maxHp = 5;
|
||||
} else if (rand < 0.015) {
|
||||
decorType = 'gravestone'; // 💀 Nagrobniki
|
||||
maxHp = 10; // Težje uničiti
|
||||
} else if (rand < 0.1) {
|
||||
decorType = 'flower';
|
||||
maxHp = 1;
|
||||
}
|
||||
} else if (terrainType.name === 'dirt' && Math.random() < 0.02) {
|
||||
decorType = 'bush';
|
||||
maxHp = 3;
|
||||
}
|
||||
|
||||
if (decorType) {
|
||||
const key = `${x},${y}`;
|
||||
const decorData = {
|
||||
gridX: x,
|
||||
gridY: y,
|
||||
type: decorType,
|
||||
id: key,
|
||||
maxHp: maxHp,
|
||||
hp: maxHp
|
||||
};
|
||||
|
||||
this.decorations.push(decorData);
|
||||
this.decorationsMap.set(key, decorData);
|
||||
|
||||
this.tiles[y][x].hasDecoration = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✅ Terrain data generated!');
|
||||
console.log('✅ Terrain and decorations data generated!');
|
||||
}
|
||||
|
||||
// Določi tip terena glede na noise vrednost
|
||||
// DAMAGE / INTERACTION LOGIC
|
||||
damageDecoration(x, y, amount) {
|
||||
const key = `${x},${y}`;
|
||||
const decor = this.decorationsMap.get(key);
|
||||
|
||||
if (!decor) return false;
|
||||
|
||||
decor.hp -= amount;
|
||||
|
||||
// Visual feedback (flash red)
|
||||
if (this.visibleDecorations.has(key)) {
|
||||
const sprite = this.visibleDecorations.get(key);
|
||||
sprite.setTint(0xff0000);
|
||||
this.scene.time.delayedCall(100, () => sprite.clearTint());
|
||||
|
||||
// Shake effect?
|
||||
this.scene.tweens.add({
|
||||
targets: sprite,
|
||||
x: sprite.x + 2,
|
||||
duration: 50,
|
||||
yoyo: true,
|
||||
repeat: 1
|
||||
});
|
||||
}
|
||||
|
||||
if (decor.hp <= 0) {
|
||||
this.removeDecoration(x, y);
|
||||
return 'destroyed';
|
||||
}
|
||||
|
||||
return 'hit';
|
||||
}
|
||||
|
||||
removeDecoration(x, y) {
|
||||
const key = `${x},${y}`;
|
||||
const decor = this.decorationsMap.get(key);
|
||||
|
||||
if (!decor) return;
|
||||
|
||||
// Remove visual
|
||||
if (this.visibleDecorations.has(key)) {
|
||||
const sprite = this.visibleDecorations.get(key);
|
||||
sprite.setVisible(false);
|
||||
this.decorationPool.release(sprite);
|
||||
this.visibleDecorations.delete(key);
|
||||
}
|
||||
|
||||
// Remove data
|
||||
this.decorationsMap.delete(key);
|
||||
|
||||
// Remove from array (slow but needed for save compat for now)
|
||||
const index = this.decorations.indexOf(decor);
|
||||
if (index > -1) this.decorations.splice(index, 1);
|
||||
|
||||
// Update tile flag
|
||||
if (this.tiles[y] && this.tiles[y][x]) {
|
||||
this.tiles[y][x].hasDecoration = false;
|
||||
}
|
||||
|
||||
return decor.type; // Return type for dropping loot
|
||||
}
|
||||
|
||||
placeStructure(x, y, structureType) {
|
||||
if (this.decorationsMap.has(`${x},${y}`)) return false;
|
||||
|
||||
const decorData = {
|
||||
gridX: x,
|
||||
gridY: y,
|
||||
type: structureType, // 'struct_fence', etc.
|
||||
id: `${x},${y}`,
|
||||
maxHp: 5,
|
||||
hp: 5
|
||||
};
|
||||
|
||||
// Add to data
|
||||
this.decorations.push(decorData);
|
||||
this.decorationsMap.set(decorData.id, decorData);
|
||||
|
||||
// Update tile
|
||||
const tile = this.getTile(x, y);
|
||||
if (tile) tile.hasDecoration = true;
|
||||
|
||||
// Force Visual Update immediately?
|
||||
// updateCulling will catch it on next frame, but to be safe:
|
||||
// Or leave it to update loop.
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Dynamic Tile Modification ---
|
||||
|
||||
setTileType(x, y, typeName) {
|
||||
if (!this.tiles[y] || !this.tiles[y][x]) return;
|
||||
|
||||
const typeDef = Object.values(this.terrainTypes).find(t => t.name === typeName);
|
||||
if (!typeDef) return;
|
||||
|
||||
this.tiles[y][x].type = typeName;
|
||||
this.tiles[y][x].texture = typeDef.texture;
|
||||
|
||||
// Force visual update if visible
|
||||
const key = `${x},${y}`;
|
||||
if (this.visibleTiles.has(key)) {
|
||||
const sprite = this.visibleTiles.get(key);
|
||||
sprite.setTexture(typeDef.texture);
|
||||
}
|
||||
}
|
||||
|
||||
addCrop(x, y, cropData) {
|
||||
const key = `${x},${y}`;
|
||||
this.cropsMap.set(key, cropData);
|
||||
this.tiles[y][x].hasCrop = true;
|
||||
// updateCulling loop will pick it up on next frame
|
||||
}
|
||||
|
||||
removeCrop(x, y) {
|
||||
const key = `${x},${y}`;
|
||||
if (this.cropsMap.has(key)) {
|
||||
// Remove visual
|
||||
if (this.visibleCrops.has(key)) {
|
||||
const sprite = this.visibleCrops.get(key);
|
||||
sprite.setVisible(false);
|
||||
this.cropPool.release(sprite);
|
||||
this.visibleCrops.delete(key);
|
||||
}
|
||||
this.cropsMap.delete(key);
|
||||
this.tiles[y][x].hasCrop = false;
|
||||
}
|
||||
}
|
||||
|
||||
updateCropVisual(x, y, stage) {
|
||||
const key = `${x},${y}`;
|
||||
if (this.visibleCrops.has(key)) {
|
||||
const sprite = this.visibleCrops.get(key);
|
||||
sprite.setTexture(`crop_stage_${stage}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize rendering (called once)
|
||||
init(offsetX, offsetY) {
|
||||
this.offsetX = offsetX;
|
||||
this.offsetY = offsetY;
|
||||
}
|
||||
|
||||
// Update culling (called every frame)
|
||||
updateCulling(camera) {
|
||||
const view = camera.worldView;
|
||||
const buffer = 200;
|
||||
const left = view.x - buffer - this.offsetX;
|
||||
const top = view.y - buffer - this.offsetY;
|
||||
const right = view.x + view.width + buffer - this.offsetX;
|
||||
const bottom = view.y + view.height + buffer - this.offsetY;
|
||||
|
||||
// Calculate visible bounding box (rough)
|
||||
const p1 = this.iso.toGrid(left, top);
|
||||
const p2 = this.iso.toGrid(right, top);
|
||||
const p3 = this.iso.toGrid(left, bottom);
|
||||
const p4 = this.iso.toGrid(right, bottom);
|
||||
|
||||
const minGridX = Math.floor(Math.min(p1.x, p2.x, p3.x, p4.x));
|
||||
const maxGridX = Math.ceil(Math.max(p1.x, p2.x, p3.x, p4.x));
|
||||
const minGridY = Math.floor(Math.min(p1.y, p2.y, p3.y, p4.y));
|
||||
const maxGridY = Math.ceil(Math.max(p1.y, p2.y, p3.y, p4.y));
|
||||
|
||||
const startX = Math.max(0, minGridX);
|
||||
const endX = Math.min(this.width, maxGridX);
|
||||
const startY = Math.max(0, minGridY);
|
||||
const endY = Math.min(this.height, maxGridY);
|
||||
|
||||
const neededKeys = new Set();
|
||||
const neededDecorKeys = new Set();
|
||||
const neededCropKeys = new Set();
|
||||
|
||||
for (let y = startY; y < endY; y++) {
|
||||
for (let x = startX; x < endX; x++) {
|
||||
const key = `${x},${y}`;
|
||||
neededKeys.add(key);
|
||||
|
||||
// Tile Logic
|
||||
if (!this.visibleTiles.has(key)) {
|
||||
const tilePos = this.iso.toScreen(x, y);
|
||||
const tileData = this.tiles[y][x];
|
||||
|
||||
const sprite = this.tilePool.get();
|
||||
sprite.setTexture(tileData.texture);
|
||||
|
||||
// Elevation effect: MOČAN vertikalni offset za hribe
|
||||
const elevationOffset = tileData.elevation * -25; // Povečano iz -10 na -25
|
||||
sprite.setPosition(
|
||||
tilePos.x + this.offsetX,
|
||||
tilePos.y + this.offsetY + elevationOffset
|
||||
);
|
||||
|
||||
// DRAMATIČNO senčenje glede na višino
|
||||
if (tileData.type === 'grass') {
|
||||
let brightness = 1.0;
|
||||
|
||||
if (tileData.elevation > 0.5) {
|
||||
// Visoko = svetlo (1.0 - 1.5)
|
||||
brightness = 1.0 + (tileData.elevation - 0.5) * 1.0;
|
||||
} else {
|
||||
// Nizko = temno (0.7 - 1.0)
|
||||
brightness = 0.7 + tileData.elevation * 0.6;
|
||||
}
|
||||
|
||||
sprite.setTint(Phaser.Display.Color.GetColor(
|
||||
Math.min(255, Math.floor(92 * brightness)),
|
||||
Math.min(255, Math.floor(184 * brightness)),
|
||||
Math.min(255, Math.floor(92 * brightness))
|
||||
));
|
||||
}
|
||||
|
||||
sprite.setDepth(this.iso.getDepth(x, y));
|
||||
|
||||
this.visibleTiles.set(key, sprite);
|
||||
}
|
||||
|
||||
// Crop Logic (render before decor or after? Same layer mostly)
|
||||
if (this.tiles[y][x].hasCrop) {
|
||||
neededCropKeys.add(key);
|
||||
if (!this.visibleCrops.has(key)) {
|
||||
const cropData = this.cropsMap.get(key);
|
||||
if (cropData) {
|
||||
const cropPos = this.iso.toScreen(x, y);
|
||||
const sprite = this.cropPool.get();
|
||||
sprite.setTexture(`crop_stage_${cropData.stage}`);
|
||||
sprite.setPosition(
|
||||
cropPos.x + this.offsetX,
|
||||
cropPos.y + this.offsetY + this.iso.tileHeight / 2
|
||||
);
|
||||
const depth = this.iso.getDepth(x, y);
|
||||
sprite.setDepth(depth + 1); // Just slightly above tile
|
||||
|
||||
this.visibleCrops.set(key, sprite);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Decoration Logic
|
||||
if (this.tiles[y][x].hasDecoration) {
|
||||
neededDecorKeys.add(key);
|
||||
|
||||
if (!this.visibleDecorations.has(key)) {
|
||||
// Fast lookup from map
|
||||
const decor = this.decorationsMap.get(key);
|
||||
|
||||
if (decor) {
|
||||
const decorPos = this.iso.toScreen(x, y);
|
||||
const sprite = this.decorationPool.get();
|
||||
sprite.setTexture(decor.type);
|
||||
sprite.setPosition(
|
||||
decorPos.x + this.offsetX,
|
||||
decorPos.y + this.offsetY + this.iso.tileHeight / 2
|
||||
);
|
||||
|
||||
const depth = this.iso.getDepth(x, y);
|
||||
if (decor.type === 'flower') sprite.setDepth(depth + 1);
|
||||
else sprite.setDepth(depth + 1000); // Taller objects update depth
|
||||
|
||||
sprite.flipX = (x + y) % 2 === 0;
|
||||
this.visibleDecorations.set(key, sprite);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup invisible tiles
|
||||
for (const [key, sprite] of this.visibleTiles) {
|
||||
if (!neededKeys.has(key)) {
|
||||
sprite.setVisible(false);
|
||||
this.tilePool.release(sprite);
|
||||
this.visibleTiles.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup invisible decorations
|
||||
for (const [key, sprite] of this.visibleDecorations) {
|
||||
if (!neededDecorKeys.has(key)) {
|
||||
sprite.setVisible(false);
|
||||
this.decorationPool.release(sprite);
|
||||
this.visibleDecorations.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup visible crops
|
||||
for (const [key, sprite] of this.visibleCrops) {
|
||||
if (!neededCropKeys.has(key)) {
|
||||
sprite.setVisible(false);
|
||||
this.cropPool.release(sprite);
|
||||
this.visibleCrops.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
getTerrainType(value) {
|
||||
for (const type of Object.values(this.terrainTypes)) {
|
||||
if (value < type.threshold) {
|
||||
@@ -56,71 +683,10 @@ class TerrainSystem {
|
||||
return this.terrainTypes.STONE;
|
||||
}
|
||||
|
||||
// Renderaj teren (visual sprites)
|
||||
render(offsetX = 0, offsetY = 300) {
|
||||
console.log('🎨 Rendering terrain sprites...');
|
||||
|
||||
const container = this.scene.add.container(offsetX, offsetY);
|
||||
|
||||
// Renderaj vse tile-e
|
||||
for (let y = 0; y < this.height; y++) {
|
||||
for (let x = 0; x < this.width; x++) {
|
||||
const tile = this.tiles[y][x];
|
||||
const screenPos = this.iso.toScreen(x, y);
|
||||
|
||||
// Kreira diamond (romb) obliko za isometric tile
|
||||
const graphics = this.scene.add.graphics();
|
||||
|
||||
// Osnovna barva
|
||||
const baseColor = tile.color;
|
||||
graphics.fillStyle(baseColor, 1);
|
||||
|
||||
// Nariši isometric tile (diamond shape)
|
||||
const tileWidth = this.iso.tileWidth;
|
||||
const tileHeight = this.iso.tileHeight;
|
||||
|
||||
graphics.beginPath();
|
||||
graphics.moveTo(screenPos.x, screenPos.y); // Top
|
||||
graphics.lineTo(screenPos.x + tileWidth / 2, screenPos.y + tileHeight / 2); // Right
|
||||
graphics.lineTo(screenPos.x, screenPos.y + tileHeight); // Bottom
|
||||
graphics.lineTo(screenPos.x - tileWidth / 2, screenPos.y + tileHeight / 2); // Left
|
||||
graphics.closePath();
|
||||
graphics.fillPath();
|
||||
|
||||
// Outline za boljšo vidljivost
|
||||
graphics.lineStyle(1, 0x000000, 0.2);
|
||||
graphics.strokePath();
|
||||
|
||||
// Dodaj v container
|
||||
container.add(graphics);
|
||||
|
||||
// Shrani referenco
|
||||
this.tileSprites.push({
|
||||
graphics: graphics,
|
||||
tile: tile,
|
||||
depth: this.iso.getDepth(x, y)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sortiraj po depth
|
||||
container.setDepth(0);
|
||||
|
||||
console.log(`✅ Rendered ${this.tileSprites.length} tiles!`);
|
||||
return container;
|
||||
}
|
||||
|
||||
// Pridobi tile na določenih grid koordinatah
|
||||
getTile(gridX, gridY) {
|
||||
if (gridX >= 0 && gridX < this.width && gridY >= 0 && gridY < this.height) {
|
||||
return this.tiles[gridY][gridX];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Screen koordinate -> tile
|
||||
getTileAtScreen(screenX, screenY) {
|
||||
const grid = this.iso.toGrid(screenX, screenY);
|
||||
return this.getTile(grid.x, grid.y);
|
||||
}
|
||||
}
|
||||
|
||||
107
src/systems/TimeSystem.js
Normal 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;
|
||||
}
|
||||
}
|
||||
159
src/systems/WeatherSystem.js
Normal 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
@@ -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;
|
||||
@@ -4,6 +4,7 @@ class TextureGenerator {
|
||||
|
||||
// Generiraj player sprite (32x32px pixel art)
|
||||
static createPlayerSprite(scene, key = 'player') {
|
||||
if (scene.textures.exists(key)) return;
|
||||
const size = 32;
|
||||
const canvas = scene.textures.createCanvas(key, size, size);
|
||||
const ctx = canvas.getContext();
|
||||
@@ -94,6 +95,7 @@ class TextureGenerator {
|
||||
|
||||
// Generiraj walking animacijo (4 frame-i)
|
||||
static createPlayerWalkSprite(scene, key = 'player_walk') {
|
||||
if (scene.textures.exists(key)) return;
|
||||
const frameWidth = 32;
|
||||
const frameHeight = 32;
|
||||
const frameCount = 4;
|
||||
@@ -177,9 +179,6 @@ class TextureGenerator {
|
||||
if (frame === 2) legOffset = 1; // Right foot forward
|
||||
|
||||
for (let y = 11; y < 16; y++) {
|
||||
const leftShift = (frame === 1) ? 0 : 0;
|
||||
const rightShift = (frame === 2) ? 0 : 0;
|
||||
|
||||
// Leva noga
|
||||
pixel(ox + 0, oy + y, outlineColor);
|
||||
pixel(ox + 1, oy + y, pantsColor);
|
||||
@@ -200,6 +199,7 @@ class TextureGenerator {
|
||||
|
||||
// Generiraj NPC sprite (32x32px pixel art)
|
||||
static createNPCSprite(scene, key = 'npc', type = 'zombie') {
|
||||
if (scene.textures.exists(key)) return;
|
||||
const size = 32;
|
||||
const canvas = scene.textures.createCanvas(key, size, size);
|
||||
const ctx = canvas.getContext();
|
||||
@@ -253,6 +253,19 @@ class TextureGenerator {
|
||||
pixel(ox + 2, oy + 4, outlineColor);
|
||||
pixel(ox + 5, oy + 4, outlineColor);
|
||||
|
||||
// Dreads (if Zombie)
|
||||
if (type === 'zombie') {
|
||||
const hairColor = '#3e2723'; // Dark Brown
|
||||
// Top
|
||||
for (let x = 1; x < 7; x++) pixel(ox + x, oy + 1, hairColor);
|
||||
// Side Dreads
|
||||
pixel(ox, oy + 2, hairColor); pixel(ox - 1, oy + 3, hairColor); pixel(ox - 1, oy + 4, hairColor);
|
||||
pixel(ox + 7, oy + 2, hairColor); pixel(ox + 8, oy + 3, hairColor); pixel(ox + 8, oy + 4, hairColor);
|
||||
// Back Dreads
|
||||
pixel(ox, oy + 5, hairColor);
|
||||
pixel(ox + 7, oy + 5, hairColor);
|
||||
}
|
||||
|
||||
// Telo - srajca
|
||||
for (let y = 6; y < 11; y++) {
|
||||
pixel(ox + 0, oy + y, outlineColor);
|
||||
@@ -290,4 +303,466 @@ class TextureGenerator {
|
||||
canvas.refresh();
|
||||
return canvas;
|
||||
}
|
||||
|
||||
// Generiraj Flower sprite (16x16px)
|
||||
static createFlowerSprite(scene, key = 'flower') {
|
||||
if (scene.textures.exists(key)) return;
|
||||
const size = 16;
|
||||
const canvas = scene.textures.createCanvas(key, size, size);
|
||||
const ctx = canvas.getContext();
|
||||
|
||||
ctx.clearRect(0, 0, size, size);
|
||||
|
||||
// Steblo
|
||||
ctx.fillStyle = '#228B22';
|
||||
ctx.fillRect(7, 8, 2, 8);
|
||||
ctx.fillRect(5, 12, 2, 1); // List levo
|
||||
ctx.fillRect(9, 10, 2, 1); // List desno
|
||||
|
||||
// Cvet (random barva vsakič ko kličemo? Ne, tekstura je statična, ampak lahko naredimo več variant)
|
||||
// Za zdaj rdeča roža
|
||||
ctx.fillStyle = '#FF0000';
|
||||
ctx.fillRect(6, 4, 4, 4); // Center
|
||||
ctx.fillStyle = '#FF69B4'; // Petals
|
||||
ctx.fillRect(6, 2, 4, 2); // Top
|
||||
ctx.fillRect(6, 8, 4, 2); // Bottom
|
||||
ctx.fillRect(4, 4, 2, 4); // Left
|
||||
ctx.fillRect(10, 4, 2, 4); // Right
|
||||
|
||||
canvas.refresh();
|
||||
return canvas;
|
||||
}
|
||||
|
||||
// Generiraj Bush sprite (32x32px)
|
||||
static createBushSprite(scene, key = 'bush') {
|
||||
if (scene.textures.exists(key)) return;
|
||||
const size = 32;
|
||||
const canvas = scene.textures.createCanvas(key, size, size);
|
||||
const ctx = canvas.getContext();
|
||||
|
||||
ctx.clearRect(0, 0, size, size);
|
||||
|
||||
// Grm
|
||||
ctx.fillStyle = '#006400'; // DarkGreen
|
||||
|
||||
// Risemo kroge/elipse pikslov za grm
|
||||
// Base
|
||||
ctx.fillRect(4, 16, 24, 14);
|
||||
ctx.fillRect(2, 20, 28, 6);
|
||||
|
||||
// Highlights
|
||||
ctx.fillStyle = '#228B22'; // ForestGreen
|
||||
ctx.fillRect(6, 18, 10, 6);
|
||||
ctx.fillRect(18, 14, 8, 8);
|
||||
|
||||
// Berries (rdeče pike)
|
||||
ctx.fillStyle = '#FF0000';
|
||||
ctx.fillRect(10, 20, 2, 2);
|
||||
ctx.fillRect(20, 18, 2, 2);
|
||||
ctx.fillRect(15, 24, 2, 2);
|
||||
|
||||
canvas.refresh();
|
||||
return canvas;
|
||||
}
|
||||
|
||||
// Generiraj Tree sprite (64x64px) - Blue Magical Tree
|
||||
static createTreeSprite(scene, key = 'tree') {
|
||||
if (scene.textures.exists(key)) return;
|
||||
const width = 64;
|
||||
const height = 64;
|
||||
const canvas = scene.textures.createCanvas(key, width, height);
|
||||
const ctx = canvas.getContext();
|
||||
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
// Trunk
|
||||
ctx.fillStyle = '#8B4513'; // SaddleBrown
|
||||
ctx.fillRect(28, 40, 8, 24); // Main trunk
|
||||
ctx.fillRect(24, 58, 4, 6); // Root L
|
||||
ctx.fillRect(36, 58, 4, 6); // Root R
|
||||
|
||||
// Branches
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(32, 40);
|
||||
ctx.lineTo(20, 30); // L
|
||||
ctx.lineTo(24, 28);
|
||||
ctx.lineTo(32, 35);
|
||||
ctx.fill();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(32, 40);
|
||||
ctx.lineTo(44, 30); // R
|
||||
ctx.lineTo(40, 28);
|
||||
ctx.lineTo(32, 35);
|
||||
ctx.fill();
|
||||
|
||||
// Foliage (Blue/Teal/Cyan)
|
||||
const cols = ['#008B8B', '#20B2AA', '#48D1CC', '#00CED1'];
|
||||
|
||||
const drawCluster = (cx, cy, r) => {
|
||||
const col = cols[Math.floor(Math.random() * cols.length)];
|
||||
ctx.fillStyle = col;
|
||||
for (let y = -r; y <= r; y++) {
|
||||
for (let x = -r; x <= r; x++) {
|
||||
if (x * x + y * y <= r * r) {
|
||||
ctx.fillRect(cx + x * 2, cy + y * 2, 2, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Main Canopy
|
||||
drawCluster(32, 20, 10);
|
||||
drawCluster(20, 25, 6);
|
||||
drawCluster(44, 25, 6);
|
||||
drawCluster(32, 10, 5);
|
||||
|
||||
// Magic sparkels
|
||||
ctx.fillStyle = '#E0FFFF'; // LightCyan
|
||||
for (let i = 0; i < 10; i++) {
|
||||
ctx.fillRect(10 + Math.random() * 44, 5 + Math.random() * 30, 2, 2);
|
||||
}
|
||||
|
||||
canvas.refresh();
|
||||
return canvas;
|
||||
}
|
||||
|
||||
// Generiraj Cloud sprite (64x32px)
|
||||
static createCloudSprite(scene, key = 'cloud') {
|
||||
if (scene.textures.exists(key)) return;
|
||||
const width = 64;
|
||||
const height = 32;
|
||||
const canvas = scene.textures.createCanvas(key, width, height);
|
||||
const ctx = canvas.getContext();
|
||||
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
ctx.fillStyle = '#FFFFFF';
|
||||
|
||||
// Simple pixel art cloud shape
|
||||
// Three circles/blobs
|
||||
ctx.fillRect(10, 10, 20, 15);
|
||||
ctx.fillRect(25, 5, 20, 20);
|
||||
ctx.fillRect(40, 10, 15, 12);
|
||||
|
||||
canvas.refresh();
|
||||
return canvas;
|
||||
}
|
||||
// Generiraj Crop sprite (32x32px) - stages 1-4
|
||||
static createCropSprite(scene, key, stage = 4) {
|
||||
if (scene.textures.exists(key)) return;
|
||||
const size = 32;
|
||||
const canvas = scene.textures.createCanvas(key, size, size);
|
||||
const ctx = canvas.getContext();
|
||||
|
||||
ctx.clearRect(0, 0, size, size);
|
||||
|
||||
const cx = 16;
|
||||
const cy = 24; // Base position
|
||||
|
||||
if (stage === 1) {
|
||||
// Seeds
|
||||
ctx.fillStyle = '#D2B48C';
|
||||
ctx.fillRect(cx - 2, cy, 2, 2);
|
||||
ctx.fillRect(cx + 2, cy - 2, 2, 2);
|
||||
ctx.fillRect(cx, cy + 2, 2, 2);
|
||||
} else if (stage === 2) {
|
||||
// Sprout
|
||||
ctx.fillStyle = '#32CD32'; // LimeGreen
|
||||
ctx.fillRect(cx - 1, cy, 2, 4); // Stem
|
||||
ctx.fillRect(cx - 3, cy - 2, 2, 2); // Leaf left
|
||||
ctx.fillRect(cx + 1, cy - 2, 2, 2); // Leaf right
|
||||
} else if (stage === 3) {
|
||||
// Growing
|
||||
ctx.fillStyle = '#228B22'; // ForestGreen
|
||||
ctx.fillRect(cx - 1, cy - 4, 3, 8); // Stem
|
||||
ctx.fillRect(cx - 5, cy - 4, 4, 3); // Leaf L
|
||||
ctx.fillRect(cx + 2, cy - 6, 4, 3); // Leaf R
|
||||
} else if (stage === 4) {
|
||||
// Ripe
|
||||
ctx.fillStyle = '#006400'; // DarkGreen
|
||||
ctx.fillRect(cx - 2, cy - 8, 4, 12); // Stem
|
||||
|
||||
// Leaves
|
||||
ctx.fillStyle = '#228B22';
|
||||
ctx.fillRect(cx - 6, cy - 2, 4, 4);
|
||||
ctx.fillRect(cx + 2, cy - 4, 4, 4);
|
||||
|
||||
// Fruit (Corn/Wheat/Generic yellow/orange)
|
||||
ctx.fillStyle = '#FFD700'; // Gold
|
||||
ctx.fillRect(cx - 2, cy - 12, 4, 6);
|
||||
}
|
||||
|
||||
canvas.refresh();
|
||||
return canvas;
|
||||
}
|
||||
// Generiraj Structure sprite (Fence, Wall, House)
|
||||
static createStructureSprite(scene, key, type) {
|
||||
if (scene.textures.exists(key)) return;
|
||||
const size = 32;
|
||||
const width = (type === 'house' || type === 'ruin') ? 64 : 32;
|
||||
const height = (type === 'house' || type === 'ruin') ? 64 : 32;
|
||||
|
||||
const canvas = scene.textures.createCanvas(key, width, height);
|
||||
const ctx = canvas.getContext();
|
||||
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
if (type === 'fence') {
|
||||
// Brown Fence
|
||||
ctx.fillStyle = '#8B4513';
|
||||
ctx.fillRect(8, 8, 4, 24); // Post L
|
||||
ctx.fillRect(20, 8, 4, 24); // Post R
|
||||
ctx.fillRect(8, 12, 16, 4); // Rail Top
|
||||
ctx.fillRect(8, 20, 16, 4); // Rail Bot
|
||||
} else if (type === 'wall') {
|
||||
// Grey Wall
|
||||
ctx.fillStyle = '#808080';
|
||||
ctx.fillRect(0, 8, 32, 24);
|
||||
ctx.fillStyle = '#696969'; // Bricks
|
||||
ctx.fillRect(4, 12, 10, 6);
|
||||
ctx.fillRect(18, 12, 10, 6);
|
||||
ctx.fillRect(2, 22, 10, 6);
|
||||
ctx.fillRect(16, 22, 10, 6);
|
||||
} else if (type === 'house') {
|
||||
// Isometric House
|
||||
// Left Wall (Darker)
|
||||
ctx.fillStyle = '#C2B280'; // Sand/Wheat dark
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(32, 60); // Bottom Center
|
||||
ctx.lineTo(10, 50); // Bottom Left corner
|
||||
ctx.lineTo(10, 30); // Top Left corner
|
||||
ctx.lineTo(32, 40); // Top Center (Roof start)
|
||||
ctx.fill();
|
||||
|
||||
// Right Wall (Lighter)
|
||||
ctx.fillStyle = '#F5DEB3'; // Wheat light
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(32, 60); // Bottom Center
|
||||
ctx.lineTo(54, 50); // Bottom Right corner
|
||||
ctx.lineTo(54, 30); // Top Right corner
|
||||
ctx.lineTo(32, 40); // Top Center
|
||||
ctx.fill();
|
||||
|
||||
// Door (Right Wall)
|
||||
ctx.fillStyle = '#8B4513';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(38, 56);
|
||||
ctx.lineTo(48, 52);
|
||||
ctx.lineTo(48, 38);
|
||||
ctx.lineTo(38, 42);
|
||||
ctx.fill();
|
||||
|
||||
// Roof (Left Slope)
|
||||
ctx.fillStyle = '#8B0000'; // Dark Red
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(32, 40);
|
||||
ctx.lineTo(10, 30);
|
||||
ctx.lineTo(32, 10); // Peak
|
||||
ctx.lineTo(32, 40);
|
||||
ctx.fill();
|
||||
|
||||
// Roof (Right Slope)
|
||||
ctx.fillStyle = '#FF0000'; // Red
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(32, 40);
|
||||
ctx.lineTo(54, 30);
|
||||
ctx.lineTo(32, 10); // Peak
|
||||
ctx.lineTo(32, 40);
|
||||
ctx.fill();
|
||||
|
||||
} else if (type === 'ruin') {
|
||||
// Isometric Ruin
|
||||
|
||||
// Left Wall (Broken)
|
||||
ctx.fillStyle = '#555555'; // Dark Grey
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(32, 60);
|
||||
ctx.lineTo(10, 50);
|
||||
ctx.lineTo(10, 40); // Lower than house
|
||||
ctx.lineTo(20, 45); // Jagged
|
||||
ctx.lineTo(25, 38);
|
||||
ctx.lineTo(32, 45);
|
||||
ctx.fill();
|
||||
|
||||
// Right Wall (Broken)
|
||||
ctx.fillStyle = '#777777'; // Light Grey
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(32, 60);
|
||||
ctx.lineTo(54, 50);
|
||||
ctx.lineTo(54, 35);
|
||||
ctx.lineTo(45, 30);
|
||||
ctx.lineTo(40, 35);
|
||||
ctx.lineTo(32, 25); // Exposed interior?
|
||||
ctx.lineTo(32, 60);
|
||||
ctx.fill();
|
||||
|
||||
// Debris piles
|
||||
ctx.fillStyle = '#333333';
|
||||
ctx.beginPath(); // Pile 1
|
||||
ctx.arc(20, 55, 5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.beginPath(); // Pile 2
|
||||
ctx.arc(45, 55, 4, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Overgrowth
|
||||
ctx.fillStyle = '#228B22';
|
||||
ctx.fillRect(10, 48, 4, 8); // Vines Left
|
||||
ctx.fillRect(50, 45, 5, 10); // Vines Right
|
||||
|
||||
// Random Bricks
|
||||
ctx.fillStyle = '#444444';
|
||||
ctx.fillRect(15, 60, 4, 2);
|
||||
ctx.fillRect(35, 62, 3, 2);
|
||||
}
|
||||
|
||||
canvas.refresh();
|
||||
return canvas;
|
||||
}
|
||||
|
||||
// ========== 2.5D VOLUMETRIC GENERATORS ==========
|
||||
|
||||
// Generiraj 3D volumetric tree (Minecraft-style)
|
||||
static createTreeSprite(scene, key = 'tree') {
|
||||
if (scene.textures.exists(key)) return;
|
||||
|
||||
const size = 64;
|
||||
const canvas = scene.textures.createCanvas(key, size, size);
|
||||
const ctx = canvas.getContext();
|
||||
|
||||
ctx.clearRect(0, 0, size, size);
|
||||
|
||||
// Tree trunk (3D block)
|
||||
const trunkW = 12;
|
||||
const trunkH = 24;
|
||||
const trunkX = size / 2 - trunkW / 2;
|
||||
const trunkY = size - trunkH - 8;
|
||||
|
||||
// Trunk - left side (darker)
|
||||
ctx.fillStyle = '#8B6F47';
|
||||
ctx.fillRect(trunkX, trunkY, trunkW / 2, trunkH);
|
||||
|
||||
// Trunk - right side (darkest)
|
||||
ctx.fillStyle = '#654321';
|
||||
ctx.fillRect(trunkX + trunkW / 2, trunkY, trunkW / 2, trunkH);
|
||||
|
||||
// Trunk - top (brightest)
|
||||
ctx.fillStyle = '#A0826D';
|
||||
ctx.fillRect(trunkX + 2, trunkY - 2, trunkW - 4, 2);
|
||||
|
||||
// Foliage (3D spherical)
|
||||
const foliageX = size / 2;
|
||||
const foliageY = trunkY - 8;
|
||||
const radius = 20;
|
||||
|
||||
// Back shadow
|
||||
ctx.fillStyle = '#228B22';
|
||||
ctx.beginPath();
|
||||
ctx.arc(foliageX - 2, foliageY + 2, radius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Main foliage
|
||||
ctx.fillStyle = '#32CD32';
|
||||
ctx.beginPath();
|
||||
ctx.arc(foliageX, foliageY, radius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Highlight
|
||||
ctx.fillStyle = '#90EE90';
|
||||
ctx.beginPath();
|
||||
ctx.arc(foliageX + 5, foliageY - 5, 8, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
canvas.refresh();
|
||||
return canvas;
|
||||
}
|
||||
|
||||
// Generiraj 3D volumetric bush/rock (Minecraft-style)
|
||||
static createBushSprite(scene, key = 'bush') {
|
||||
if (scene.textures.exists(key)) return;
|
||||
|
||||
const size = 48;
|
||||
const canvas = scene.textures.createCanvas(key, size, size);
|
||||
const ctx = canvas.getContext();
|
||||
|
||||
ctx.clearRect(0, 0, size, size);
|
||||
|
||||
// Rock/Bush as 3D isometric block
|
||||
const w = 24;
|
||||
const h = 16;
|
||||
const x = size / 2 - w / 2;
|
||||
const y = size - h - 4;
|
||||
|
||||
// Left face (darker)
|
||||
ctx.fillStyle = '#7d7d7d';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y + h / 2);
|
||||
ctx.lineTo(x + w / 2, y + h);
|
||||
ctx.lineTo(x + w / 2, y);
|
||||
ctx.lineTo(x, y + h / 2);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
// Right face (darkest)
|
||||
ctx.fillStyle = '#5a5a5a';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + w / 2, y + h);
|
||||
ctx.lineTo(x + w, y + h / 2);
|
||||
ctx.lineTo(x + w, y - h / 2);
|
||||
ctx.lineTo(x + w / 2, y);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
// Top face (brightest)
|
||||
ctx.fillStyle = '#a0a0a0';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y + h / 2);
|
||||
ctx.lineTo(x + w / 2, y);
|
||||
ctx.lineTo(x + w, y - h / 2);
|
||||
ctx.lineTo(x + w / 2, y);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
// Black outline
|
||||
ctx.strokeStyle = '#000000';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.stroke();
|
||||
|
||||
canvas.refresh();
|
||||
return canvas;
|
||||
}
|
||||
|
||||
// Generiraj 3D flower (simple volumetric)
|
||||
static createFlowerSprite(scene, key = 'flower') {
|
||||
if (scene.textures.exists(key)) return;
|
||||
|
||||
const size = 32;
|
||||
const canvas = scene.textures.createCanvas(key, size, size);
|
||||
const ctx = canvas.getContext();
|
||||
|
||||
ctx.clearRect(0, 0, size, size);
|
||||
|
||||
// Stem
|
||||
ctx.fillStyle = '#228B22';
|
||||
ctx.fillRect(size / 2 - 1, size / 2, 2, size / 2 - 4);
|
||||
|
||||
// Flower petals (simple 2D for flowers)
|
||||
const colors = ['#FF69B4', '#FFD700', '#FF4500', '#9370DB'];
|
||||
const color = colors[Math.floor(Math.random() * colors.length)];
|
||||
|
||||
ctx.fillStyle = color;
|
||||
ctx.beginPath();
|
||||
ctx.arc(size / 2, size / 2 - 4, 6, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = '#FFFF00';
|
||||
ctx.beginPath();
|
||||
ctx.arc(size / 2, size / 2 - 4, 3, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
canvas.refresh();
|
||||
return canvas;
|
||||
}
|
||||
}
|
||||
|
||||