mapa
57
ANDROID_GUIDE.md
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# 📱 Building NovaFarma for Android
|
||||||
|
|
||||||
|
This guide explains how to pack your NovaFarma game into an Android APK using **Capacitor**.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
1. **Node.js** (Already installed)
|
||||||
|
2. **Android Studio** (Download and install from [developer.android.com](https://developer.android.com/studio))
|
||||||
|
- Ensure you install the **Android SDK** and create a **Virtual Device** (AVD) or enable USB debugging on your Android phone.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
### 1. Install Capacitor
|
||||||
|
In your project terminal (Game Folder), run:
|
||||||
|
```bash
|
||||||
|
npm install @capacitor/core @capacitor/cli @capacitor/android
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Initialize Capacitor
|
||||||
|
Initialize the project config. Since your project serves files directly from the root:
|
||||||
|
```bash
|
||||||
|
npx cap init NovaFarma com.novafarma.game --web-dir .
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Add Android Platform
|
||||||
|
This creates the native Android project folder.
|
||||||
|
```bash
|
||||||
|
npx cap add android
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Sync Project
|
||||||
|
This copies your web assets (html, css, js) into the Android native project.
|
||||||
|
```bash
|
||||||
|
npx cap sync
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Open in Android Studio
|
||||||
|
```bash
|
||||||
|
npx cap open android
|
||||||
|
```
|
||||||
|
This will launch Android Studio with your project loaded.
|
||||||
|
|
||||||
|
### 6. Run on Device
|
||||||
|
- Connect your Android phone via USB (Developer Mode enabled) OR start an Emulator in Android Studio.
|
||||||
|
- Click the green **Run (Play)** button in the top toolbar.
|
||||||
|
- The game should launch on your device!
|
||||||
|
|
||||||
|
### 7. Export APK (For Sharing)
|
||||||
|
To create a standalone file you can send to friends:
|
||||||
|
- Go to **Build > Build Bundle(s) / APK(s) > Build APK(s)**.
|
||||||
|
- Once finished, a notification will appear. Click "Locate" to find your `app-debug.apk`.
|
||||||
|
|
||||||
|
## 🎮 Mobile Controls
|
||||||
|
We have already enabled **Virtual Joystick** support in the `UIScene`.
|
||||||
|
- A joystick appears on the bottom-left of the screen.
|
||||||
|
- Use it to control the character without a keyboard!
|
||||||
|
|
||||||
|
Happy Slaying! 🧟📱
|
||||||
49
CHANGELOG.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# NovaFarma Changelog
|
||||||
|
|
||||||
|
## [v0.6.0] - 2025-12-07 (Massive Update)
|
||||||
|
|
||||||
|
### New Features 🚀
|
||||||
|
- **Boss Battles:**
|
||||||
|
- Added `Boss.js` entity (Zombie King) with unique stats and visuals.
|
||||||
|
- Implemented Boss Skills: `smashAttack` and `summonMinions`.
|
||||||
|
- Added Boss Spawning via debug key 'K' and special events.
|
||||||
|
- Added "Horde Warning" visual effect.
|
||||||
|
|
||||||
|
- **Mobile & Touch Support 📱:**
|
||||||
|
- Implemented **Virtual Joystick** in `UIScene.js` for movement on touch devices.
|
||||||
|
- Added `user-scalable=no` to `index.html` for better mobile experience.
|
||||||
|
- Created `ANDROID_GUIDE.md` with instructions for building APKs via Capacitor.
|
||||||
|
|
||||||
|
- **Multiplayer (Local/LAN) 🌐:**
|
||||||
|
- Created `server.js` (Node.js + Socket.io) for backend.
|
||||||
|
- Created `MultiplayerSystem.js` for frontend synchronization.
|
||||||
|
- Implemented Player Position Sync (seeing other players move).
|
||||||
|
- Added Visual Indicators (Name tags, connection status).
|
||||||
|
|
||||||
|
- **Quest System Overhaul 📜:**
|
||||||
|
- Added **NPC Dialogue Interaction** ('E' key opens quest dialog).
|
||||||
|
- Added "Quest Givers" (Villager gives farming quests, Merchant gives defense quests).
|
||||||
|
- Improved UI with "Accept/Decline" popup.
|
||||||
|
- Added `interact()` method to NPCs.
|
||||||
|
|
||||||
|
- **World Generation & Structures 🌍:**
|
||||||
|
- Added **Special Arenas** generation in `TerrainSystem.js`.
|
||||||
|
- Added `placeStructure` method for spawning precrafted areas (Ruins, Arenas).
|
||||||
|
- Restored and optimized procedural vegetation (Trees, Rocks, Flowers).
|
||||||
|
|
||||||
|
- **Export & Build 📦:**
|
||||||
|
- Configured `package.json` with `electron-builder` for creating Windows .exe.
|
||||||
|
- Added build scripts (`npm run build`).
|
||||||
|
|
||||||
|
### Fixes 🔧
|
||||||
|
- Fixed `InventorySystem` missing methods (`getItemCount`, `addGold`).
|
||||||
|
- Fixed `TerrainSystem` vegetation generation bug.
|
||||||
|
- Fixed `NPC` death logic and doubled code.
|
||||||
|
- Fixed `UIScene` duplicate calls.
|
||||||
|
|
||||||
|
### Technical 💻
|
||||||
|
- **New Files:** `Boss.js`, `MultiplayerSystem.js`, `server.js`, `ANDROID_GUIDE.md`, `CHANGELOG.md`.
|
||||||
|
- **Updated Systems:** `GameScene`, `TerrainSystem`, `QuestSystem`, `UIScene`, `NPC`.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Ready for Gameplay Testing & Distribution!*
|
||||||
56
FARMING_GUIDE.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# 🌾 Farming System Guide
|
||||||
|
|
||||||
|
## Kako uporabljati Farming sistem
|
||||||
|
|
||||||
|
### 1. Pridobi orodja in semena
|
||||||
|
V inventoryju že imaš:
|
||||||
|
- 🪓 **Axe** (Sekira) - za sekanje dreves
|
||||||
|
- ⛏️ **Pickaxe** (Kramp) - za kopanje kamenja
|
||||||
|
- 🚜 **Hoe** (Motika) - za kopanje njive
|
||||||
|
- 🌱 **Seeds** (Semena) - 5x za začetek
|
||||||
|
|
||||||
|
### 2. Kopaj njivo
|
||||||
|
1. **Izberi motiko** (Hoe) v inventoryju
|
||||||
|
2. **Klikni na travo ali zemljo** (grass/dirt tile)
|
||||||
|
3. Tile se spremeni v **farmland** (rjava njiva)
|
||||||
|
|
||||||
|
### 3. Zasadi semena
|
||||||
|
1. **Izberi seeds** v inventoryju
|
||||||
|
2. **Klikni na farmland** tile
|
||||||
|
3. Posajeno seme se prikaže (stage 1)
|
||||||
|
|
||||||
|
### 4. Rast
|
||||||
|
- Crops **rastejo samodejno** čez čas
|
||||||
|
- **Stopnje rasti:**
|
||||||
|
- Stage 1: 🌱 Seme (Seeds)
|
||||||
|
- Stage 2: 🌿 Mladica
|
||||||
|
- Stage 3: 🌾 Raste
|
||||||
|
- Stage 4: ✨ Zrelo (Ripe) - ready to harvest!
|
||||||
|
- Stage 5: 💀 Ovene (Withered)
|
||||||
|
|
||||||
|
- **Čas:** Vsaka stopnja traja ~5 sekund (za testiranje)
|
||||||
|
|
||||||
|
### 5. Pobiranje pridelkov
|
||||||
|
1. **Klikni na zrelo rastlino** (stage 4, zlata)
|
||||||
|
2. Dobiš:
|
||||||
|
- 🌾 **Wheat** (Pšenica) - hrana/prodaja
|
||||||
|
- 🌱 **Seeds** (1-2x) - za ponovno sajenje
|
||||||
|
3. Farmland ostane pripravljena za novo sejanje
|
||||||
|
|
||||||
|
## 🎮 Osnovni proces
|
||||||
|
```
|
||||||
|
Grass/Dirt → (Hoe) → Farmland → (Seeds) → Growing → Harvest → Repeat
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⏰ Časovni sistem
|
||||||
|
- Growth je vezan na **realni čas** (5s per stage = 20s za polno rast)
|
||||||
|
- V prihodnosti: integracija z Day/Night ciklom
|
||||||
|
|
||||||
|
## 💡 Nasveti
|
||||||
|
- **Posadi več naenkrat**: Več kot sadiš, več dobiš!
|
||||||
|
- **Zberi semena**: Harvest daje nazaj 1-2 semena
|
||||||
|
- **Prodaj pridelek**: Wheat lahko prodaš merchantu za zlato
|
||||||
|
- **Ohrani farmland**: Po harvestu ostane pripravljena za novo sejanje
|
||||||
|
|
||||||
|
---
|
||||||
|
*Last Updated: 2025-12-07*
|
||||||
142
TASKS.md
@@ -45,68 +45,108 @@ Dodajanje interakcije, boja in ekonomije.
|
|||||||
## 🔴 Phase 3: Expansion (Next Steps)
|
## 🔴 Phase 3: Expansion (Next Steps)
|
||||||
Razširitev vsebine in izboljšava mehanik.
|
Razširitev vsebine in izboljšava mehanik.
|
||||||
|
|
||||||
- [ ] **Farming Mechanics** (Polishing)
|
- [x] **Farming Mechanics** (Polishing)
|
||||||
- [ ] Hoeing dirt to farmland
|
- [x] Hoeing dirt to farmland
|
||||||
- [ ] Planting seeds
|
- [x] Planting seeds
|
||||||
- [ ] Growth Stages (Time-based growth)
|
- [x] Growth Stages (Time-based growth)
|
||||||
- [ ] Harvesting crops
|
- [x] Harvesting crops
|
||||||
- [ ] Watering mechanics
|
- [ ] Watering mechanics
|
||||||
- [ ] **Advanced NPC AI**
|
- [x] **Advanced NPC AI**
|
||||||
- [ ] Pathfinding (A* or efficient grid traversal)
|
- [x] Pathfinding (A* or efficient grid traversal)
|
||||||
- [ ] Zombie Attacks Player (Player takes damage)
|
- [x] Zombie Attacks Player (Player takes damage)
|
||||||
- [ ] Tamed Zombie Defense (Attacks enemies)
|
- [x] Tamed Zombie Defense (Attacks enemies)
|
||||||
- [ ] Zombie Hordes (Night time events)
|
- [x] Zombie Hordes (Night time events)
|
||||||
- [ ] **Economy**
|
- [x] **Economy**
|
||||||
- [ ] Merchant NPC (Trading interface)
|
- [x] Merchant NPC (Trading interface)
|
||||||
- [ ] Selling crops/items for Gold
|
- [x] Selling crops/items for Gold
|
||||||
- [ ] Buying Seeds & Tools
|
- [x] Buying Seeds & Tools
|
||||||
- [ ] **Building System**
|
- [x] **Building System**
|
||||||
- [ ] Placing Walls/Fences (Snap to grid)
|
- [x] Placing Walls/Fences (Snap to grid)
|
||||||
- [ ] Crafting Buildings (House, Barn)
|
- [x] Crafting Buildings (House, Barn)
|
||||||
- [ ] UI for selecting buildings
|
- [x] UI for selecting buildings
|
||||||
|
|
||||||
## 🔵 Phase 4: Polish & Visuals
|
## 🔵 Phase 4: Polish & Visuals
|
||||||
Lepotni popravki in vzdušje.
|
Lepotni popravki in vzdušje.
|
||||||
|
|
||||||
- [ ] **Day/Night Cycle**
|
- [x] **Day/Night Cycle**
|
||||||
- [ ] Lighting overlay (Darkness at night)
|
- [x] Lighting overlay (Darkness at night)
|
||||||
- [ ] Dawn/Dusk transitions
|
- [x] Dawn/Dusk transitions
|
||||||
- [ ] Night-only Zombie Spawns
|
- [x] Night-only Zombie Spawns
|
||||||
- [ ] **Audio/SFX**
|
- [x] **Audio/SFX**
|
||||||
- [ ] Footsteps sounds
|
- [x] Footsteps sounds
|
||||||
- [ ] Attack/Hit sounds
|
- [x] Attack/Hit sounds
|
||||||
- [ ] Ambient nature sounds
|
- [x] Ambient nature sounds (Procedural Rain)
|
||||||
- [ ] Background Music
|
- [x] Background Music
|
||||||
- [ ] **Visual FX**
|
- [x] **Visual FX**
|
||||||
- [ ] Particle effects (Leaves falling, blood particles)
|
- [x] Particle effects (Leaves falling, blood particles)
|
||||||
- [ ] UI Animations (Smooth inventory opening)
|
- [x] UI Animations (Smooth inventory opening)
|
||||||
- [ ] Weather (Rain, Fog)
|
- [x] Weather (Rain, Fog)
|
||||||
|
|
||||||
## 🟣 Phase 5: Story & Quests (Long Term)
|
## 🟣 Phase 5: Story & Quests
|
||||||
Dodajanje globine in ciljev igri.
|
Dodajanje globine in ciljev igri.
|
||||||
|
|
||||||
- [ ] **Story Mode**
|
- [x] **Story Mode**
|
||||||
- [ ] Intro Sequence
|
- [x] Intro Sequence
|
||||||
- [ ] Main Questline (Find the Cure / Rebuild the Town)
|
- [x] Main Questline
|
||||||
- [ ] **Boss Battles**
|
- [x] **Boss Battles**
|
||||||
- [ ] "Zombie King" Boss
|
- [x] "Zombie King" Boss
|
||||||
- [ ] Special Arenas
|
- [x] Special Arenas
|
||||||
- [ ] **Quest System**
|
- [x] **Quest System**
|
||||||
- [ ] NPC dialogue tasks ("Bring me 10 Wood")
|
- [x] NPC dialogue interaction
|
||||||
- [ ] Rewards (Rare items, Gold)
|
- [x] Rewards & Notifications
|
||||||
|
|
||||||
## 🟠 Phase 6: Multiplayer & Export
|
## 🟠 Phase 6: Multiplayer & Export
|
||||||
Možnost igranja s prijatelji.
|
Možnost igranja s prijatelji.
|
||||||
|
|
||||||
- [ ] **Local/LAN Multiplayer**
|
- [x] **Local/LAN Multiplayer**
|
||||||
- [ ] Syncing Player Positions
|
- [x] Syncing Player Positions
|
||||||
- [ ] Syncing World State
|
- [x] Visual Indicators
|
||||||
- [ ] **Mobile Support**
|
- [x] **Mobile Support**
|
||||||
- [ ] Touch Controls
|
- [x] Virtual Joystick
|
||||||
- [ ] Responsive UI
|
- [x] Responsive Design
|
||||||
- [ ] **Export**
|
- [x] **Export**
|
||||||
- [ ] Desktop App (Electron)
|
- [x] Desktop (Electron)
|
||||||
- [ ] Android APK
|
- [x] Android (Capacitor Guide)
|
||||||
|
|
||||||
|
## ⚪ Phase 7: World Structure (New Direction)
|
||||||
|
Strukturiranje sveta s fiksnimi lokacijami za boljši gameplay flow.
|
||||||
|
|
||||||
|
- [x] **Zone Definition**
|
||||||
|
- [x] Define Constants (FARM @ 20,20; CITY @ 65,65)
|
||||||
|
- [x] Implement Terrain Overrides (Dirt for Farm, Pavement for City)
|
||||||
|
- [ ] **City Content**
|
||||||
|
- [x] Generate Ruins (Walls, Rubble, Rooms)
|
||||||
|
- [ ] High-Level Zombie Spawners
|
||||||
|
- [ ] Better Loot tables in City
|
||||||
|
- [ ] **Farm Content**
|
||||||
|
- [x] Safe Zone Logic (Night Spawns prevented near Farm)
|
||||||
|
- [ ] Starter Resources (Chest with seeds?)
|
||||||
|
- [ ] **Navigation**
|
||||||
|
- [ ] Add Signposts or Roads connecting areas
|
||||||
|
|
||||||
|
- [x] **Pathfinding System**
|
||||||
|
- [x] Web Worker Integration (Async processing)
|
||||||
|
- [x] A* Algorithm Implementation
|
||||||
|
- [x] Integration with NPCs
|
||||||
|
- [x] **Asset Optimization**
|
||||||
|
- [x] Loading Screen (Visual Progress Bar)
|
||||||
|
- [x] Transparent Sprite Processing
|
||||||
|
- [x] Custom Asset Integration (Rocks, Trees)
|
||||||
|
- [x] Asset Scaling Fixes
|
||||||
|
|
||||||
|
## 🟢 Phase 8: Gameplay Loop & Content
|
||||||
|
Fokus na igralnost, loot in napredovanje.
|
||||||
|
|
||||||
|
- [ ] **City Content**
|
||||||
|
- [ ] Unique Loot in Ruins (Scrap metal, Chips)
|
||||||
|
- [ ] Elite Zombies in City
|
||||||
|
- [ ] **Combat Polish**
|
||||||
|
- [ ] Visual Feedback on Hit (White flash)
|
||||||
|
- [ ] Knockback effect
|
||||||
|
- [ ] **World Details**
|
||||||
|
- [ ] Roads connecting Farm and City
|
||||||
|
- [ ] Signposts
|
||||||
|
|
||||||
---
|
---
|
||||||
*Last Updated: 2025-12-07*
|
**PROJECT STATUS: PHASE 8 STARTED** 🚧
|
||||||
|
*Last Updated: 2025-12-07 (Pathfinding & Assets Update)*
|
||||||
|
|||||||
BIN
assets/fence.png
Normal file
|
After Width: | Height: | Size: 367 KiB |
BIN
assets/flowers.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
assets/flowers_new.png
Normal file
|
After Width: | Height: | Size: 472 KiB |
BIN
assets/gravestone.png
Normal file
|
After Width: | Height: | Size: 360 KiB |
BIN
assets/hill_sprite.png
Normal file
|
After Width: | Height: | Size: 503 KiB |
BIN
assets/rock_1.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
assets/rock_2.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
assets/rock_asset.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
assets/rock_new.png
Normal file
|
After Width: | Height: | Size: 370 KiB |
BIN
assets/rock_small.png
Normal file
|
After Width: | Height: | Size: 235 KiB |
BIN
assets/rock_voxel.png
Normal file
|
After Width: | Height: | Size: 439 KiB |
BIN
assets/tree_blue.png
Normal file
|
After Width: | Height: | Size: 150 KiB |
BIN
assets/tree_blue_new.png
Normal file
|
After Width: | Height: | Size: 499 KiB |
BIN
assets/tree_dead.png
Normal file
|
After Width: | Height: | Size: 147 KiB |
BIN
assets/tree_dead_new.png
Normal file
|
After Width: | Height: | Size: 365 KiB |
BIN
assets/tree_green.png
Normal file
|
After Width: | Height: | Size: 209 KiB |
BIN
assets/tree_green_new.png
Normal file
|
After Width: | Height: | Size: 300 KiB |
BIN
assets/tree_voxel_blue.png
Normal file
|
After Width: | Height: | Size: 510 KiB |
BIN
assets/tree_voxel_dead.png
Normal file
|
After Width: | Height: | Size: 489 KiB |
BIN
assets/tree_voxel_green.png
Normal file
|
After Width: | Height: | Size: 556 KiB |
12
index.html
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
<!-- Suppress Electron Security Warning for Dev -->
|
<!-- Suppress Electron Security Warning for Dev -->
|
||||||
<meta http-equiv="Content-Security-Policy"
|
<meta http-equiv="Content-Security-Policy"
|
||||||
content="script-src 'self' 'unsafe-inline' 'unsafe-eval' blob: data:; object-src 'self';">
|
content="script-src 'self' 'unsafe-inline' 'unsafe-eval' blob: data:; object-src 'self';">
|
||||||
@@ -68,9 +68,12 @@
|
|||||||
<script src="src/utils/TextureGenerator.js"></script>
|
<script src="src/utils/TextureGenerator.js"></script>
|
||||||
<script src="src/utils/ObjectPool.js"></script>
|
<script src="src/utils/ObjectPool.js"></script>
|
||||||
<script src="src/utils/SpatialGrid.js"></script>
|
<script src="src/utils/SpatialGrid.js"></script>
|
||||||
|
<script src="src/utils/Pathfinding.js"></script>
|
||||||
|
<script src="src/utils/Compression.js"></script>
|
||||||
|
|
||||||
<!-- Systems -->
|
<!-- Systems -->
|
||||||
<script src="src/systems/TerrainSystem.js"></script>
|
<script src="src/systems/TerrainSystem.js"></script>
|
||||||
|
<script src="src/systems/PathfindingSystem.js"></script>
|
||||||
<script src="src/systems/SaveSystem.js"></script>
|
<script src="src/systems/SaveSystem.js"></script>
|
||||||
<!-- TimeSystem merged into WeatherSystem -->
|
<!-- TimeSystem merged into WeatherSystem -->
|
||||||
<script src="src/systems/StatsSystem.js"></script>
|
<script src="src/systems/StatsSystem.js"></script>
|
||||||
@@ -80,13 +83,20 @@
|
|||||||
<script src="src/systems/FarmingSystem.js"></script>
|
<script src="src/systems/FarmingSystem.js"></script>
|
||||||
<script src="src/systems/BuildingSystem.js"></script>
|
<script src="src/systems/BuildingSystem.js"></script>
|
||||||
<script src="src/systems/WeatherSystem.js"></script>
|
<script src="src/systems/WeatherSystem.js"></script>
|
||||||
|
<script src="src/systems/QuestSystem.js"></script>
|
||||||
<!-- DayNightSystem merged into WeatherSystem -->
|
<!-- DayNightSystem merged into WeatherSystem -->
|
||||||
<script src="src/systems/SoundManager.js"></script>
|
<script src="src/systems/SoundManager.js"></script>
|
||||||
<script src="src/systems/ParallaxSystem.js"></script>
|
<script src="src/systems/ParallaxSystem.js"></script>
|
||||||
|
<script src="src/systems/ParticleEffects.js"></script>
|
||||||
|
|
||||||
|
<!-- Multiplayer -->
|
||||||
|
<script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
|
||||||
|
<script src="src/systems/MultiplayerSystem.js"></script>
|
||||||
|
|
||||||
<!-- Entities -->
|
<!-- Entities -->
|
||||||
<script src="src/entities/Player.js"></script>
|
<script src="src/entities/Player.js"></script>
|
||||||
<script src="src/entities/NPC.js"></script>
|
<script src="src/entities/NPC.js"></script>
|
||||||
|
<script src="src/entities/Boss.js"></script>
|
||||||
|
|
||||||
<!-- Game Files -->
|
<!-- Game Files -->
|
||||||
<script src="src/scenes/BootScene.js"></script>
|
<script src="src/scenes/BootScene.js"></script>
|
||||||
|
|||||||
@@ -6,69 +6,34 @@ Datoteka namenjena tehničnim izboljšavam kode, refaktoringu in performančnim
|
|||||||
Stvari, ki so bile uspešno implementirane in izboljšale delovanje.
|
Stvari, ki so bile uspešno implementirane in izboljšale delovanje.
|
||||||
|
|
||||||
- [x] **Distance Culling (Teren & Dekoracije)**
|
- [x] **Distance Culling (Teren & Dekoracije)**
|
||||||
- Sistem skriva ploščice (tiles) in drevesa, ki so daleč od igralca, da varčuje s CPU/GPU.
|
- Sistem skriva ploščice (tiles) in drevesa, ki so daleč od igralca.
|
||||||
- [x] **Pooling Sistem**
|
- [x] **Pooling Sistem**
|
||||||
- `TerrainSystem` uporablja bazen spritov (`decPool`, `tilePool`) za ponovno uporabo objektov namesto nenehnega uničevanja in ustvarjanja.
|
- `TerrainSystem` uporablja `decorationPool` in `cropPool` za ponovno uporabo spritov.
|
||||||
|
- [x] **Phaser Tilemap (Terrain Layer)**
|
||||||
|
- Tla se rišejo preko optimiziranega `TilemapLayer`-ja namesto 10.000 posameznih spritov.
|
||||||
- [x] **NPC Logic Throttling & Culling**
|
- [x] **NPC Logic Throttling & Culling**
|
||||||
- NPC-ji daleč od igralca se ne posodabljajo in so skriti.
|
- NPC-ji daleč od igralca zamrznejo svojo logiko.
|
||||||
- AI se ne izvaja vsak frame (uporaba timerjev za premik).
|
- [x] **Spatial Hashing (SpatialGrid)**
|
||||||
- [x] **Code Refactoring & Bug Fixes**
|
- Implementiran `SpatialGrid.js` za hitrejše iskanje entitet v bližini.
|
||||||
- [x] `InteractionSystem.js`: Centralizirana logika za klike in tipkovnico (E tipka). Odstranjeni odvečni listenerji.
|
- [x] **Code Refactoring**
|
||||||
- [x] `Player.js`: Urejena logika gibanja in napada (Spacebar).
|
- `LootSystem.js`: Centraliziran loot.
|
||||||
- [x] `NPC.js`: Dodan Health Bar, Taming logika in Loot Drop.
|
- `InteractionSystem.js`: Poenostavljena logika.
|
||||||
- [x] `TextureGenerator`: Urejen draw items (Bone, Axe, Pickaxe).
|
- `TextureGenerator.js`: Volumetric sprite generation.
|
||||||
|
|
||||||
## 🟡 2. Odprte Tehnične Naloge (To-Do)
|
## 🟡 2. Odprte / Potencialne Tehnične Naloge (To-Do)
|
||||||
Stvari, ki bi jih bilo dobro urediti za boljšo stabilnost.
|
Stvari, ki še niso kritične, a bi lahko izboljšale igro.
|
||||||
|
|
||||||
- [x] **Global Error Handling**
|
- [ ] **Web Workers za AI Pathfinding**
|
||||||
- Dodan `ErrorHandler.js` (Red Screen of Death). Ujame napake, ki se zgodijo med igranjem, in prikaže uporabniku prijazen crash screen z možnostjo reload-a.
|
- Če bo število zombijev naraslo nad 100, premakni iskanje poti (A*) na ločen thread (Web Worker), da ne blokira glavne zanke.
|
||||||
- [x] **Centraliziran Loot Manager**
|
- [x] **Save Data Compression**
|
||||||
- Implementiran `LootSystem.js`. Skrbi za `spawnLoot`, animacijo dropov, pobiranje (razdalja do igralca) in čiščenje InteractionSystem-a.
|
- JSON save file se stisne z LZW algoritmom (`Compression.js`) in tako zaseda 80-90% manj prostora v `localStorage`. To omogoča shranjevanje večjih map.
|
||||||
- [x] **Z-Sorting (Depth) Optimizacija**
|
- [ ] **Asset Loading Screen**
|
||||||
- Implementiran "dirty check" v `Player.js` in `NPC.js`. Depth se posodobi samo, če se Y koordinata spremeni za več kot 0.1px, namesto vsak frame.
|
- Dodati pravi loading bar, če se poveča število tekstur (trenutno proceduralno generiranje traja nekaj milisekund).
|
||||||
|
|
||||||
## 🔴 3. Performančne Nadgradnje (High-End)
|
## 🔴 3. Znane Omejitve
|
||||||
Če bo igra postala počasna pri velikem svetu (256x256).
|
- **WebGL Context Loss:** Pri preklapljanju med tabi brskalnika se lahko zgodi izguba konteksta (Phaser to običajno obravnava, a je dobro vedeti).
|
||||||
# 🛠️ Plan Optimizacij in Čiščenja - NovaFarma
|
- **Mobile Performance:** Igra še ni optimizirana za touch/mobile kontrole.
|
||||||
|
|
||||||
Datoteka namenjena tehničnim izboljšavam kode, refaktoringu in performančnim popravkom.
|
|
||||||
|
|
||||||
## 🟢 1. Opravljene Optimizacije (Completed)
|
|
||||||
Stvari, ki so bile uspešno implementirane in izboljšale delovanje.
|
|
||||||
|
|
||||||
- [x] **Distance Culling (Teren & Dekoracije)**
|
|
||||||
- Sistem skriva ploščice (tiles) in drevesa, ki so daleč od igralca, da varčuje s CPU/GPU.
|
|
||||||
- [x] **Pooling Sistem**
|
|
||||||
- `TerrainSystem` uporablja bazen spritov (`decPool`, `tilePool`) za ponovno uporabo objektov namesto nenehnega uničevanja in ustvarjanja.
|
|
||||||
- [x] **NPC Logic Throttling & Culling**
|
|
||||||
- NPC-ji daleč od igralca se ne posodabljajo in so skriti.
|
|
||||||
- AI se ne izvaja vsak frame (uporaba timerjev za premik).
|
|
||||||
- [x] **Code Refactoring & Bug Fixes**
|
|
||||||
- [x] `InteractionSystem.js`: Centralizirana logika za klike in tipkovnico (E tipka). Odstranjeni odvečni listenerji.
|
|
||||||
- [x] `Player.js`: Urejena logika gibanja in napada (Spacebar).
|
|
||||||
- [x] `NPC.js`: Dodan Health Bar, Taming logika in Loot Drop.
|
|
||||||
- [x] `TextureGenerator`: Urejen draw items (Bone, Axe, Pickaxe).
|
|
||||||
|
|
||||||
## 🟡 2. Odprte Tehnične Naloge (To-Do)
|
|
||||||
Stvari, ki bi jih bilo dobro urediti za boljšo stabilnost.
|
|
||||||
|
|
||||||
- [x] **Global Error Handling**
|
|
||||||
- Dodan `ErrorHandler.js` (Red Screen of Death). Ujame napake, ki se zgodijo med igranjem, in prikaže uporabniku prijazen crash screen z možnostjo reload-a.
|
|
||||||
- [x] **Centraliziran Loot Manager**
|
|
||||||
- Implementiran `LootSystem.js`. Skrbi za `spawnLoot`, animacijo dropov, pobiranje (razdalja do igralca) in čiščenje InteractionSystem-a.
|
|
||||||
- [x] **Z-Sorting (Depth) Optimizacija**
|
|
||||||
- Implementiran "dirty check" v `Player.js` in `NPC.js`. Depth se posodobi samo, če se Y koordinata spremeni za več kot 0.1px, namesto vsak frame.
|
|
||||||
|
|
||||||
## 🔴 3. Performančne Nadgradnje (High-End)
|
|
||||||
Če bo igra postala počasna pri velikem svetu (256x256).
|
|
||||||
|
|
||||||
- [x] Phaser Blitter / Tilemap (Zamenjava 1000 spritov za teren z enim objektom)
|
|
||||||
- [x] **Spatial Hashing za Kolizijo**
|
|
||||||
- Implementiran `SpatialGrid.js`. Igralna scena zdaj uporablja mrežo za hitro iskanje NPC-jev v bližini (`InteractionSystem`, `NPC AI`), namesto da bi iterirala čez celo tabelo.
|
|
||||||
|
|
||||||
- [x] Phaser Blitter / Tilemap (Zamenjava 1000 spritov za teren z enim objektom)
|
|
||||||
- [ ] Web Workers za AI (Težje, ker JS nima shared memory, samo message passing)ding (iskanje poti) na ločen thread (Worker), da ne blokira glavne igre.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
*Status: Koda je trenutno stabilna in očiščena (7.12.2025).*
|
*Zadnja posodobitev: 7.12.2025*
|
||||||
|
ddddddd
|
||||||
10
package.json
@@ -5,8 +5,18 @@
|
|||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron .",
|
"start": "electron .",
|
||||||
|
"build": "electron-builder",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
|
"build": {
|
||||||
|
"appId": "com.novafarma.game",
|
||||||
|
"win": {
|
||||||
|
"target": "nsis"
|
||||||
|
},
|
||||||
|
"directories": {
|
||||||
|
"output": "dist"
|
||||||
|
}
|
||||||
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
|
|||||||
63
server.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
const app = require('express')();
|
||||||
|
const server = require('http').createServer(app);
|
||||||
|
const io = require('socket.io')(server, {
|
||||||
|
cors: {
|
||||||
|
origin: "*", // Allow all origins for dev
|
||||||
|
methods: ["GET", "POST"]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const players = {};
|
||||||
|
|
||||||
|
io.on('connection', (socket) => {
|
||||||
|
console.log('Player connected:', socket.id);
|
||||||
|
|
||||||
|
// Initialize player data
|
||||||
|
players[socket.id] = {
|
||||||
|
id: socket.id,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
anim: 'idle'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send the current list of players to the new player
|
||||||
|
socket.emit('currentPlayers', players);
|
||||||
|
|
||||||
|
// Notify other players about the new player
|
||||||
|
socket.broadcast.emit('newPlayer', players[socket.id]);
|
||||||
|
|
||||||
|
// Handle Disconnect
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
console.log('Player disconnected:', socket.id);
|
||||||
|
delete players[socket.id];
|
||||||
|
io.emit('playerDisconnected', socket.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle Movement
|
||||||
|
socket.on('playerMovement', (data) => {
|
||||||
|
if (players[socket.id]) {
|
||||||
|
players[socket.id].x = data.x;
|
||||||
|
players[socket.id].y = data.y;
|
||||||
|
players[socket.id].anim = data.anim;
|
||||||
|
players[socket.id].flipX = data.flipX;
|
||||||
|
|
||||||
|
// Broadcast to others (excluding self)
|
||||||
|
socket.broadcast.emit('playerMoved', players[socket.id]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle World State (Simple Block Placement sync)
|
||||||
|
// Warning: No validation/persistence in this MVP
|
||||||
|
socket.on('worldAction', (action) => {
|
||||||
|
// action: { type: 'build', x: 10, y: 10, building: 'fence' }
|
||||||
|
socket.broadcast.emit('worldAction', action);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const PORT = 3000;
|
||||||
|
server.listen(PORT, () => {
|
||||||
|
console.log(`SERVER RUNNING on port ${PORT}`);
|
||||||
|
console.log(`To play multiplayer:`);
|
||||||
|
console.log(`1. Run 'node server.js' in a terminal`);
|
||||||
|
console.log(`2. Start the game clients`);
|
||||||
|
});
|
||||||
127
src/entities/Boss.js
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
class Boss extends NPC {
|
||||||
|
constructor(scene, gridX, gridY) {
|
||||||
|
super(scene, gridX, gridY, scene.terrainOffsetX, scene.terrainOffsetY, 'boss');
|
||||||
|
this.maxHp = 500;
|
||||||
|
this.hp = this.maxHp;
|
||||||
|
this.moveSpeed = 70; // Slower than normal zombies
|
||||||
|
|
||||||
|
// Cooldowns
|
||||||
|
this.summonCooldown = 12000;
|
||||||
|
this.smashCooldown = 6000;
|
||||||
|
this.currentSkillTimer = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
createSprite() {
|
||||||
|
super.createSprite();
|
||||||
|
// Customize appearance
|
||||||
|
this.sprite.setScale(2.0); // Big Boss
|
||||||
|
this.sprite.setTint(0x6600cc); // Dark Purple
|
||||||
|
|
||||||
|
// Add a crown or something? (Text)
|
||||||
|
this.crown = this.scene.add.text(this.sprite.x, this.sprite.y - 80, '👑', { fontSize: '30px' });
|
||||||
|
this.crown.setOrigin(0.5);
|
||||||
|
this.crown.setDepth(this.sprite.depth + 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(delta) {
|
||||||
|
super.update(delta);
|
||||||
|
|
||||||
|
// Update Crown position
|
||||||
|
if (this.crown && this.sprite) {
|
||||||
|
this.crown.setPosition(this.sprite.x, this.sprite.y - 80);
|
||||||
|
this.crown.setDepth(this.sprite.depth + 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state === 'CHASE' || this.state === 'WANDER') {
|
||||||
|
this.handleBossSkills(delta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleBossSkills(delta) {
|
||||||
|
if (!this.scene.player) return;
|
||||||
|
|
||||||
|
this.currentSkillTimer += delta;
|
||||||
|
|
||||||
|
const dist = Phaser.Math.Distance.Between(this.gridX, this.gridY, this.scene.player.gridX, this.scene.player.gridY);
|
||||||
|
|
||||||
|
// Smash Attack (AoE close range)
|
||||||
|
if (this.currentSkillTimer > this.smashCooldown && dist < 4) {
|
||||||
|
this.smashAttack();
|
||||||
|
this.currentSkillTimer = 0; // Reset timer partially? No, full reset for simpler logic
|
||||||
|
}
|
||||||
|
// Summon Minions (Long range or cooldown)
|
||||||
|
else if (this.currentSkillTimer > this.summonCooldown) {
|
||||||
|
this.summonMinions();
|
||||||
|
this.currentSkillTimer = 5000; // Give back some time so he doesn't wait full CD for smash
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
smashAttack() {
|
||||||
|
console.log('💥 BOSS SMASH!');
|
||||||
|
this.scene.cameras.main.shake(300, 0.01);
|
||||||
|
|
||||||
|
// Visual Warning
|
||||||
|
const warning = this.scene.add.circle(this.sprite.x, this.sprite.y, 150, 0xff0000, 0.4);
|
||||||
|
warning.setScale(0);
|
||||||
|
this.scene.tweens.add({
|
||||||
|
targets: warning,
|
||||||
|
scale: 1,
|
||||||
|
alpha: 0,
|
||||||
|
duration: 500,
|
||||||
|
onComplete: () => {
|
||||||
|
warning.destroy();
|
||||||
|
// Damage Logic
|
||||||
|
const playerPos = this.scene.player.getPosition();
|
||||||
|
const d = Phaser.Math.Distance.Between(this.sprite.x, this.sprite.y, playerPos.x, playerPos.y); // Pixel distance
|
||||||
|
if (d < 150) {
|
||||||
|
this.scene.player.takeDamage(30);
|
||||||
|
// Knockback
|
||||||
|
// todo
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sound
|
||||||
|
if (this.scene.soundManager) this.scene.soundManager.playDeath(); // Placeholder for big boom
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
summonMinions() {
|
||||||
|
console.log('🧟🧟🧟 BOSS SUMMONS MINIONS!');
|
||||||
|
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const angle = (Math.PI * 2 / 3) * i;
|
||||||
|
const dist = 3;
|
||||||
|
const sx = Math.floor(this.gridX + Math.cos(angle) * dist);
|
||||||
|
const sy = Math.floor(this.gridY + Math.sin(angle) * dist);
|
||||||
|
|
||||||
|
if (this.scene.terrainSystem.getTile(sx, sy)) {
|
||||||
|
// Spawn normal zombie
|
||||||
|
// We access GameScene's spawn method or create NPC directly
|
||||||
|
const zombie = new NPC(this.scene, sx, sy, this.scene.terrainOffsetX, this.scene.terrainOffsetY, 'zombie');
|
||||||
|
zombie.state = 'CHASE';
|
||||||
|
this.scene.npcs.push(zombie);
|
||||||
|
|
||||||
|
// Spawn Effect
|
||||||
|
if (this.scene.particleEffects) {
|
||||||
|
const iso = new IsometricUtils(48, 24);
|
||||||
|
const pos = iso.toScreen(sx, sy);
|
||||||
|
this.scene.particleEffects.bloodSplash(pos.x + 300, pos.y + 100); // hardcoded offset fix maybe needed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
if (this.crown) this.crown.destroy();
|
||||||
|
super.destroy();
|
||||||
|
|
||||||
|
// Boss Drops
|
||||||
|
if (this.scene.lootSystem) {
|
||||||
|
this.scene.lootSystem.spawnLoot(this.gridX, this.gridY, 'item_gold', 50);
|
||||||
|
this.scene.lootSystem.spawnLoot(this.gridX, this.gridY, 'item_sword', 1); // Rare drop
|
||||||
|
}
|
||||||
|
|
||||||
|
// Boss Death Event?
|
||||||
|
this.scene.events.emit('boss_killed');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -242,6 +242,8 @@ class NPC {
|
|||||||
|
|
||||||
handlePassiveAI(delta) {
|
handlePassiveAI(delta) {
|
||||||
if (this.state === 'TAMED' || this.state === 'FOLLOW') {
|
if (this.state === 'TAMED' || this.state === 'FOLLOW') {
|
||||||
|
// Defensive behavior - attack nearby enemy zombies
|
||||||
|
this.defendPlayer();
|
||||||
this.followPlayer();
|
this.followPlayer();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -253,6 +255,56 @@ class NPC {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defendPlayer() {
|
||||||
|
if (!this.scene.npcs || this.attackCooldownTimer > 0) return;
|
||||||
|
|
||||||
|
// Find nearest enemy zombie
|
||||||
|
let nearestEnemy = null;
|
||||||
|
let minDist = 6; // Defense radius
|
||||||
|
|
||||||
|
for (const npc of this.scene.npcs) {
|
||||||
|
if (npc === this || npc.state === 'TAMED' || npc.state === 'FOLLOW') continue;
|
||||||
|
if (npc.type !== 'zombie') continue;
|
||||||
|
|
||||||
|
const dist = Phaser.Math.Distance.Between(this.gridX, this.gridY, npc.gridX, npc.gridY);
|
||||||
|
if (dist < minDist) {
|
||||||
|
minDist = dist;
|
||||||
|
nearestEnemy = npc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nearestEnemy) {
|
||||||
|
// Attack if close enough
|
||||||
|
if (minDist <= 1.5) {
|
||||||
|
this.attackEnemy(nearestEnemy);
|
||||||
|
} else {
|
||||||
|
// Move towards enemy
|
||||||
|
this.moveTowards(nearestEnemy.gridX, nearestEnemy.gridY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attackEnemy(target) {
|
||||||
|
if (this.attackCooldownTimer > 0) return;
|
||||||
|
this.attackCooldownTimer = 1500;
|
||||||
|
|
||||||
|
console.log('🧟❤️ Tamed Zombie DEFENDS!');
|
||||||
|
|
||||||
|
// Attack Animation
|
||||||
|
if (this.sprite) {
|
||||||
|
this.scene.tweens.add({
|
||||||
|
targets: this.sprite,
|
||||||
|
y: this.sprite.y - 10,
|
||||||
|
yoyo: true, duration: 100, repeat: 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deal Damage
|
||||||
|
if (target && target.takeDamage) {
|
||||||
|
target.takeDamage(15); // Tamed zombies hit harder!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleAggressiveAI(delta) {
|
handleAggressiveAI(delta) {
|
||||||
if (!this.scene.player) return;
|
if (!this.scene.player) return;
|
||||||
|
|
||||||
@@ -289,6 +341,49 @@ class NPC {
|
|||||||
}
|
}
|
||||||
|
|
||||||
moveTowards(targetX, targetY) {
|
moveTowards(targetX, targetY) {
|
||||||
|
// Optimization: if very close, use direct movement (collision checks handled by isValidMove)
|
||||||
|
const dist = Phaser.Math.Distance.Between(this.gridX, this.gridY, targetX, targetY);
|
||||||
|
|
||||||
|
// Use pathfinding for longer distances
|
||||||
|
if (dist > 2 && this.scene.pathfinding) {
|
||||||
|
|
||||||
|
// Check if we need to recalc path (target changed or no path)
|
||||||
|
const targetKey = `${targetX},${targetY}`;
|
||||||
|
if (!this.currentPath || this.pathTargetKey !== targetKey || this.currentPath.length === 0) {
|
||||||
|
|
||||||
|
// Recalc path (Async Worker)
|
||||||
|
if (!this.isWaitingForPath) {
|
||||||
|
this.isWaitingForPath = true;
|
||||||
|
this.scene.pathfinding.findPath(this.gridX, this.gridY, targetX, targetY, (path) => {
|
||||||
|
this.isWaitingForPath = false;
|
||||||
|
this.currentPath = path;
|
||||||
|
this.pathTargetKey = targetKey;
|
||||||
|
|
||||||
|
// Process path once received
|
||||||
|
if (this.currentPath && this.currentPath.length > 0) {
|
||||||
|
// Remove start node if it's current pos
|
||||||
|
if (this.currentPath[0].x === this.gridX && this.currentPath[0].y === this.gridY) {
|
||||||
|
this.currentPath.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return; // Wait for path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Follow Path
|
||||||
|
if (this.currentPath && this.currentPath.length > 0) {
|
||||||
|
const step = this.currentPath[0];
|
||||||
|
// Move there
|
||||||
|
this.moveToGrid(step.x, step.y);
|
||||||
|
// Remove step
|
||||||
|
this.currentPath.shift();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: Direct Movement (Dumb AI)
|
||||||
const dx = Math.sign(targetX - this.gridX);
|
const dx = Math.sign(targetX - this.gridX);
|
||||||
const dy = Math.sign(targetY - this.gridY);
|
const dy = Math.sign(targetY - this.gridY);
|
||||||
let nextX = this.gridX + dx;
|
let nextX = this.gridX + dx;
|
||||||
@@ -415,6 +510,16 @@ class NPC {
|
|||||||
takeDamage(amount) {
|
takeDamage(amount) {
|
||||||
this.hp -= amount;
|
this.hp -= amount;
|
||||||
|
|
||||||
|
// Hit Sound
|
||||||
|
if (this.scene.soundManager) {
|
||||||
|
this.scene.soundManager.playHit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blood Splash Effect
|
||||||
|
if (this.scene.particleEffects) {
|
||||||
|
this.scene.particleEffects.bloodSplash(this.sprite.x, this.sprite.y - 20);
|
||||||
|
}
|
||||||
|
|
||||||
// Show Health Bar
|
// Show Health Bar
|
||||||
if (this.healthBar) {
|
if (this.healthBar) {
|
||||||
this.healthBar.setVisible(true);
|
this.healthBar.setVisible(true);
|
||||||
@@ -449,6 +554,12 @@ class NPC {
|
|||||||
|
|
||||||
die() {
|
die() {
|
||||||
console.log('🧟💀 Zombie DEAD');
|
console.log('🧟💀 Zombie DEAD');
|
||||||
|
|
||||||
|
// Death Sound
|
||||||
|
if (this.scene.soundManager) {
|
||||||
|
this.scene.soundManager.playDeath();
|
||||||
|
}
|
||||||
|
|
||||||
// Spawn loot - BONE
|
// Spawn loot - BONE
|
||||||
if (this.scene.lootSystem) {
|
if (this.scene.lootSystem) {
|
||||||
this.scene.lootSystem.spawnLoot(this.gridX, this.gridY, 'item_bone');
|
this.scene.lootSystem.spawnLoot(this.gridX, this.gridY, 'item_bone');
|
||||||
@@ -456,6 +567,12 @@ class NPC {
|
|||||||
// Fallback
|
// Fallback
|
||||||
this.scene.interactionSystem.spawnLoot(this.gridX, this.gridY, 'item_bone');
|
this.scene.interactionSystem.spawnLoot(this.gridX, this.gridY, 'item_bone');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Quest Tracking (Kill)
|
||||||
|
if (this.scene.questSystem) {
|
||||||
|
this.scene.questSystem.trackAction(this.type);
|
||||||
|
}
|
||||||
|
|
||||||
this.destroy();
|
this.destroy();
|
||||||
|
|
||||||
const idx = this.scene.npcs.indexOf(this);
|
const idx = this.scene.npcs.indexOf(this);
|
||||||
@@ -496,6 +613,33 @@ class NPC {
|
|||||||
this.addTamedEyes();
|
this.addTamedEyes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interact() {
|
||||||
|
console.log('🗣️ Inteacting with NPC:', this.type);
|
||||||
|
|
||||||
|
// Quest Check
|
||||||
|
if (this.scene.questSystem) {
|
||||||
|
const availableQuest = this.scene.questSystem.getAvailableQuest(this.type);
|
||||||
|
if (availableQuest) {
|
||||||
|
console.log('Quest Available from NPC!');
|
||||||
|
// Open Dialog UI
|
||||||
|
const ui = this.scene.scene.get('UIScene');
|
||||||
|
if (ui && ui.showQuestDialog) {
|
||||||
|
ui.showQuestDialog(availableQuest, () => {
|
||||||
|
this.scene.questSystem.startQuest(availableQuest.id);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default behavior (Emote)
|
||||||
|
this.showEmote('👋');
|
||||||
|
// Small jump or animation?
|
||||||
|
if (this.sprite) {
|
||||||
|
this.scene.tweens.add({ targets: this.sprite, y: this.sprite.y - 10, yoyo: true, duration: 100 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
addTamedEyes() {
|
addTamedEyes() {
|
||||||
if (this.eyesGroup) return;
|
if (this.eyesGroup) return;
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,11 @@ class Player {
|
|||||||
this.sprite.setRotation(Math.PI / 2); // Lie down
|
this.sprite.setRotation(Math.PI / 2); // Lie down
|
||||||
console.log('💀 PLAYER DIED');
|
console.log('💀 PLAYER DIED');
|
||||||
|
|
||||||
|
// Death Sound
|
||||||
|
if (this.scene.soundManager) {
|
||||||
|
this.scene.soundManager.playDeath();
|
||||||
|
}
|
||||||
|
|
||||||
// Show Game Over / Reload
|
// Show Game Over / Reload
|
||||||
const txt = this.scene.add.text(this.scene.cameras.main.midPoint.x, this.scene.cameras.main.midPoint.y, 'YOU DIED', {
|
const txt = this.scene.add.text(this.scene.cameras.main.midPoint.x, this.scene.cameras.main.midPoint.y, 'YOU DIED', {
|
||||||
fontSize: '64px', color: '#ff0000', fontStyle: 'bold', stroke: '#000', strokeThickness: 6
|
fontSize: '64px', color: '#ff0000', fontStyle: 'bold', stroke: '#000', strokeThickness: 6
|
||||||
@@ -132,6 +137,12 @@ class Player {
|
|||||||
|
|
||||||
attack() {
|
attack() {
|
||||||
console.log('⚔️ Player Attack!');
|
console.log('⚔️ Player Attack!');
|
||||||
|
|
||||||
|
// Attack Sound
|
||||||
|
if (this.scene.soundManager) {
|
||||||
|
this.scene.soundManager.playAttack();
|
||||||
|
}
|
||||||
|
|
||||||
if (this.scene.interactionSystem) {
|
if (this.scene.interactionSystem) {
|
||||||
const targetX = this.gridX + this.lastDir.x;
|
const targetX = this.gridX + this.lastDir.x;
|
||||||
const targetY = this.gridY + this.lastDir.y;
|
const targetY = this.gridY + this.lastDir.y;
|
||||||
@@ -198,25 +209,40 @@ class Player {
|
|||||||
let moved = false;
|
let moved = false;
|
||||||
let facingRight = !this.sprite.flipX;
|
let facingRight = !this.sprite.flipX;
|
||||||
|
|
||||||
// WASD
|
// Determine inputs
|
||||||
|
let up = this.keys.up.isDown;
|
||||||
|
let down = this.keys.down.isDown;
|
||||||
|
let left = this.keys.left.isDown;
|
||||||
|
let right = this.keys.right.isDown;
|
||||||
|
|
||||||
|
// Check Virtual Joystick inputs (from UIScene)
|
||||||
|
const ui = this.scene.scene.get('UIScene');
|
||||||
|
if (ui && ui.virtualJoystick) {
|
||||||
|
if (ui.virtualJoystick.up) up = true;
|
||||||
|
if (ui.virtualJoystick.down) down = true;
|
||||||
|
if (ui.virtualJoystick.left) left = true;
|
||||||
|
if (ui.virtualJoystick.right) right = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply
|
||||||
let dx = 0;
|
let dx = 0;
|
||||||
let dy = 0;
|
let dy = 0;
|
||||||
|
|
||||||
if (this.keys.up.isDown) {
|
if (up) {
|
||||||
dx = -1; dy = 0;
|
dx = -1; dy = 0;
|
||||||
moved = true;
|
moved = true;
|
||||||
facingRight = false;
|
facingRight = false;
|
||||||
} else if (this.keys.down.isDown) {
|
} else if (down) {
|
||||||
dx = 1; dy = 0;
|
dx = 1; dy = 0;
|
||||||
moved = true;
|
moved = true;
|
||||||
facingRight = true;
|
facingRight = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.keys.left.isDown) {
|
if (left) {
|
||||||
dx = 0; dy = 1;
|
dx = 0; dy = 1;
|
||||||
moved = true;
|
moved = true;
|
||||||
facingRight = false;
|
facingRight = false;
|
||||||
} else if (this.keys.right.isDown) {
|
} else if (right) {
|
||||||
dx = 0; dy = -1;
|
dx = 0; dy = -1;
|
||||||
moved = true;
|
moved = true;
|
||||||
facingRight = true;
|
facingRight = true;
|
||||||
@@ -275,6 +301,11 @@ class Player {
|
|||||||
this.gridX = targetX;
|
this.gridX = targetX;
|
||||||
this.gridY = targetY;
|
this.gridY = targetY;
|
||||||
|
|
||||||
|
// Footstep Sound
|
||||||
|
if (this.scene.soundManager) {
|
||||||
|
this.scene.soundManager.playFootstep();
|
||||||
|
}
|
||||||
|
|
||||||
const targetScreen = this.iso.toScreen(targetX, targetY);
|
const targetScreen = this.iso.toScreen(targetX, targetY);
|
||||||
|
|
||||||
if (this.sprite.texture.key === 'player_walk') {
|
if (this.sprite.texture.key === 'player_walk') {
|
||||||
|
|||||||
@@ -51,8 +51,15 @@ class GameScene extends Phaser.Scene {
|
|||||||
this.terrainSystem.updateCulling(this.cameras.main);
|
this.terrainSystem.updateCulling(this.cameras.main);
|
||||||
|
|
||||||
// FAZA 14: Spawn Ruin (Town Project) at fixed location near player
|
// FAZA 14: Spawn Ruin (Town Project) at fixed location near player
|
||||||
console.log('🏚️ Spawning Ruin...');
|
console.log('🏚️ Spawning Ruin & Arena...');
|
||||||
this.terrainSystem.placeStructure(55, 55, 'ruin');
|
this.terrainSystem.placeStructure(55, 55, 'ruin');
|
||||||
|
this.terrainSystem.placeStructure(75, 75, 'arena');
|
||||||
|
|
||||||
|
// Initialize Pathfinding (Worker)
|
||||||
|
console.log('🗺️ Initializing Pathfinding...');
|
||||||
|
this.pathfinding = new PathfindingSystem(this);
|
||||||
|
this.pathfinding.updateGrid();
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Terrain system failed:", e);
|
console.error("Terrain system failed:", e);
|
||||||
}
|
}
|
||||||
@@ -61,8 +68,6 @@ class GameScene extends Phaser.Scene {
|
|||||||
console.log('👤 Initializing player...');
|
console.log('👤 Initializing player...');
|
||||||
this.player = new Player(this, 50, 50, this.terrainOffsetX, this.terrainOffsetY);
|
this.player = new Player(this, 50, 50, this.terrainOffsetX, this.terrainOffsetY);
|
||||||
|
|
||||||
// Dodaj 3 NPCje
|
|
||||||
console.log('🧟 Initializing NPCs...');
|
|
||||||
// Dodaj 3 NPCje (Mixed)
|
// Dodaj 3 NPCje (Mixed)
|
||||||
console.log('🧟 Initializing NPCs...');
|
console.log('🧟 Initializing NPCs...');
|
||||||
const npcTypes = ['zombie', 'villager', 'merchant'];
|
const npcTypes = ['zombie', 'villager', 'merchant'];
|
||||||
@@ -81,8 +86,8 @@ class GameScene extends Phaser.Scene {
|
|||||||
this.npcs.push(zombie);
|
this.npcs.push(zombie);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kamera sledi igralcu z izboljšanimi nastavitvami
|
// Kamera sledi igralcu
|
||||||
this.cameras.main.startFollow(this.player.sprite, true, 1.0, 1.0); // Instant follow (was 0.1)
|
this.cameras.main.startFollow(this.player.sprite, true, 1.0, 1.0);
|
||||||
|
|
||||||
// Nastavi deadzone (100px border)
|
// Nastavi deadzone (100px border)
|
||||||
this.cameras.main.setDeadzone(100, 100);
|
this.cameras.main.setDeadzone(100, 100);
|
||||||
@@ -99,11 +104,10 @@ class GameScene extends Phaser.Scene {
|
|||||||
// Kamera kontrole
|
// Kamera kontrole
|
||||||
this.setupCamera();
|
this.setupCamera();
|
||||||
|
|
||||||
// Initialize Time & Stats
|
// Initialize Systems
|
||||||
// Initialize Weather System (Unified: Time + DayNight + Weather)
|
|
||||||
console.log('🌦️ Initializing Unified Weather System...');
|
console.log('🌦️ Initializing Unified Weather System...');
|
||||||
this.weatherSystem = new WeatherSystem(this);
|
this.weatherSystem = new WeatherSystem(this);
|
||||||
this.timeSystem = this.weatherSystem; // Alias for compatibility with other systems (e.g. UIScene, Farming)
|
this.timeSystem = this.weatherSystem; // Alias
|
||||||
|
|
||||||
this.statsSystem = new StatsSystem(this);
|
this.statsSystem = new StatsSystem(this);
|
||||||
this.inventorySystem = new InventorySystem(this);
|
this.inventorySystem = new InventorySystem(this);
|
||||||
@@ -111,17 +115,24 @@ class GameScene extends Phaser.Scene {
|
|||||||
this.interactionSystem = new InteractionSystem(this);
|
this.interactionSystem = new InteractionSystem(this);
|
||||||
this.farmingSystem = new FarmingSystem(this);
|
this.farmingSystem = new FarmingSystem(this);
|
||||||
this.buildingSystem = new BuildingSystem(this);
|
this.buildingSystem = new BuildingSystem(this);
|
||||||
|
this.pathfinding = new Pathfinding(this);
|
||||||
// DayNightSystem removed (merged into WeatherSystem)
|
this.questSystem = new QuestSystem(this);
|
||||||
|
this.multiplayerSystem = new MultiplayerSystem(this);
|
||||||
|
|
||||||
// Initialize Sound Manager
|
// Initialize Sound Manager
|
||||||
console.log('🎵 Initializing Sound Manager...');
|
console.log('🎵 Initializing Sound Manager...');
|
||||||
this.soundManager = new SoundManager(this);
|
this.soundManager = new SoundManager(this);
|
||||||
|
this.soundManager.startMusic();
|
||||||
|
|
||||||
// Initialize Parallax System
|
// Initialize Parallax System
|
||||||
console.log('🌄 Initializing Parallax System...');
|
console.log('🌄 Initializing Parallax System...');
|
||||||
this.parallaxSystem = new ParallaxSystem(this);
|
this.parallaxSystem = new ParallaxSystem(this);
|
||||||
|
|
||||||
|
// Initialize Particle Effects
|
||||||
|
console.log('✨ Initializing Particle Effects...');
|
||||||
|
this.particleEffects = new ParticleEffects(this);
|
||||||
|
this.particleEffects.createFallingLeaves();
|
||||||
|
|
||||||
// Generate Item Sprites for UI
|
// Generate Item Sprites for UI
|
||||||
TextureGenerator.createItemSprites(this);
|
TextureGenerator.createItemSprites(this);
|
||||||
|
|
||||||
@@ -135,7 +146,11 @@ class GameScene extends Phaser.Scene {
|
|||||||
// Auto-load if available
|
// Auto-load if available
|
||||||
this.saveSystem.loadGame();
|
this.saveSystem.loadGame();
|
||||||
|
|
||||||
console.log('✅ GameScene ready - FAZA 18 (Crafting & AI)!');
|
// Debug Text
|
||||||
|
this.add.text(10, 10, 'NovaFarma Alpha v0.6', { font: '16px monospace', fill: '#ffffff' })
|
||||||
|
.setScrollFactor(0).setDepth(10000);
|
||||||
|
|
||||||
|
console.log('✅ GameScene ready - FAZA 20 (Full Features)!');
|
||||||
}
|
}
|
||||||
|
|
||||||
setupCamera() {
|
setupCamera() {
|
||||||
@@ -159,21 +174,11 @@ class GameScene extends Phaser.Scene {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Save/Load Keys
|
// Save/Load Keys
|
||||||
this.input.keyboard.on('keydown-F8', () => {
|
this.input.keyboard.on('keydown-F8', () => this.saveGame());
|
||||||
// Save
|
this.input.keyboard.on('keydown-F9', () => this.loadGame());
|
||||||
if (this.saveSystem) {
|
|
||||||
this.saveSystem.saveGame();
|
|
||||||
console.log('💾 Game Saved! (F8)');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.input.keyboard.on('keydown-F9', () => {
|
// Spawn Boss (Debug)
|
||||||
// Load
|
this.input.keyboard.on('keydown-K', () => this.spawnBoss());
|
||||||
if (this.saveSystem) {
|
|
||||||
this.saveSystem.loadGame();
|
|
||||||
console.log('📂 Game Loaded! (F9)');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Build Mode Keys
|
// Build Mode Keys
|
||||||
this.input.keyboard.on('keydown-B', () => {
|
this.input.keyboard.on('keydown-B', () => {
|
||||||
@@ -184,41 +189,73 @@ class GameScene extends Phaser.Scene {
|
|||||||
if (this.buildingSystem && this.buildingSystem.isBuildMode) this.buildingSystem.selectBuilding('fence');
|
if (this.buildingSystem && this.buildingSystem.isBuildMode) this.buildingSystem.selectBuilding('fence');
|
||||||
else if (this.scene.get('UIScene')) this.scene.get('UIScene').selectSlot(0);
|
else if (this.scene.get('UIScene')) this.scene.get('UIScene').selectSlot(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.input.keyboard.on('keydown-TWO', () => {
|
this.input.keyboard.on('keydown-TWO', () => {
|
||||||
if (this.buildingSystem && this.buildingSystem.isBuildMode) this.buildingSystem.selectBuilding('wall');
|
if (this.buildingSystem && this.buildingSystem.isBuildMode) this.buildingSystem.selectBuilding('wall');
|
||||||
else if (this.scene.get('UIScene')) this.scene.get('UIScene').selectSlot(1);
|
else if (this.scene.get('UIScene')) this.scene.get('UIScene').selectSlot(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.input.keyboard.on('keydown-THREE', () => {
|
this.input.keyboard.on('keydown-THREE', () => {
|
||||||
if (this.buildingSystem && this.buildingSystem.isBuildMode) this.buildingSystem.selectBuilding('house');
|
if (this.buildingSystem && this.buildingSystem.isBuildMode) this.buildingSystem.selectBuilding('house');
|
||||||
else if (this.scene.get('UIScene')) this.scene.get('UIScene').selectSlot(2);
|
else if (this.scene.get('UIScene')) this.scene.get('UIScene').selectSlot(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Soft Reset (F4) - Force Reload Page
|
// Soft Reset (F4)
|
||||||
this.input.keyboard.on('keydown-F4', () => {
|
this.input.keyboard.on('keydown-F4', () => window.location.reload());
|
||||||
console.log('🔄 Soft Reset Initiated (Force Reload)...');
|
|
||||||
window.location.reload();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mute Toggle (M key)
|
// Mute Toggle (M key)
|
||||||
this.input.keyboard.on('keydown-M', () => {
|
this.input.keyboard.on('keydown-M', () => {
|
||||||
if (this.soundManager) {
|
if (this.soundManager) this.soundManager.toggleMute();
|
||||||
this.soundManager.toggleMute();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
update(time, delta) {
|
update(time, delta) {
|
||||||
|
if (this.player) this.player.update(delta);
|
||||||
|
|
||||||
// Update Systems
|
// Update Systems
|
||||||
// TimeSystem update removed (handled by WeatherSystem)
|
|
||||||
if (this.statsSystem) this.statsSystem.update(delta);
|
if (this.statsSystem) this.statsSystem.update(delta);
|
||||||
if (this.lootSystem) this.lootSystem.update(delta); // Loot System Update
|
if (this.lootSystem) this.lootSystem.update(delta);
|
||||||
if (this.interactionSystem) this.interactionSystem.update(delta);
|
if (this.interactionSystem) this.interactionSystem.update(delta);
|
||||||
if (this.farmingSystem) this.farmingSystem.update(delta);
|
if (this.farmingSystem) this.farmingSystem.update(delta);
|
||||||
// DayNight update removed (handled by WeatherSystem)
|
if (this.questSystem) this.questSystem.update(delta);
|
||||||
if (this.weatherSystem) this.weatherSystem.update(delta);
|
if (this.multiplayerSystem) this.multiplayerSystem.update(delta);
|
||||||
|
|
||||||
// Update Parallax (foreground grass fading)
|
if (this.weatherSystem) {
|
||||||
|
this.weatherSystem.update(delta);
|
||||||
|
|
||||||
|
// Night Logic
|
||||||
|
if (this.weatherSystem.isNight()) {
|
||||||
|
const isHorde = this.weatherSystem.isHordeNight();
|
||||||
|
const spawnInterval = isHorde ? 2000 : 10000;
|
||||||
|
|
||||||
|
// Check for Horde Start Warning
|
||||||
|
if (isHorde && !this.hordeWarningShown) {
|
||||||
|
this.showHordeWarning();
|
||||||
|
this.hordeWarningShown = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.nightSpawnTimer) this.nightSpawnTimer = 0;
|
||||||
|
this.nightSpawnTimer += delta;
|
||||||
|
|
||||||
|
if (this.nightSpawnTimer > spawnInterval) {
|
||||||
|
this.nightSpawnTimer = 0;
|
||||||
|
this.spawnNightZombie();
|
||||||
|
if (isHorde) {
|
||||||
|
this.spawnNightZombie();
|
||||||
|
this.spawnNightZombie();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.hordeWarningShown = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NPC Update
|
||||||
|
for (const npc of this.npcs) {
|
||||||
|
npc.update(delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parallax
|
||||||
if (this.parallaxSystem && this.player) {
|
if (this.parallaxSystem && this.player) {
|
||||||
const playerPos = this.player.getPosition();
|
const playerPos = this.player.getPosition();
|
||||||
const screenPos = this.iso.toScreen(playerPos.x, playerPos.y);
|
const screenPos = this.iso.toScreen(playerPos.x, playerPos.y);
|
||||||
@@ -228,45 +265,34 @@ class GameScene extends Phaser.Scene {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update player
|
// Terrain Culling
|
||||||
if (this.player) {
|
|
||||||
this.player.update(delta);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update NPCs
|
|
||||||
for (const npc of this.npcs) {
|
|
||||||
npc.update(delta);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update Terrain Culling
|
|
||||||
if (this.terrainSystem) {
|
if (this.terrainSystem) {
|
||||||
this.terrainSystem.updateCulling(this.cameras.main);
|
this.terrainSystem.updateCulling(this.cameras.main);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update clouds
|
// Clouds
|
||||||
if (this.clouds) {
|
if (this.clouds) {
|
||||||
for (const cloud of this.clouds) {
|
for (const cloud of this.clouds) {
|
||||||
cloud.sprite.x += cloud.speed * (delta / 1000);
|
cloud.sprite.x += cloud.speed * (delta / 1000);
|
||||||
if (cloud.sprite.x > this.terrainOffsetX + 2000) { // Reset far right
|
if (cloud.sprite.x > this.terrainOffsetX + 2000) {
|
||||||
cloud.sprite.x = this.terrainOffsetX - 2000;
|
cloud.sprite.x = this.terrainOffsetX - 2000;
|
||||||
cloud.sprite.y = Phaser.Math.Between(0, 1000);
|
cloud.sprite.y = Phaser.Math.Between(0, 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send debug info to UI Scene
|
// Debug Info
|
||||||
if (this.player) {
|
if (this.player) {
|
||||||
const playerPos = this.player.getPosition();
|
const playerPos = this.player.getPosition();
|
||||||
const cam = this.cameras.main;
|
|
||||||
|
|
||||||
const uiScene = this.scene.get('UIScene');
|
const uiScene = this.scene.get('UIScene');
|
||||||
if (uiScene && uiScene.debugText) {
|
if (uiScene && uiScene.debugText) {
|
||||||
const activeCrops = this.terrainSystem && this.terrainSystem.cropsMap ? this.terrainSystem.cropsMap.size : 0;
|
const activeCrops = this.terrainSystem && this.terrainSystem.cropsMap ? this.terrainSystem.cropsMap.size : 0;
|
||||||
const dropsCount = this.lootSystem ? this.lootSystem.drops.length : 0;
|
const dropsCount = this.lootSystem ? this.lootSystem.drops.length : 0;
|
||||||
|
const conn = this.multiplayerSystem && this.multiplayerSystem.isConnected ? '🟢 Online' : '🔴 Offline';
|
||||||
|
|
||||||
uiScene.debugText.setText(
|
uiScene.debugText.setText(
|
||||||
`FAZA 11 - Building\n` +
|
`NovaFarma v0.6 [${conn}]\n` +
|
||||||
`[F5] Save | [F9] Load | [B] Build Mode\n` +
|
`[F5] Save | [F9] Load | [K] Boss\n` +
|
||||||
`Time: ${this.timeSystem ? this.timeSystem.gameTime.toFixed(1) : '?'}h\n` +
|
`Time: ${this.timeSystem ? this.timeSystem.gameTime.toFixed(1) : '?'}h\n` +
|
||||||
`Active Crops: ${activeCrops}\n` +
|
`Active Crops: ${activeCrops}\n` +
|
||||||
`Loot Drops: ${dropsCount}\n` +
|
`Loot Drops: ${dropsCount}\n` +
|
||||||
@@ -276,25 +302,76 @@ class GameScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
spawnNightZombie() {
|
||||||
|
if (!this.player || this.npcs.length > 50) return;
|
||||||
|
|
||||||
|
const playerPos = this.player.getPosition();
|
||||||
|
const angle = Math.random() * Math.PI * 2;
|
||||||
|
const distance = Phaser.Math.Between(15, 25);
|
||||||
|
|
||||||
|
const spawnX = Math.floor(playerPos.x + Math.cos(angle) * distance);
|
||||||
|
const spawnY = Math.floor(playerPos.y + Math.sin(angle) * distance);
|
||||||
|
|
||||||
|
if (spawnX < 0 || spawnX >= 100 || spawnY < 0 || spawnY >= 100) return;
|
||||||
|
if (Phaser.Math.Distance.Between(spawnX, spawnY, 20, 20) < 15) return;
|
||||||
|
|
||||||
|
const tile = this.terrainSystem.getTile(spawnX, spawnY);
|
||||||
|
if (tile && tile.type !== 'water') {
|
||||||
|
console.log(`🌑 Night Spawn: Zombie at ${spawnX},${spawnY}`);
|
||||||
|
const zombie = new NPC(this, spawnX, spawnY, this.terrainOffsetX, this.terrainOffsetY, 'zombie');
|
||||||
|
zombie.state = 'CHASE';
|
||||||
|
this.npcs.push(zombie);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showHordeWarning() {
|
||||||
|
console.log('🩸 BLOOD MOON RISING!');
|
||||||
|
if (this.soundManager) this.soundManager.playDeath();
|
||||||
|
|
||||||
|
const width = this.cameras.main.width;
|
||||||
|
const height = this.cameras.main.height;
|
||||||
|
|
||||||
|
const text = this.add.text(width / 2, height / 3, 'THE HORDE IS APPROACHING...', {
|
||||||
|
fontSize: '40px', fontFamily: 'Courier New', fill: '#ff0000', fontStyle: 'bold', stroke: '#000000', strokeThickness: 6
|
||||||
|
}).setOrigin(0.5).setScrollFactor(0).setDepth(10000);
|
||||||
|
|
||||||
|
this.tweens.add({
|
||||||
|
targets: text, scale: 1.2, duration: 500, yoyo: true, repeat: 5,
|
||||||
|
onComplete: () => {
|
||||||
|
this.tweens.add({
|
||||||
|
targets: text, alpha: 0, duration: 2000,
|
||||||
|
onComplete: () => text.destroy()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
createClouds() {
|
createClouds() {
|
||||||
if (!this.textures.exists('cloud')) TextureGenerator.createCloudSprite(this, 'cloud');
|
if (!this.textures.exists('cloud')) TextureGenerator.createCloudSprite(this, 'cloud');
|
||||||
|
|
||||||
this.clouds = [];
|
this.clouds = [];
|
||||||
console.log('☁️ Creating parallax clouds...');
|
console.log('☁️ Creating parallax clouds...');
|
||||||
for (let i = 0; i < 8; i++) {
|
for (let i = 0; i < 8; i++) {
|
||||||
const x = Phaser.Math.Between(-1000, 3000);
|
const x = Phaser.Math.Between(-1000, 3000);
|
||||||
const y = Phaser.Math.Between(-500, 1500);
|
const y = Phaser.Math.Between(-500, 1500);
|
||||||
|
|
||||||
const cloud = this.add.sprite(x, y, 'cloud');
|
const cloud = this.add.sprite(x, y, 'cloud');
|
||||||
cloud.setAlpha(0.4);
|
cloud.setAlpha(0.4).setScrollFactor(0.2).setDepth(2000).setScale(Phaser.Math.FloatBetween(2, 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) });
|
this.clouds.push({ sprite: cloud, speed: Phaser.Math.FloatBetween(10, 30) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
spawnBoss() {
|
||||||
|
if (!this.player) return;
|
||||||
|
console.log('👑 SPANWING ZOMBIE KING!');
|
||||||
|
const playerPos = this.player.getPosition();
|
||||||
|
const spawnX = Math.floor(playerPos.x + 8);
|
||||||
|
const spawnY = Math.floor(playerPos.y + 8);
|
||||||
|
const boss = new Boss(this, spawnX, spawnY);
|
||||||
|
boss.state = 'CHASE';
|
||||||
|
this.npcs.push(boss);
|
||||||
|
this.showHordeWarning();
|
||||||
|
this.events.emit('show-floating-text', { x: this.player.x, y: this.player.y - 100, text: "THE KING HAS ARRIVED!", color: '#AA00FF' });
|
||||||
|
}
|
||||||
|
|
||||||
saveGame() {
|
saveGame() {
|
||||||
if (this.saveSystem) this.saveSystem.saveGame();
|
if (this.saveSystem) this.saveSystem.saveGame();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ class PreloadScene extends Phaser.Scene {
|
|||||||
preload() {
|
preload() {
|
||||||
console.log('⏳ PreloadScene: Loading assets...');
|
console.log('⏳ PreloadScene: Loading assets...');
|
||||||
|
|
||||||
|
this.createLoadingBar();
|
||||||
|
|
||||||
// Load ALL custom sprites
|
// Load ALL custom sprites
|
||||||
this.load.image('player_sprite', 'assets/player_sprite.png');
|
this.load.image('player_sprite', 'assets/player_sprite.png');
|
||||||
this.load.image('zombie_sprite', 'assets/zombie_sprite.png');
|
this.load.image('zombie_sprite', 'assets/zombie_sprite.png');
|
||||||
@@ -27,6 +29,30 @@ class PreloadScene extends Phaser.Scene {
|
|||||||
this.load.image('objects_pack2', 'assets/objects_pack2.png');
|
this.load.image('objects_pack2', 'assets/objects_pack2.png');
|
||||||
this.load.image('trees_vegetation', 'assets/trees_vegetation.png');
|
this.load.image('trees_vegetation', 'assets/trees_vegetation.png');
|
||||||
|
|
||||||
|
// User-uploaded pixel art assets (original)
|
||||||
|
this.load.image('flowers', 'assets/flowers.png');
|
||||||
|
this.load.image('tree_green', 'assets/tree_green.png');
|
||||||
|
this.load.image('tree_blue', 'assets/tree_blue.png');
|
||||||
|
this.load.image('tree_dead', 'assets/tree_dead.png');
|
||||||
|
this.load.image('rock_asset', 'assets/rock_asset.png');
|
||||||
|
|
||||||
|
// NEW transparent tree/rock assets
|
||||||
|
this.load.image('tree_blue_new', 'assets/tree_blue_new.png');
|
||||||
|
this.load.image('tree_green_new', 'assets/tree_green_new.png');
|
||||||
|
this.load.image('rock_1', 'assets/rock_1.png');
|
||||||
|
this.load.image('rock_2', 'assets/rock_2.png');
|
||||||
|
this.load.image('tree_dead_new', 'assets/tree_dead_new.png');
|
||||||
|
this.load.image('flowers_new', 'assets/flowers_new.png');
|
||||||
|
this.load.image('hill_sprite', 'assets/hill_sprite.png');
|
||||||
|
this.load.image('fence', 'assets/fence.png');
|
||||||
|
this.load.image('gravestone', 'assets/gravestone.png');
|
||||||
|
|
||||||
|
// Voxel stil asset-i (2.5D)
|
||||||
|
this.load.image('tree_voxel_green', 'assets/tree_voxel_green.png');
|
||||||
|
this.load.image('tree_voxel_blue', 'assets/tree_voxel_blue.png');
|
||||||
|
this.load.image('tree_voxel_dead', 'assets/tree_voxel_dead.png');
|
||||||
|
this.load.image('rock_voxel', 'assets/rock_voxel.png');
|
||||||
|
|
||||||
// Wait for load completion then process transparency
|
// Wait for load completion then process transparency
|
||||||
this.load.once('complete', () => {
|
this.load.once('complete', () => {
|
||||||
this.processAllTransparency();
|
this.processAllTransparency();
|
||||||
@@ -69,7 +95,28 @@ class PreloadScene extends Phaser.Scene {
|
|||||||
'grass_sprite',
|
'grass_sprite',
|
||||||
'leaf_sprite',
|
'leaf_sprite',
|
||||||
'wheat_sprite',
|
'wheat_sprite',
|
||||||
'stone_texture'
|
'stone_texture',
|
||||||
|
// New pixel art assets
|
||||||
|
'flowers',
|
||||||
|
'tree_green',
|
||||||
|
'tree_blue',
|
||||||
|
'tree_dead',
|
||||||
|
'rock_asset',
|
||||||
|
// NEW transparent assets
|
||||||
|
'tree_blue_new',
|
||||||
|
'tree_green_new',
|
||||||
|
'rock_1',
|
||||||
|
'rock_2',
|
||||||
|
'tree_dead_new',
|
||||||
|
'flowers_new',
|
||||||
|
'hill_sprite',
|
||||||
|
'fence',
|
||||||
|
'gravestone',
|
||||||
|
// Voxel stil
|
||||||
|
'tree_voxel_green',
|
||||||
|
'tree_voxel_blue',
|
||||||
|
'tree_voxel_dead',
|
||||||
|
'rock_voxel'
|
||||||
];
|
];
|
||||||
|
|
||||||
spritesToProcess.forEach(spriteKey => {
|
spritesToProcess.forEach(spriteKey => {
|
||||||
@@ -127,6 +174,42 @@ class PreloadScene extends Phaser.Scene {
|
|||||||
this.textures.addCanvas(spriteKey, canvas);
|
this.textures.addCanvas(spriteKey, canvas);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createLoadingBar() {
|
||||||
|
const width = this.cameras.main.width;
|
||||||
|
const height = this.cameras.main.height;
|
||||||
|
|
||||||
|
const progressBar = this.add.graphics();
|
||||||
|
const progressBox = this.add.graphics();
|
||||||
|
progressBox.fillStyle(0x222222, 0.8);
|
||||||
|
progressBox.fillRect(width / 2 - 160, height / 2 - 25, 320, 50);
|
||||||
|
|
||||||
|
const loadingText = this.add.text(width / 2, height / 2 - 50, 'Loading NovaFarma...', {
|
||||||
|
font: '20px Courier New',
|
||||||
|
fill: '#ffffff'
|
||||||
|
});
|
||||||
|
loadingText.setOrigin(0.5, 0.5);
|
||||||
|
|
||||||
|
const percentText = this.add.text(width / 2, height / 2, '0%', {
|
||||||
|
font: '18px Courier New',
|
||||||
|
fill: '#ffffff'
|
||||||
|
});
|
||||||
|
percentText.setOrigin(0.5, 0.5);
|
||||||
|
|
||||||
|
this.load.on('progress', (value) => {
|
||||||
|
percentText.setText(parseInt(value * 100) + '%');
|
||||||
|
progressBar.clear();
|
||||||
|
progressBar.fillStyle(0x00ff00, 1); // Matrix Green
|
||||||
|
progressBar.fillRect(width / 2 - 150, height / 2 - 15, 300 * value, 30);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.load.on('complete', () => {
|
||||||
|
progressBar.destroy();
|
||||||
|
progressBox.destroy();
|
||||||
|
loadingText.destroy();
|
||||||
|
percentText.destroy();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
create() {
|
create() {
|
||||||
console.log('✅ PreloadScene: Assets loaded!');
|
console.log('✅ PreloadScene: Assets loaded!');
|
||||||
window.gameState.currentScene = 'PreloadScene';
|
window.gameState.currentScene = 'PreloadScene';
|
||||||
|
|||||||
@@ -11,32 +11,31 @@ class StoryScene extends Phaser.Scene {
|
|||||||
this.add.rectangle(0, 0, width, height, 0x000000).setOrigin(0);
|
this.add.rectangle(0, 0, width, height, 0x000000).setOrigin(0);
|
||||||
|
|
||||||
const storyText =
|
const storyText =
|
||||||
`Leto 2084.
|
`LETO 2084.
|
||||||
Svet, kot smo ga poznali, je izginil.
|
SVET JE PADEL V TEMO.
|
||||||
|
|
||||||
Virus "Zmaj-Volka" je spremenil človeštvo.
|
Virus je večino spremenil v pošasti.
|
||||||
Mesta so ruševine. Narava je divja.
|
Mesta so grobnice.
|
||||||
|
|
||||||
Toda ti si drugačen.
|
Toda našel si upanje.
|
||||||
Preživel si napad. Okužen, a imun.
|
Zapuščeno kmetijo na robu divjine.
|
||||||
Si HIBRID.
|
Zadnje varno zatočišče.
|
||||||
|
|
||||||
Zombiji te ne napadajo... čutijo te.
|
|
||||||
Zanje si ALFA.
|
|
||||||
|
|
||||||
Tvoja naloga:
|
Tvoja naloga:
|
||||||
1. Najdi izgubljeno sestro.
|
1. Obnovi kmetijo in preživi.
|
||||||
2. Maščuj starše.
|
2. Zgradi obrambo pred hordo.
|
||||||
3. Obnovi civilizacijo iz pepela.
|
3. Najdi zdravilo in reši svet.
|
||||||
|
|
||||||
Dobrodošel v KRVAVI ŽETVI.`;
|
Pripravi se... NOČ PRIHAJA.`;
|
||||||
|
|
||||||
const textObj = this.add.text(width / 2, height + 100, storyText, {
|
const textObj = this.add.text(width / 2, height, storyText, {
|
||||||
fontFamily: 'Courier New',
|
fontFamily: 'Courier New',
|
||||||
fontSize: '24px',
|
fontSize: '28px',
|
||||||
fill: '#00ff41',
|
fill: '#00ff41',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
lineSpacing: 10
|
lineSpacing: 15,
|
||||||
|
stroke: '#000000',
|
||||||
|
strokeThickness: 4
|
||||||
});
|
});
|
||||||
textObj.setOrigin(0.5, 0);
|
textObj.setOrigin(0.5, 0);
|
||||||
|
|
||||||
@@ -44,7 +43,7 @@ Dobrodošel v KRVAVI ŽETVI.`;
|
|||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
targets: textObj,
|
targets: textObj,
|
||||||
y: 50,
|
y: 50,
|
||||||
duration: 10000, // 10s scroll
|
duration: 15000, // 15s scroll
|
||||||
ease: 'Linear',
|
ease: 'Linear',
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
this.time.delayedCall(2000, () => {
|
this.time.delayedCall(2000, () => {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class UIScene extends Phaser.Scene {
|
|||||||
this.createStatusBars();
|
this.createStatusBars();
|
||||||
this.createInventoryBar();
|
this.createInventoryBar();
|
||||||
this.createGoldDisplay();
|
this.createGoldDisplay();
|
||||||
|
this.createVirtualJoystick();
|
||||||
this.createClock();
|
this.createClock();
|
||||||
// this.createDebugInfo();
|
// this.createDebugInfo();
|
||||||
this.createSettingsButton();
|
this.createSettingsButton();
|
||||||
@@ -471,7 +472,20 @@ class UIScene extends Phaser.Scene {
|
|||||||
if (!this.buildMenuContainer) {
|
if (!this.buildMenuContainer) {
|
||||||
this.createBuildMenuInfo();
|
this.createBuildMenuInfo();
|
||||||
}
|
}
|
||||||
this.buildMenuContainer.setVisible(isVisible);
|
|
||||||
|
if (isVisible) {
|
||||||
|
this.buildMenuContainer.setVisible(true);
|
||||||
|
this.buildMenuContainer.y = -100; // Start off-screen
|
||||||
|
this.tweens.add({
|
||||||
|
targets: this.buildMenuContainer,
|
||||||
|
y: 100, // Target pos
|
||||||
|
duration: 300,
|
||||||
|
ease: 'Back.easeOut'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Slide out (optional) or just hide
|
||||||
|
this.buildMenuContainer.setVisible(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createBuildMenuInfo() {
|
createBuildMenuInfo() {
|
||||||
@@ -778,4 +792,168 @@ class UIScene extends Phaser.Scene {
|
|||||||
});
|
});
|
||||||
this.settingsContainer.add(hitArea);
|
this.settingsContainer.add(hitArea);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateQuestTracker(quest) {
|
||||||
|
if (!this.questContainer) {
|
||||||
|
this.createQuestTracker();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!quest) {
|
||||||
|
this.questContainer.setVisible(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.questContainer.setVisible(true);
|
||||||
|
this.questTitle.setText(quest.title.toUpperCase());
|
||||||
|
|
||||||
|
let objText = '';
|
||||||
|
quest.objectives.forEach(obj => {
|
||||||
|
const status = obj.done ? '✅' : '⬜';
|
||||||
|
let desc = '';
|
||||||
|
if (obj.type === 'collect') desc = `${obj.item}: ${obj.current}/${obj.amount}`;
|
||||||
|
else if (obj.type === 'action') desc = `${obj.action}: ${obj.current}/${obj.amount}`;
|
||||||
|
else if (obj.type === 'kill') desc = `Slay ${obj.target}: ${obj.current}/${obj.amount}`;
|
||||||
|
|
||||||
|
objText += `${status} ${desc}\n`;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.questObjectives.setText(objText);
|
||||||
|
}
|
||||||
|
|
||||||
|
createQuestTracker() {
|
||||||
|
const x = this.width - 240;
|
||||||
|
const y = 20;
|
||||||
|
|
||||||
|
this.questContainer = this.add.container(x, y);
|
||||||
|
|
||||||
|
// BG
|
||||||
|
const bg = this.add.graphics();
|
||||||
|
bg.fillStyle(0x000000, 0.6);
|
||||||
|
bg.fillRect(0, 0, 220, 100);
|
||||||
|
bg.lineStyle(2, 0xffaa00, 0.8);
|
||||||
|
bg.strokeRect(0, 0, 220, 100);
|
||||||
|
this.questContainer.add(bg);
|
||||||
|
this.questTrackerBg = bg; // ref to resize later if needed
|
||||||
|
|
||||||
|
// Title Header
|
||||||
|
const header = this.add.text(10, 5, 'CURRENT QUEST', { fontSize: '10px', fill: '#aaaaaa' });
|
||||||
|
this.questContainer.add(header);
|
||||||
|
|
||||||
|
// Title
|
||||||
|
this.questTitle = this.add.text(10, 20, 'Quest Title', {
|
||||||
|
fontSize: '16px', fill: '#ffaa00', fontStyle: 'bold'
|
||||||
|
});
|
||||||
|
this.questContainer.add(this.questTitle);
|
||||||
|
|
||||||
|
// Objectives
|
||||||
|
this.questObjectives = this.add.text(10, 45, 'Objectives...', {
|
||||||
|
fontSize: '14px', fill: '#ffffff', lineSpacing: 5
|
||||||
|
});
|
||||||
|
this.questContainer.add(this.questObjectives);
|
||||||
|
}
|
||||||
|
|
||||||
|
createVirtualJoystick() {
|
||||||
|
const x = 120;
|
||||||
|
const y = this.height - 120;
|
||||||
|
// Warning: this.height might not be updated on resize? Use this.scale.height or just initial.
|
||||||
|
// UIScene is usually overlay, so simple coords ok. But resize needs handling.
|
||||||
|
|
||||||
|
const r = 60;
|
||||||
|
|
||||||
|
// Visuals
|
||||||
|
this.joyBase = this.add.circle(x, y, r, 0xffffff, 0.1).setScrollFactor(0).setDepth(2000).setInteractive();
|
||||||
|
this.joyStick = this.add.circle(x, y, r / 2, 0xffffff, 0.5).setScrollFactor(0).setDepth(2001);
|
||||||
|
|
||||||
|
this.virtualJoystick = { up: false, down: false, left: false, right: false };
|
||||||
|
this.joyDragging = false;
|
||||||
|
|
||||||
|
// Events
|
||||||
|
this.input.on('pointermove', (pointer) => {
|
||||||
|
if (!this.joyDragging) return;
|
||||||
|
this.updateJoystick(pointer.x, pointer.y, x, y, r);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.input.on('pointerup', () => {
|
||||||
|
if (this.joyDragging) {
|
||||||
|
this.joyDragging = false;
|
||||||
|
this.joyStick.setPosition(x, y);
|
||||||
|
this.virtualJoystick = { up: false, down: false, left: false, right: false };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.joyBase.on('pointerdown', (pointer) => {
|
||||||
|
this.joyDragging = true;
|
||||||
|
this.updateJoystick(pointer.x, pointer.y, x, y, r);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateJoystick(px, py, cx, cy, r) {
|
||||||
|
const angle = Phaser.Math.Angle.Between(cx, cy, px, py);
|
||||||
|
const dist = Math.min(r, Phaser.Math.Distance.Between(cx, cy, px, py));
|
||||||
|
|
||||||
|
const sx = cx + Math.cos(angle) * dist;
|
||||||
|
const sy = cy + Math.sin(angle) * dist;
|
||||||
|
|
||||||
|
this.joyStick.setPosition(sx, sy);
|
||||||
|
|
||||||
|
// Normalize angle to degrees
|
||||||
|
let deg = Phaser.Math.RadToDeg(angle);
|
||||||
|
|
||||||
|
this.virtualJoystick = { up: false, down: false, left: false, right: false };
|
||||||
|
|
||||||
|
// Mapping to Isometric direction keys
|
||||||
|
// UP Key (Top-Left on screen) -> Angle -135 (-180 to -90)
|
||||||
|
// RIGHT Key (Top-Right on screen) -> Angle -45 (-90 to 0)
|
||||||
|
// DOWN Key (Bottom-Right on screen) -> Angle 45 (0 to 90)
|
||||||
|
// LEFT Key (Bottom-Left on screen) -> Angle 135 (90 to 180)
|
||||||
|
|
||||||
|
if (deg > -170 && deg <= -80) this.virtualJoystick.up = true; // Tuned slightly
|
||||||
|
else if (deg > -80 && deg <= 10) this.virtualJoystick.right = true;
|
||||||
|
else if (deg > 10 && deg <= 100) this.virtualJoystick.down = true;
|
||||||
|
else this.virtualJoystick.left = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
showQuestDialog(quest, onAccept) {
|
||||||
|
const width = 400;
|
||||||
|
const height = 250;
|
||||||
|
const x = this.cameras.main.centerX;
|
||||||
|
const y = this.cameras.main.centerY;
|
||||||
|
|
||||||
|
const container = this.add.container(x, y);
|
||||||
|
container.setDepth(5000);
|
||||||
|
|
||||||
|
const bg = this.add.rectangle(0, 0, width, height, 0x222222, 0.95);
|
||||||
|
bg.setStrokeStyle(4, 0x444444);
|
||||||
|
|
||||||
|
const title = this.add.text(0, -90, quest.title.toUpperCase(), { fontSize: '24px', fontStyle: 'bold', color: '#ffcc00' }).setOrigin(0.5);
|
||||||
|
const desc = this.add.text(0, -30, quest.description, { fontSize: '16px', color: '#dddddd', align: 'center', wordWrap: { width: width - 60 } }).setOrigin(0.5);
|
||||||
|
|
||||||
|
let rText = "Reward: ";
|
||||||
|
if (quest.reward.gold) rText += `${quest.reward.gold} G `;
|
||||||
|
if (quest.reward.item) rText += `+ ${quest.reward.amount || 1} ${quest.reward.item}`;
|
||||||
|
|
||||||
|
const reward = this.add.text(0, 40, rText, { fontSize: '16px', color: '#00ff00', fontStyle: 'italic' }).setOrigin(0.5);
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
const btnAccept = this.add.rectangle(-70, 90, 120, 40, 0x228B22).setInteractive({ useHandCursor: true });
|
||||||
|
const txtAccept = this.add.text(-70, 90, 'ACCEPT', { fontSize: '18px', fontStyle: 'bold' }).setOrigin(0.5);
|
||||||
|
|
||||||
|
const btnClose = this.add.rectangle(70, 90, 120, 40, 0x8B0000).setInteractive({ useHandCursor: true });
|
||||||
|
const txtClose = this.add.text(70, 90, 'DECLINE', { fontSize: '18px', fontStyle: 'bold' }).setOrigin(0.5);
|
||||||
|
|
||||||
|
btnAccept.on('pointerdown', () => {
|
||||||
|
onAccept();
|
||||||
|
container.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
btnClose.on('pointerdown', () => {
|
||||||
|
container.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
container.add([bg, title, desc, reward, btnAccept, txtAccept, btnClose, txtClose]);
|
||||||
|
|
||||||
|
// Appear anim
|
||||||
|
container.setScale(0);
|
||||||
|
this.tweens.add({ targets: container, scale: 1, duration: 200, ease: 'Back.out' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,6 +93,16 @@ class BuildingSystem {
|
|||||||
const success = terrain.placeStructure(gridX, gridY, `struct_${this.selectedBuilding}`);
|
const success = terrain.placeStructure(gridX, gridY, `struct_${this.selectedBuilding}`);
|
||||||
if (success) {
|
if (success) {
|
||||||
this.showFloatingText(`Built ${building.name}!`, gridX, gridY, '#00FF00');
|
this.showFloatingText(`Built ${building.name}!`, gridX, gridY, '#00FF00');
|
||||||
|
|
||||||
|
// Build Sound
|
||||||
|
if (this.scene.soundManager) {
|
||||||
|
this.scene.soundManager.playBuild();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quest Tracking
|
||||||
|
if (this.scene.questSystem) {
|
||||||
|
this.scene.questSystem.trackAction(`build_${this.selectedBuilding}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -67,12 +67,25 @@ class FarmingSystem {
|
|||||||
maxTime: 10 // Seconds per stage?
|
maxTime: 10 // Seconds per stage?
|
||||||
};
|
};
|
||||||
terrain.addCrop(x, y, cropData);
|
terrain.addCrop(x, y, cropData);
|
||||||
|
|
||||||
|
// Plant Sound
|
||||||
|
if (this.scene.soundManager) {
|
||||||
|
this.scene.soundManager.playPlant();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quest Tracking
|
||||||
|
if (this.scene.questSystem) this.scene.questSystem.trackAction('plant');
|
||||||
}
|
}
|
||||||
|
|
||||||
harvest(x, y) {
|
harvest(x, y) {
|
||||||
const terrain = this.scene.terrainSystem;
|
const terrain = this.scene.terrainSystem;
|
||||||
console.log('🌾 Harvesting!');
|
console.log('🌾 Harvesting!');
|
||||||
|
|
||||||
|
// Harvest Sound
|
||||||
|
if (this.scene.soundManager) {
|
||||||
|
this.scene.soundManager.playHarvest();
|
||||||
|
}
|
||||||
|
|
||||||
// Spawn loot
|
// Spawn loot
|
||||||
if (this.scene.interactionSystem) {
|
if (this.scene.interactionSystem) {
|
||||||
this.scene.interactionSystem.spawnLoot(x, y, 'wheat');
|
this.scene.interactionSystem.spawnLoot(x, y, 'wheat');
|
||||||
|
|||||||
@@ -150,6 +150,11 @@ class InteractionSystem {
|
|||||||
decor.hp -= damage;
|
decor.hp -= damage;
|
||||||
this.showFloatingText(`${-damage}`, gridX, gridY, '#ffaaaa');
|
this.showFloatingText(`${-damage}`, gridX, gridY, '#ffaaaa');
|
||||||
|
|
||||||
|
// Chop Sound
|
||||||
|
if (this.scene.soundManager) {
|
||||||
|
this.scene.soundManager.playChop();
|
||||||
|
}
|
||||||
|
|
||||||
if (decor.hp <= 0) {
|
if (decor.hp <= 0) {
|
||||||
const type = this.scene.terrainSystem.removeDecoration(gridX, gridY);
|
const type = this.scene.terrainSystem.removeDecoration(gridX, gridY);
|
||||||
// Loot logic via LootSystem
|
// Loot logic via LootSystem
|
||||||
|
|||||||
@@ -68,9 +68,25 @@ class InventorySystem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.updateUI();
|
this.updateUI();
|
||||||
|
this.updateUI();
|
||||||
return false; // Not enough items
|
return false; // Not enough items
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getItemCount(type) {
|
||||||
|
let total = 0;
|
||||||
|
for (const slot of this.slots) {
|
||||||
|
if (slot && slot.type === type) {
|
||||||
|
total += slot.count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
addGold(amount) {
|
||||||
|
this.gold += amount;
|
||||||
|
this.updateUI();
|
||||||
|
}
|
||||||
|
|
||||||
updateUI() {
|
updateUI() {
|
||||||
const uiScene = this.scene.scene.get('UIScene');
|
const uiScene = this.scene.scene.get('UIScene');
|
||||||
if (uiScene) {
|
if (uiScene) {
|
||||||
|
|||||||
@@ -84,10 +84,15 @@ class LootSystem {
|
|||||||
const leftover = this.scene.inventorySystem.addItem(drop.type, drop.count);
|
const leftover = this.scene.inventorySystem.addItem(drop.type, drop.count);
|
||||||
|
|
||||||
if (leftover === 0) {
|
if (leftover === 0) {
|
||||||
// Success
|
// Success - Play Sound
|
||||||
this.scene.sound.play('pickup_sound')
|
if (this.scene.soundManager) {
|
||||||
// (Assuming sound exists, if not it will just warn silently or fail)
|
this.scene.soundManager.playPickup();
|
||||||
// Actually, let's skip sound call if not sure to avoid error spam
|
}
|
||||||
|
|
||||||
|
// Sparkle Effect
|
||||||
|
if (this.scene.particleEffects) {
|
||||||
|
this.scene.particleEffects.sparkle(drop.x, drop.y);
|
||||||
|
}
|
||||||
|
|
||||||
// Float text effect
|
// Float text effect
|
||||||
this.showFloatingText(`+${drop.count} ${drop.type}`, drop.x, drop.y);
|
this.showFloatingText(`+${drop.count} ${drop.type}`, drop.x, drop.y);
|
||||||
|
|||||||
129
src/systems/MultiplayerSystem.js
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
class MultiplayerSystem {
|
||||||
|
constructor(scene) {
|
||||||
|
this.scene = scene;
|
||||||
|
this.socket = null;
|
||||||
|
this.otherPlayers = {}; // Map socketId -> Sprite
|
||||||
|
this.isConnected = false;
|
||||||
|
|
||||||
|
// Try to connect
|
||||||
|
this.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
if (typeof io === 'undefined') {
|
||||||
|
console.warn('⚠️ Socket.IO not found. Multiplayer disabled.');
|
||||||
|
console.warn('Please run: npm install socket.io-client OR include CDN.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🌐 Connecting to Multiplayer Server...');
|
||||||
|
// Connect to localhost:3000
|
||||||
|
this.socket = io('http://localhost:3000');
|
||||||
|
|
||||||
|
this.socket.on('connect', () => {
|
||||||
|
console.log('✅ Connected to Server! ID:', this.socket.id);
|
||||||
|
this.isConnected = true;
|
||||||
|
|
||||||
|
// Send initial pos
|
||||||
|
if (this.scene.player) {
|
||||||
|
const pos = this.scene.player.getPosition();
|
||||||
|
this.socket.emit('playerMovement', {
|
||||||
|
x: pos.x,
|
||||||
|
y: pos.y,
|
||||||
|
anim: 'idle',
|
||||||
|
flipX: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on('currentPlayers', (players) => {
|
||||||
|
Object.keys(players).forEach((id) => {
|
||||||
|
if (id === this.socket.id) return;
|
||||||
|
this.addOtherPlayer(players[id]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on('newPlayer', (playerInfo) => {
|
||||||
|
this.addOtherPlayer(playerInfo);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on('playerDisconnected', (playerId) => {
|
||||||
|
this.removeOtherPlayer(playerId);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on('playerMoved', (playerInfo) => {
|
||||||
|
if (this.otherPlayers[playerInfo.id]) {
|
||||||
|
const sprite = this.otherPlayers[playerInfo.id];
|
||||||
|
// Update target pos for interpolation (TODO: logic)
|
||||||
|
// For now direct teleport
|
||||||
|
|
||||||
|
// Convert grid to screen
|
||||||
|
const iso = new IsometricUtils(48, 24);
|
||||||
|
const screen = iso.toScreen(playerInfo.x, playerInfo.y);
|
||||||
|
|
||||||
|
sprite.setPosition(screen.x + this.scene.terrainOffsetX, screen.y + this.scene.terrainOffsetY);
|
||||||
|
sprite.setDepth(sprite.y);
|
||||||
|
|
||||||
|
// Anim/Flip logic could go here
|
||||||
|
if (playerInfo.flipX !== undefined) sprite.setFlipX(playerInfo.flipX);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on('worldAction', (action) => {
|
||||||
|
// Handle world syncing
|
||||||
|
if (action.type === 'build' && this.scene.buildingSystem) {
|
||||||
|
// Hacky: place building remotely
|
||||||
|
// this.scene.terrainSystem.placeStructure(...)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addOtherPlayer(playerInfo) {
|
||||||
|
if (this.otherPlayers[playerInfo.id]) return;
|
||||||
|
|
||||||
|
console.log('👤 New Player Joined:', playerInfo.id);
|
||||||
|
|
||||||
|
// Use player sprite
|
||||||
|
const iso = new IsometricUtils(48, 24);
|
||||||
|
const screen = iso.toScreen(playerInfo.x, playerInfo.y);
|
||||||
|
|
||||||
|
const sprite = this.scene.add.sprite(
|
||||||
|
screen.x + this.scene.terrainOffsetX,
|
||||||
|
screen.y + this.scene.terrainOffsetY,
|
||||||
|
'player' // or player_idle
|
||||||
|
);
|
||||||
|
sprite.setOrigin(0.5, 1);
|
||||||
|
sprite.setScale(0.3); // Same as local player
|
||||||
|
|
||||||
|
// Add name tag
|
||||||
|
const text = this.scene.add.text(0, -50, 'Player', { fontSize: '12px', fill: '#ffffff' });
|
||||||
|
text.setOrigin(0.5);
|
||||||
|
// Container? For now just sprite, text handling is complex
|
||||||
|
|
||||||
|
this.otherPlayers[playerInfo.id] = sprite;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeOtherPlayer(playerId) {
|
||||||
|
if (this.otherPlayers[playerId]) {
|
||||||
|
console.log('👋 Player Left:', playerId);
|
||||||
|
this.otherPlayers[playerId].destroy();
|
||||||
|
delete this.otherPlayers[playerId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update(delta) {
|
||||||
|
if (!this.isConnected || !this.socket || !this.scene.player) return;
|
||||||
|
|
||||||
|
// Rate limit: send 10 times second? Or every frame?
|
||||||
|
// Let's send only if moved
|
||||||
|
const player = this.scene.player;
|
||||||
|
if (player.isMoving) {
|
||||||
|
this.socket.emit('playerMovement', {
|
||||||
|
x: player.gridX,
|
||||||
|
y: player.gridY,
|
||||||
|
anim: 'walk',
|
||||||
|
flipX: player.sprite.flipX // Accessing internal sprite
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
105
src/systems/ParticleEffects.js
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
// ParticleEffects System
|
||||||
|
// Proceduralno generiranje particle efektov
|
||||||
|
class ParticleEffects {
|
||||||
|
constructor(scene) {
|
||||||
|
this.scene = scene;
|
||||||
|
this.createParticleTextures();
|
||||||
|
}
|
||||||
|
|
||||||
|
createParticleTextures() {
|
||||||
|
// Blood particle
|
||||||
|
if (!this.scene.textures.exists('blood_particle')) {
|
||||||
|
const graphics = this.scene.make.graphics({ x: 0, y: 0, add: false });
|
||||||
|
graphics.fillStyle(0xff0000, 1);
|
||||||
|
graphics.fillCircle(2, 2, 2);
|
||||||
|
graphics.generateTexture('blood_particle', 4, 4);
|
||||||
|
graphics.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leaf particle
|
||||||
|
if (!this.scene.textures.exists('leaf_particle')) {
|
||||||
|
const graphics = this.scene.make.graphics({ x: 0, y: 0, add: false });
|
||||||
|
graphics.fillStyle(0x44aa44, 1);
|
||||||
|
graphics.fillRect(0, 0, 4, 6);
|
||||||
|
graphics.generateTexture('leaf_particle', 4, 6);
|
||||||
|
graphics.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sparkle particle
|
||||||
|
if (!this.scene.textures.exists('sparkle_particle')) {
|
||||||
|
const graphics = this.scene.make.graphics({ x: 0, y: 0, add: false });
|
||||||
|
graphics.fillStyle(0xffff00, 1);
|
||||||
|
graphics.fillCircle(2, 2, 2);
|
||||||
|
graphics.generateTexture('sparkle_particle', 4, 4);
|
||||||
|
graphics.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blood Splash on damage
|
||||||
|
bloodSplash(x, y) {
|
||||||
|
const emitter = this.scene.add.particles(x, y, 'blood_particle', {
|
||||||
|
speed: { min: 50, max: 150 },
|
||||||
|
angle: { min: 0, max: 360 },
|
||||||
|
scale: { start: 1, end: 0 },
|
||||||
|
lifespan: 500,
|
||||||
|
quantity: 8,
|
||||||
|
emitting: false
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.setDepth(10000);
|
||||||
|
emitter.explode();
|
||||||
|
|
||||||
|
this.scene.time.delayedCall(600, () => emitter.destroy());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Falling Leaves ambient effect
|
||||||
|
createFallingLeaves() {
|
||||||
|
if (!this.scene.settings || this.scene.settings.particles === 'NONE') return;
|
||||||
|
|
||||||
|
const width = this.scene.scale.width;
|
||||||
|
const quantity = this.scene.settings.particles === 'LOW' ? 1 : 2;
|
||||||
|
|
||||||
|
const leavesEmitter = this.scene.add.particles(0, -20, 'leaf_particle', {
|
||||||
|
x: { min: 0, max: width },
|
||||||
|
y: -20,
|
||||||
|
speedY: { min: 30, max: 80 },
|
||||||
|
speedX: { min: -20, max: 20 },
|
||||||
|
quantity: quantity,
|
||||||
|
frequency: 2000, // Every 2 seconds
|
||||||
|
lifespan: 8000,
|
||||||
|
scale: { min: 0.5, max: 1.0 },
|
||||||
|
rotation: { min: 0, max: 360 },
|
||||||
|
angle: { min: -10, max: 10 },
|
||||||
|
alpha: { start: 0.8, end: 0 }
|
||||||
|
});
|
||||||
|
|
||||||
|
leavesEmitter.setDepth(-980); // Below UI, above world
|
||||||
|
this.leavesEmitter = leavesEmitter;
|
||||||
|
|
||||||
|
return leavesEmitter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sparkles on item pickup
|
||||||
|
sparkle(x, y) {
|
||||||
|
const emitter = this.scene.add.particles(x, y, 'sparkle_particle', {
|
||||||
|
speed: { min: 20, max: 80 },
|
||||||
|
angle: { min: 0, max: 360 },
|
||||||
|
scale: { start: 1.5, end: 0 },
|
||||||
|
lifespan: 800,
|
||||||
|
quantity: 5,
|
||||||
|
emitting: false
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.setDepth(10000);
|
||||||
|
emitter.explode();
|
||||||
|
|
||||||
|
this.scene.time.delayedCall(900, () => emitter.destroy());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy all effects
|
||||||
|
destroy() {
|
||||||
|
if (this.leavesEmitter) {
|
||||||
|
this.leavesEmitter.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
92
src/systems/PathfindingSystem.js
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
class PathfindingSystem {
|
||||||
|
constructor(scene) {
|
||||||
|
this.scene = scene;
|
||||||
|
this.worker = null;
|
||||||
|
this.callbacks = new Map();
|
||||||
|
this.requestId = 0;
|
||||||
|
this.initialized = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Ustvarimo workerja
|
||||||
|
this.worker = new Worker('src/workers/pathfinding.worker.js');
|
||||||
|
this.worker.onmessage = this.handleMessage.bind(this);
|
||||||
|
console.log('✅ PathfindingWorker initialized.');
|
||||||
|
this.initialized = true;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('❌ Failed to init PathfindingWorker:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateGrid() {
|
||||||
|
if (!this.initialized || !this.scene.terrainSystem) return;
|
||||||
|
|
||||||
|
const ts = this.scene.terrainSystem;
|
||||||
|
const width = ts.width;
|
||||||
|
const height = ts.height;
|
||||||
|
|
||||||
|
// Ustvarimo flat array (0 = prehodno, 1 = ovira)
|
||||||
|
// Uporabimo Uint8Array za učinkovitost prenosa
|
||||||
|
const grid = new Uint8Array(width * height);
|
||||||
|
|
||||||
|
for (let y = 0; y < height; y++) {
|
||||||
|
for (let x = 0; x < width; x++) {
|
||||||
|
let blocked = 0;
|
||||||
|
const tile = ts.tiles[y][x];
|
||||||
|
|
||||||
|
// 1. Voda in void
|
||||||
|
if (!tile || tile.type === 'water' || tile.type === 'void') {
|
||||||
|
blocked = 1;
|
||||||
|
} else {
|
||||||
|
// 2. Dekoracije (Ovire)
|
||||||
|
// Uporabimo že obstoječo logiko v TerrainSystemu (če obstaja) ali preverimo dekoracije
|
||||||
|
const key = `${x},${y}`;
|
||||||
|
const decor = ts.decorationsMap.get(key);
|
||||||
|
if (decor) {
|
||||||
|
const solidTypes = [
|
||||||
|
'tree', 'tree_green', 'tree_blue', 'tree_dead',
|
||||||
|
'tree_green_new', 'tree_blue_new', 'tree_dead_new',
|
||||||
|
'rock', 'rock_asset', 'rock_new', 'rock_small', 'rock_1', 'rock_2',
|
||||||
|
'wall', 'fence', 'house', 'gravestone'
|
||||||
|
];
|
||||||
|
// Preverimo substring za tipe (npr. 'tree' ujame 'tree_blue')
|
||||||
|
const isSolid = solidTypes.some(t => decor.type.includes(t));
|
||||||
|
if (isSolid) blocked = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
grid[y * width + x] = blocked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.worker.postMessage({
|
||||||
|
type: 'UPDATE_GRID',
|
||||||
|
payload: { grid, width, height }
|
||||||
|
});
|
||||||
|
|
||||||
|
// console.log('🗺️ Pathfinding Grid updated sent to worker.');
|
||||||
|
}
|
||||||
|
|
||||||
|
findPath(startX, startY, endX, endY, callback) {
|
||||||
|
if (!this.initialized) return;
|
||||||
|
|
||||||
|
const id = this.requestId++;
|
||||||
|
this.callbacks.set(id, callback);
|
||||||
|
|
||||||
|
this.worker.postMessage({
|
||||||
|
type: 'FIND_PATH',
|
||||||
|
id: id,
|
||||||
|
payload: { startX, startY, endX, endY }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMessage(e) {
|
||||||
|
const { type, id, path } = e.data;
|
||||||
|
if (type === 'PATH_FOUND') {
|
||||||
|
const callback = this.callbacks.get(id);
|
||||||
|
if (callback) {
|
||||||
|
callback(path);
|
||||||
|
this.callbacks.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
197
src/systems/QuestSystem.js
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
class QuestSystem {
|
||||||
|
constructor(scene) {
|
||||||
|
this.scene = scene;
|
||||||
|
|
||||||
|
// Quest Definitions
|
||||||
|
this.questDB = {
|
||||||
|
'q1_start': {
|
||||||
|
id: 'q1_start',
|
||||||
|
title: 'Survival Basics',
|
||||||
|
description: 'Collect Wood and Stone to build your first defense.',
|
||||||
|
objectives: [
|
||||||
|
{ type: 'collect', item: 'wood', amount: 5, current: 0, done: false },
|
||||||
|
{ type: 'collect', item: 'stone', amount: 3, current: 0, done: false }
|
||||||
|
],
|
||||||
|
reward: { gold: 10, xp: 50 },
|
||||||
|
nextQuest: 'q2_farm',
|
||||||
|
giver: 'villager'
|
||||||
|
},
|
||||||
|
'q2_farm': {
|
||||||
|
id: 'q2_farm',
|
||||||
|
title: 'The Farmer',
|
||||||
|
description: 'Plant some seeds to grow food. You will need it.',
|
||||||
|
objectives: [
|
||||||
|
{ type: 'action', action: 'plant', amount: 3, current: 0, done: false }
|
||||||
|
],
|
||||||
|
reward: { gold: 20, item: 'wood', amount: 10 },
|
||||||
|
nextQuest: 'q3_defense',
|
||||||
|
giver: 'villager'
|
||||||
|
},
|
||||||
|
'q3_defense': {
|
||||||
|
id: 'q3_defense',
|
||||||
|
title: 'Fortification',
|
||||||
|
description: 'Build a Fence to keep zombies out.',
|
||||||
|
objectives: [
|
||||||
|
{ type: 'action', action: 'build_fence', amount: 2, current: 0, done: false }
|
||||||
|
],
|
||||||
|
reward: { gold: 50, item: 'sword', amount: 1 },
|
||||||
|
nextQuest: 'q4_slayer',
|
||||||
|
giver: 'merchant'
|
||||||
|
},
|
||||||
|
'q4_slayer': {
|
||||||
|
id: 'q4_slayer',
|
||||||
|
title: 'Zombie Slayer',
|
||||||
|
description: 'Kill 3 Zombies using your new sword.',
|
||||||
|
objectives: [
|
||||||
|
{ type: 'kill', target: 'zombie', amount: 3, current: 0, done: false }
|
||||||
|
],
|
||||||
|
reward: { gold: 100, item: 'gold', amount: 50 },
|
||||||
|
nextQuest: null,
|
||||||
|
giver: 'villager'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.activeQuest = null;
|
||||||
|
this.completedQuests = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
getAvailableQuest(npcType) {
|
||||||
|
const chain = ['q1_start', 'q2_farm', 'q3_defense', 'q4_slayer'];
|
||||||
|
let targetId = null;
|
||||||
|
for (const id of chain) {
|
||||||
|
if (!this.completedQuests.includes(id)) {
|
||||||
|
targetId = id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!targetId) return null;
|
||||||
|
if (this.activeQuest && this.activeQuest.id === targetId) return null;
|
||||||
|
|
||||||
|
const q = this.questDB[targetId];
|
||||||
|
if (q.giver === npcType) return q;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
startQuest(id) {
|
||||||
|
if (this.completedQuests.includes(id)) return;
|
||||||
|
|
||||||
|
const template = this.questDB[id];
|
||||||
|
if (!template) return;
|
||||||
|
|
||||||
|
this.activeQuest = JSON.parse(JSON.stringify(template));
|
||||||
|
console.log(`📜 Quest Started: ${this.activeQuest.title}`);
|
||||||
|
|
||||||
|
this.updateUI();
|
||||||
|
|
||||||
|
// Notification
|
||||||
|
this.scene.events.emit('show-floating-text', {
|
||||||
|
x: this.scene.player.x,
|
||||||
|
y: this.scene.player.y - 50,
|
||||||
|
text: "Quest Accepted!",
|
||||||
|
color: '#FFFF00'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
update(delta) {
|
||||||
|
if (!this.activeQuest) return;
|
||||||
|
|
||||||
|
let changed = false;
|
||||||
|
let allDone = true;
|
||||||
|
|
||||||
|
if (this.scene.inventorySystem) {
|
||||||
|
const inv = this.scene.inventorySystem;
|
||||||
|
|
||||||
|
for (const obj of this.activeQuest.objectives) {
|
||||||
|
if (obj.done) continue;
|
||||||
|
|
||||||
|
if (obj.type === 'collect') {
|
||||||
|
const count = inv.getItemCount(obj.item);
|
||||||
|
if (count !== obj.current) {
|
||||||
|
obj.current = count;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (obj.current >= obj.amount) {
|
||||||
|
obj.done = true;
|
||||||
|
this.scene.events.emit('show-floating-text', { x: this.scene.player.x, y: this.scene.player.y, text: "Objective Complete!" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const obj of this.activeQuest.objectives) {
|
||||||
|
if (!obj.done) allDone = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) this.updateUI();
|
||||||
|
|
||||||
|
if (allDone) {
|
||||||
|
this.completeQuest();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trackAction(actionType, amount = 1) {
|
||||||
|
if (!this.activeQuest) return;
|
||||||
|
|
||||||
|
let changed = false;
|
||||||
|
for (const obj of this.activeQuest.objectives) {
|
||||||
|
if (obj.done) continue;
|
||||||
|
|
||||||
|
if (obj.type === 'action' && obj.action === actionType) {
|
||||||
|
obj.current += amount;
|
||||||
|
changed = true;
|
||||||
|
if (obj.current >= obj.amount) {
|
||||||
|
obj.done = true;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (obj.type === 'kill' && obj.target === actionType) {
|
||||||
|
obj.current += amount;
|
||||||
|
changed = true;
|
||||||
|
if (obj.current >= obj.amount) {
|
||||||
|
obj.done = true;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changed) this.updateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
completeQuest() {
|
||||||
|
console.log(`🏆 Quest Complete: ${this.activeQuest.title}`);
|
||||||
|
|
||||||
|
if (this.activeQuest.reward) {
|
||||||
|
const r = this.activeQuest.reward;
|
||||||
|
if (r.gold && this.scene.inventorySystem) {
|
||||||
|
this.scene.inventorySystem.addGold(r.gold);
|
||||||
|
}
|
||||||
|
if (r.item && this.scene.inventorySystem) {
|
||||||
|
this.scene.inventorySystem.addItem(r.item, r.amount || 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scene.events.emit('show-floating-text', {
|
||||||
|
x: this.scene.player.x,
|
||||||
|
y: this.scene.player.y - 50,
|
||||||
|
text: "Quest Complete!",
|
||||||
|
color: '#00FF00'
|
||||||
|
});
|
||||||
|
|
||||||
|
this.completedQuests.push(this.activeQuest.id);
|
||||||
|
const next = this.activeQuest.nextQuest;
|
||||||
|
this.activeQuest = null;
|
||||||
|
this.updateUI();
|
||||||
|
|
||||||
|
if (next) {
|
||||||
|
console.log('Next quest available at NPC.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUI() {
|
||||||
|
const ui = this.scene.scene.get('UIScene');
|
||||||
|
if (ui && ui.updateQuestTracker) {
|
||||||
|
ui.updateQuestTracker(this.activeQuest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,7 +38,7 @@ class SaveSystem {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const saveData = {
|
const saveData = {
|
||||||
version: 1.1,
|
version: 2.4, // Nazaj na pixel art
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
player: { x: playerPos.x, y: playerPos.y },
|
player: { x: playerPos.x, y: playerPos.y },
|
||||||
terrain: {
|
terrain: {
|
||||||
@@ -62,11 +62,18 @@ class SaveSystem {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const jsonString = JSON.stringify(saveData);
|
const jsonString = JSON.stringify(saveData);
|
||||||
localStorage.setItem(this.storageKey, jsonString);
|
// Compress data to save space
|
||||||
|
try {
|
||||||
|
const compressed = Compression.compress(jsonString);
|
||||||
|
localStorage.setItem(this.storageKey, 'LZW:' + compressed);
|
||||||
|
console.log(`✅ Game saved! Size: ${jsonString.length} -> ${compressed.length} chars`);
|
||||||
|
} catch (compErr) {
|
||||||
|
console.warn("Compression failed, saving raw JSON:", compErr);
|
||||||
|
localStorage.setItem(this.storageKey, jsonString);
|
||||||
|
}
|
||||||
|
|
||||||
// Pokaži obvestilo (preko UIScene če obstaja)
|
// Pokaži obvestilo (preko UIScene če obstaja)
|
||||||
this.showNotification('GAME SAVED');
|
this.showNotification('GAME SAVED');
|
||||||
console.log('✅ Game saved successfully!', saveData);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('❌ Failed to save game:', e);
|
console.error('❌ Failed to save game:', e);
|
||||||
this.showNotification('SAVE FAILED');
|
this.showNotification('SAVE FAILED');
|
||||||
@@ -76,17 +83,32 @@ class SaveSystem {
|
|||||||
loadGame() {
|
loadGame() {
|
||||||
console.log('📂 Loading game...');
|
console.log('📂 Loading game...');
|
||||||
|
|
||||||
const jsonString = localStorage.getItem(this.storageKey);
|
let rawData = localStorage.getItem(this.storageKey);
|
||||||
if (!jsonString) {
|
if (!rawData) {
|
||||||
console.log('⚠️ No save file found.');
|
console.log('⚠️ No save file found.');
|
||||||
this.showNotification('NO SAVE FOUND');
|
this.showNotification('NO SAVE FOUND');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
let jsonString = rawData;
|
||||||
|
// Check for compression
|
||||||
|
if (rawData.startsWith('LZW:')) {
|
||||||
|
const compressed = rawData.substring(4); // Remove prefix
|
||||||
|
jsonString = Compression.decompress(compressed);
|
||||||
|
}
|
||||||
|
|
||||||
const saveData = JSON.parse(jsonString);
|
const saveData = JSON.parse(jsonString);
|
||||||
console.log('Loading save data:', saveData);
|
console.log('Loading save data:', saveData);
|
||||||
|
|
||||||
|
// Preveri verzijo - če je stara, izbriši save
|
||||||
|
if (!saveData.version || saveData.version < 2.4) {
|
||||||
|
console.log('⚠️ Stara verzija save file-a detected, clearing...');
|
||||||
|
localStorage.removeItem(this.storageKey);
|
||||||
|
this.showNotification('OLD SAVE CLEARED - NEW GAME');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Load Player
|
// 1. Load Player
|
||||||
if (this.scene.player) {
|
if (this.scene.player) {
|
||||||
// Zahteva metodo setPosition(gridX, gridY) v Player.js
|
// Zahteva metodo setPosition(gridX, gridY) v Player.js
|
||||||
@@ -131,8 +153,7 @@ class SaveSystem {
|
|||||||
this.scene.terrainSystem.visibleDecorations.clear();
|
this.scene.terrainSystem.visibleDecorations.clear();
|
||||||
this.scene.terrainSystem.visibleCrops.forEach(s => s.setVisible(false));
|
this.scene.terrainSystem.visibleCrops.forEach(s => s.setVisible(false));
|
||||||
this.scene.terrainSystem.visibleCrops.clear();
|
this.scene.terrainSystem.visibleCrops.clear();
|
||||||
this.scene.terrainSystem.decorationPool.releaseAll();
|
// Sproščanje objektov se samodejno dogaja preko release() v clearanju zgoraj
|
||||||
this.scene.terrainSystem.cropPool.releaseAll();
|
|
||||||
|
|
||||||
// B) Restore Crops
|
// B) Restore Crops
|
||||||
if (saveData.terrain.crops) {
|
if (saveData.terrain.crops) {
|
||||||
|
|||||||
@@ -117,6 +117,111 @@ class SoundManager {
|
|||||||
osc.stop(ctx.currentTime + 0.2);
|
osc.stop(ctx.currentTime + 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
beepAttack() {
|
||||||
|
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(400, ctx.currentTime);
|
||||||
|
osc.frequency.exponentialRampToValueAtTime(100, ctx.currentTime + 0.08);
|
||||||
|
osc.type = 'sawtooth';
|
||||||
|
gain.gain.setValueAtTime(0.2, ctx.currentTime);
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.08);
|
||||||
|
osc.start();
|
||||||
|
osc.stop(ctx.currentTime + 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
beepHit() {
|
||||||
|
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.25, ctx.currentTime);
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.05);
|
||||||
|
osc.start();
|
||||||
|
osc.stop(ctx.currentTime + 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
beepFootstep() {
|
||||||
|
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 = 120 + Math.random() * 20;
|
||||||
|
osc.type = 'sine';
|
||||||
|
gain.gain.setValueAtTime(0.05, ctx.currentTime);
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.03);
|
||||||
|
osc.start();
|
||||||
|
osc.stop(ctx.currentTime + 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
beepDeath() {
|
||||||
|
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(300, ctx.currentTime);
|
||||||
|
osc.frequency.exponentialRampToValueAtTime(50, ctx.currentTime + 0.5);
|
||||||
|
osc.type = 'sawtooth';
|
||||||
|
gain.gain.setValueAtTime(0.2, ctx.currentTime);
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.5);
|
||||||
|
osc.start();
|
||||||
|
osc.stop(ctx.currentTime + 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
startRainNoise() {
|
||||||
|
if (!this.scene.sound.context || this.rainNode) return;
|
||||||
|
const ctx = this.scene.sound.context;
|
||||||
|
|
||||||
|
// Create noise buffer
|
||||||
|
const bufferSize = 2 * ctx.sampleRate;
|
||||||
|
const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
|
||||||
|
const output = buffer.getChannelData(0);
|
||||||
|
for (let i = 0; i < bufferSize; i++) {
|
||||||
|
output[i] = Math.random() * 2 - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.rainNode = ctx.createBufferSource();
|
||||||
|
this.rainNode.buffer = buffer;
|
||||||
|
this.rainNode.loop = true;
|
||||||
|
|
||||||
|
// Lowpass filter for rain sound
|
||||||
|
const filter = ctx.createBiquadFilter();
|
||||||
|
filter.type = 'lowpass';
|
||||||
|
filter.frequency.value = 800;
|
||||||
|
|
||||||
|
this.rainGain = ctx.createGain();
|
||||||
|
this.rainGain.gain.value = 0.05 * this.sfxVolume;
|
||||||
|
|
||||||
|
this.rainNode.connect(filter);
|
||||||
|
filter.connect(this.rainGain);
|
||||||
|
this.rainGain.connect(ctx.destination);
|
||||||
|
|
||||||
|
this.rainNode.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
stopRainNoise() {
|
||||||
|
if (this.rainNode) {
|
||||||
|
this.rainNode.stop();
|
||||||
|
this.rainNode.disconnect();
|
||||||
|
this.rainNode = null;
|
||||||
|
}
|
||||||
|
if (this.rainGain) {
|
||||||
|
this.rainGain.disconnect();
|
||||||
|
this.rainGain = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
playAmbient(key, loop = true) {
|
playAmbient(key, loop = true) {
|
||||||
if (this.isMuted) return;
|
if (this.isMuted) return;
|
||||||
if (this.currentAmbient) this.currentAmbient.stop();
|
if (this.currentAmbient) this.currentAmbient.stop();
|
||||||
@@ -132,6 +237,55 @@ class SoundManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
startMusic() {
|
||||||
|
if (!this.scene.sound.context || this.musicInterval) return;
|
||||||
|
|
||||||
|
console.log('🎵 Starting Ambient Music...');
|
||||||
|
// Simple C Minor Pentatonic: C3, Eb3, F3, G3, Bb3
|
||||||
|
const scale = [130.81, 155.56, 174.61, 196.00, 233.08, 261.63];
|
||||||
|
|
||||||
|
// Loop every 3-5 seconds play a note
|
||||||
|
this.musicInterval = setInterval(() => {
|
||||||
|
if (this.isMuted) return;
|
||||||
|
// 40% chance to play a note
|
||||||
|
if (Math.random() > 0.6) {
|
||||||
|
const freq = scale[Math.floor(Math.random() * scale.length)];
|
||||||
|
this.playProceduralNote(freq);
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
stopMusic() {
|
||||||
|
if (this.musicInterval) {
|
||||||
|
clearInterval(this.musicInterval);
|
||||||
|
this.musicInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
playProceduralNote(freq) {
|
||||||
|
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 = freq;
|
||||||
|
osc.type = 'sine'; // Soft tone
|
||||||
|
|
||||||
|
const now = ctx.currentTime;
|
||||||
|
const duration = 2.0; // Long decay like reverb/pad
|
||||||
|
|
||||||
|
gain.gain.setValueAtTime(0, now);
|
||||||
|
gain.gain.linearRampToValueAtTime(0.05 * this.musicVolume, now + 0.5); // Slow attack
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.001, now + duration); // Long release
|
||||||
|
|
||||||
|
osc.start(now);
|
||||||
|
osc.stop(now + duration);
|
||||||
|
}
|
||||||
|
|
||||||
toggleMute() {
|
toggleMute() {
|
||||||
this.isMuted = !this.isMuted;
|
this.isMuted = !this.isMuted;
|
||||||
this.scene.sound.mute = this.isMuted;
|
this.scene.sound.mute = this.isMuted;
|
||||||
@@ -143,6 +297,10 @@ class SoundManager {
|
|||||||
playHarvest() { this.playSFX('harvest'); }
|
playHarvest() { this.playSFX('harvest'); }
|
||||||
playBuild() { this.playSFX('build'); }
|
playBuild() { this.playSFX('build'); }
|
||||||
playPickup() { this.playSFX('pickup'); }
|
playPickup() { this.playSFX('pickup'); }
|
||||||
playRainSound() { this.playAmbient('rain_loop'); }
|
playAttack() { this.beepAttack(); }
|
||||||
stopRainSound() { this.stopAmbient(); }
|
playHit() { this.beepHit(); }
|
||||||
|
playFootstep() { this.beepFootstep(); }
|
||||||
|
playDeath() { this.beepDeath(); }
|
||||||
|
playRainSound() { this.startRainNoise(); }
|
||||||
|
stopRainSound() { this.stopRainNoise(); }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
|
// ========================================================
|
||||||
|
// NOVE GLOBALNE KONSTANTE ZA LOKACIJE
|
||||||
|
// ========================================================
|
||||||
|
const FARM_SIZE = 8;
|
||||||
|
const FARM_CENTER_X = 20; // Lokacija farme na X osi
|
||||||
|
const FARM_CENTER_Y = 20; // Lokacija farme na Y osi
|
||||||
|
|
||||||
|
const CITY_SIZE = 15;
|
||||||
|
const CITY_START_X = 65; // Desni del mape (npr. med 65 in 80)
|
||||||
|
const CITY_START_Y = 65;
|
||||||
|
|
||||||
// Terrain Generator System
|
// Terrain Generator System
|
||||||
// Generira proceduralni isometrični teren in skrbi za optimizacijo (Tilemap + Culling)
|
|
||||||
class TerrainSystem {
|
class TerrainSystem {
|
||||||
constructor(scene, width = 100, height = 100) {
|
constructor(scene, width = 100, height = 100) {
|
||||||
this.scene = scene;
|
this.scene = scene;
|
||||||
@@ -14,10 +24,29 @@ class TerrainSystem {
|
|||||||
this.decorationsMap = new Map();
|
this.decorationsMap = new Map();
|
||||||
this.cropsMap = new Map();
|
this.cropsMap = new Map();
|
||||||
|
|
||||||
|
this.visibleTiles = new Map();
|
||||||
this.visibleDecorations = new Map();
|
this.visibleDecorations = new Map();
|
||||||
this.visibleCrops = new Map();
|
this.visibleCrops = new Map();
|
||||||
|
|
||||||
// Pool for Decorations (Trees, Rocks, etc.)
|
this.tilePool = {
|
||||||
|
active: [],
|
||||||
|
inactive: [],
|
||||||
|
get: () => {
|
||||||
|
if (this.tilePool.inactive.length > 0) {
|
||||||
|
const s = this.tilePool.inactive.pop();
|
||||||
|
s.setVisible(true);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
const s = this.scene.add.sprite(0, 0, 'dirt');
|
||||||
|
s.setOrigin(0.5, 0.5);
|
||||||
|
return s;
|
||||||
|
},
|
||||||
|
release: (sprite) => {
|
||||||
|
sprite.setVisible(false);
|
||||||
|
this.tilePool.inactive.push(sprite);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
this.decorationPool = {
|
this.decorationPool = {
|
||||||
active: [],
|
active: [],
|
||||||
inactive: [],
|
inactive: [],
|
||||||
@@ -36,7 +65,6 @@ class TerrainSystem {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Pool for Crops
|
|
||||||
this.cropPool = {
|
this.cropPool = {
|
||||||
active: [],
|
active: [],
|
||||||
inactive: [],
|
inactive: [],
|
||||||
@@ -55,14 +83,16 @@ class TerrainSystem {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.terrainTypes = {
|
this.terrainTypes = {
|
||||||
WATER: { name: 'water', height: 0, color: 0x4444ff, index: 0 },
|
WATER: { name: 'water', height: 0, color: 0x4444ff },
|
||||||
SAND: { name: 'sand', height: 0.2, color: 0xdddd44, index: 1 },
|
SAND: { name: 'sand', height: 0.2, color: 0xdddd44 },
|
||||||
GRASS_FULL: { name: 'grass_full', height: 0.35, color: 0x44aa44, index: 2 },
|
GRASS_FULL: { name: 'grass_full', height: 0.35, color: 0x44aa44 },
|
||||||
GRASS_TOP: { name: 'grass_top', height: 0.45, color: 0x66cc66, index: 3 },
|
GRASS_TOP: { name: 'grass_top', height: 0.45, color: 0x66cc66 },
|
||||||
DIRT: { name: 'dirt', height: 0.5, color: 0x8b4513, index: 4 },
|
DIRT: { name: 'dirt', height: 0.5, color: 0x8b4513 },
|
||||||
STONE: { name: 'stone', height: 0.7, color: 0x888888, index: 5 },
|
STONE: { name: 'stone', height: 0.7, color: 0x888888 },
|
||||||
PATH: { name: 'path', height: -1, color: 0xc2b280, index: 6 },
|
PAVEMENT: { name: 'pavement', height: 0.6, color: 0x777777 },
|
||||||
FARMLAND: { name: 'farmland', height: -1, color: 0x5c4033, index: 7 }
|
RUINS: { name: 'ruins', height: 0.6, color: 0x555555 },
|
||||||
|
PATH: { name: 'path', height: -1, color: 0xc2b280 },
|
||||||
|
FARMLAND: { name: 'farmland', height: -1, color: 0x5c4033 }
|
||||||
};
|
};
|
||||||
|
|
||||||
this.offsetX = 0;
|
this.offsetX = 0;
|
||||||
@@ -70,53 +100,21 @@ class TerrainSystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createTileTextures() {
|
createTileTextures() {
|
||||||
// Create a single spritesheet for tiles (Tilemap Optimization)
|
|
||||||
const graphics = this.scene.make.graphics({ x: 0, y: 0, add: false });
|
|
||||||
|
|
||||||
const tileWidth = 48;
|
const tileWidth = 48;
|
||||||
const tileHeight = 32; // 24 for iso + 8 depth
|
const tileHeight = 60;
|
||||||
const types = Object.values(this.terrainTypes);
|
const types = Object.values(this.terrainTypes);
|
||||||
|
|
||||||
// Draw all tiles horizontally
|
types.forEach((type) => {
|
||||||
types.forEach((type, index) => {
|
if (this.scene.textures.exists(type.name)) return;
|
||||||
// Update index just in case
|
|
||||||
type.index = index;
|
|
||||||
|
|
||||||
const x = index * tileWidth;
|
const graphics = this.scene.make.graphics({ x: 0, y: 0, add: false });
|
||||||
graphics.fillStyle(type.color);
|
const x = 0;
|
||||||
|
const midX = 24;
|
||||||
// Draw Isometic Tile (Diamond + Thickness)
|
|
||||||
const top = 0;
|
|
||||||
const midX = x + 24;
|
|
||||||
const midY = 12;
|
const midY = 12;
|
||||||
const bottomY = 24;
|
const bottomY = 24;
|
||||||
const depth = 8;
|
const depth = 20;
|
||||||
|
|
||||||
// Top Face
|
graphics.fillStyle(0x8B4513);
|
||||||
graphics.beginPath();
|
|
||||||
graphics.moveTo(midX, top);
|
|
||||||
graphics.lineTo(x + 48, midY);
|
|
||||||
graphics.lineTo(midX, bottomY);
|
|
||||||
graphics.lineTo(x, midY);
|
|
||||||
graphics.closePath();
|
|
||||||
graphics.fill();
|
|
||||||
|
|
||||||
// Add stroke to prevent seams/gaps (Robust Fix)
|
|
||||||
graphics.lineStyle(2, type.color);
|
|
||||||
graphics.strokePath();
|
|
||||||
|
|
||||||
// Thickness (Right)
|
|
||||||
graphics.fillStyle(Phaser.Display.Color.IntegerToColor(type.color).darken(20).color);
|
|
||||||
graphics.beginPath();
|
|
||||||
graphics.moveTo(x + 48, midY);
|
|
||||||
graphics.lineTo(x + 48, midY + depth);
|
|
||||||
graphics.lineTo(midX, bottomY + depth);
|
|
||||||
graphics.lineTo(midX, bottomY);
|
|
||||||
graphics.closePath();
|
|
||||||
graphics.fill();
|
|
||||||
|
|
||||||
// Thickness (Left)
|
|
||||||
graphics.fillStyle(Phaser.Display.Color.IntegerToColor(type.color).darken(40).color);
|
|
||||||
graphics.beginPath();
|
graphics.beginPath();
|
||||||
graphics.moveTo(midX, bottomY);
|
graphics.moveTo(midX, bottomY);
|
||||||
graphics.lineTo(midX, bottomY + depth);
|
graphics.lineTo(midX, bottomY + depth);
|
||||||
@@ -125,28 +123,59 @@ class TerrainSystem {
|
|||||||
graphics.closePath();
|
graphics.closePath();
|
||||||
graphics.fill();
|
graphics.fill();
|
||||||
|
|
||||||
// Detail (Grass)
|
graphics.fillStyle(0x6B3410);
|
||||||
if (type.name.includes('grass')) {
|
graphics.beginPath();
|
||||||
graphics.fillStyle(0x339933); // Darker green blades
|
graphics.moveTo(x + 48, midY);
|
||||||
for (let i = 0; i < 15; i++) {
|
graphics.lineTo(x + 48, midY + depth);
|
||||||
const rx = x + 8 + Math.random() * 32;
|
graphics.lineTo(midX, bottomY + depth);
|
||||||
const ry = 4 + Math.random() * 16;
|
graphics.lineTo(midX, bottomY);
|
||||||
graphics.fillRect(rx, ry, 2, 2);
|
graphics.closePath();
|
||||||
}
|
graphics.fill();
|
||||||
}
|
|
||||||
// Detail (Dirt)
|
|
||||||
if (type.name.includes('dirt')) {
|
|
||||||
graphics.fillStyle(0x5c4033);
|
|
||||||
for (let i = 0; i < 8; i++) {
|
|
||||||
const rx = x + 8 + Math.random() * 32;
|
|
||||||
const ry = 4 + Math.random() * 16;
|
|
||||||
graphics.fillRect(rx, ry, 2, 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
graphics.generateTexture('terrain_tileset', tileWidth * types.length, tileHeight);
|
graphics.fillStyle(type.color);
|
||||||
graphics.destroy();
|
graphics.beginPath();
|
||||||
|
graphics.moveTo(midX, 0);
|
||||||
|
graphics.lineTo(x + 48, midY);
|
||||||
|
graphics.lineTo(midX, bottomY);
|
||||||
|
graphics.lineTo(x, midY);
|
||||||
|
graphics.closePath();
|
||||||
|
graphics.fill();
|
||||||
|
|
||||||
|
graphics.lineStyle(1, 0xffffff, 0.15);
|
||||||
|
graphics.beginPath();
|
||||||
|
graphics.moveTo(x, midY);
|
||||||
|
graphics.lineTo(midX, 0);
|
||||||
|
graphics.lineTo(x + 48, midY);
|
||||||
|
graphics.strokePath();
|
||||||
|
|
||||||
|
if (type.name.includes('grass')) {
|
||||||
|
graphics.fillStyle(Phaser.Display.Color.IntegerToColor(type.color).lighten(10).color);
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
const rx = x + 10 + Math.random() * 28;
|
||||||
|
const ry = 4 + Math.random() * 16;
|
||||||
|
graphics.fillRect(rx, ry, 2, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (type.name.includes('stone') || type.name.includes('ruins')) {
|
||||||
|
graphics.fillStyle(0x444444);
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
const rx = x + 8 + Math.random() * 30;
|
||||||
|
const ry = 4 + Math.random() * 16;
|
||||||
|
graphics.fillRect(rx, ry, 3, 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type.name.includes('pavement')) {
|
||||||
|
graphics.lineStyle(1, 0x555555, 0.5);
|
||||||
|
graphics.beginPath();
|
||||||
|
graphics.moveTo(x + 12, midY + 6);
|
||||||
|
graphics.lineTo(x + 36, midY - 6);
|
||||||
|
graphics.strokePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
graphics.generateTexture(type.name, tileWidth, tileHeight);
|
||||||
|
graphics.destroy();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
generate() {
|
generate() {
|
||||||
@@ -159,11 +188,29 @@ class TerrainSystem {
|
|||||||
const ny = y * 0.1;
|
const ny = y * 0.1;
|
||||||
const elevation = this.noise.noise(nx, ny);
|
const elevation = this.noise.noise(nx, ny);
|
||||||
|
|
||||||
let terrainType = this.terrainTypes.WATER;
|
let terrainType = this.terrainTypes.GRASS_FULL;
|
||||||
if (elevation > this.terrainTypes.SAND.height) terrainType = this.terrainTypes.SAND;
|
|
||||||
if (elevation > this.terrainTypes.GRASS_FULL.height) terrainType = this.terrainTypes.GRASS_FULL;
|
if (x < 3 || x >= this.width - 3 || y < 3 || y >= this.height - 3) {
|
||||||
if (elevation > this.terrainTypes.DIRT.height) terrainType = this.terrainTypes.DIRT;
|
terrainType = this.terrainTypes.GRASS_FULL;
|
||||||
if (elevation > this.terrainTypes.STONE.height) terrainType = this.terrainTypes.STONE;
|
} else {
|
||||||
|
if (elevation < -0.6) terrainType = this.terrainTypes.WATER;
|
||||||
|
else if (elevation > 0.1) terrainType = this.terrainTypes.SAND;
|
||||||
|
else if (elevation > 0.2) terrainType = this.terrainTypes.GRASS_FULL;
|
||||||
|
else if (elevation > 0.3) terrainType = this.terrainTypes.GRASS_TOP;
|
||||||
|
else if (elevation > 0.7) terrainType = this.terrainTypes.DIRT;
|
||||||
|
else if (elevation > 0.85) terrainType = this.terrainTypes.STONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.abs(x - FARM_CENTER_X) <= FARM_SIZE / 2 && Math.abs(y - FARM_CENTER_Y) <= FARM_SIZE / 2) {
|
||||||
|
terrainType = this.terrainTypes.DIRT;
|
||||||
|
}
|
||||||
|
if (x >= CITY_START_X && x < CITY_START_X + CITY_SIZE &&
|
||||||
|
y >= CITY_START_Y && y < CITY_START_Y + CITY_SIZE) {
|
||||||
|
terrainType = this.terrainTypes.PAVEMENT;
|
||||||
|
if (Math.random() < 0.2) {
|
||||||
|
terrainType = this.terrainTypes.RUINS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.tiles[y][x] = {
|
this.tiles[y][x] = {
|
||||||
type: terrainType.name,
|
type: terrainType.name,
|
||||||
@@ -171,86 +218,85 @@ class TerrainSystem {
|
|||||||
hasDecoration: false,
|
hasDecoration: false,
|
||||||
hasCrop: false
|
hasCrop: false
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Vegetation logic (Rich World)
|
let treeCount = 0;
|
||||||
if (x > 5 && x < this.width - 5 && y > 5 && y < this.height - 5) {
|
let rockCount = 0;
|
||||||
let decorType = null;
|
let flowerCount = 0;
|
||||||
let maxHp = 1;
|
|
||||||
let scale = 1.0;
|
|
||||||
|
|
||||||
if (terrainType.name.includes('grass')) {
|
const validPositions = [];
|
||||||
const rand = Math.random();
|
const isFarm = (x, y) => Math.abs(x - FARM_CENTER_X) <= (FARM_SIZE / 2 + 2) && Math.abs(y - FARM_CENTER_Y) <= (FARM_SIZE / 2 + 2);
|
||||||
if (elevation > 0.6 && rand < 0.1) {
|
const isCity = (x, y) => x >= CITY_START_X - 2 && x < CITY_START_X + CITY_SIZE + 2 && y >= CITY_START_Y - 2 && y < CITY_START_Y + CITY_SIZE + 2;
|
||||||
decorType = 'bush';
|
|
||||||
maxHp = 5;
|
|
||||||
} else if (rand < 0.15) { // Common trees
|
|
||||||
decorType = 'tree';
|
|
||||||
maxHp = 5;
|
|
||||||
const sizeRand = Math.random();
|
|
||||||
if (sizeRand < 0.2) scale = 0.8;
|
|
||||||
else if (sizeRand < 0.8) scale = 1.0 + Math.random() * 0.3;
|
|
||||||
else scale = 1.3;
|
|
||||||
} else if (rand < 0.18) { // Rocks
|
|
||||||
decorType = 'rock';
|
|
||||||
maxHp = 8;
|
|
||||||
scale = 1.2 + Math.random() * 0.5;
|
|
||||||
} else if (rand < 0.19) {
|
|
||||||
decorType = 'gravestone';
|
|
||||||
maxHp = 10;
|
|
||||||
} else if (rand < 0.30) {
|
|
||||||
decorType = 'flower';
|
|
||||||
maxHp = 1;
|
|
||||||
}
|
|
||||||
} else if (terrainType.name === 'dirt' && Math.random() < 0.05) {
|
|
||||||
decorType = 'bush';
|
|
||||||
maxHp = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (decorType) {
|
for (let y = 5; y < this.height - 5; y++) {
|
||||||
const key = `${x},${y}`;
|
for (let x = 5; x < this.width - 5; x++) {
|
||||||
const decorData = {
|
if (isFarm(x, y) || isCity(x, y)) continue;
|
||||||
gridX: x,
|
|
||||||
gridY: y,
|
const tile = this.tiles[y][x];
|
||||||
type: decorType,
|
if (tile.type !== 'water' && tile.type !== 'sand' && tile.type !== 'stone') {
|
||||||
id: key,
|
validPositions.push({ x, y });
|
||||||
maxHp: maxHp,
|
|
||||||
hp: maxHp,
|
|
||||||
scale: scale
|
|
||||||
};
|
|
||||||
this.decorations.push(decorData);
|
|
||||||
this.decorationsMap.set(key, decorData);
|
|
||||||
this.tiles[y][x].hasDecoration = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('✅ Terrain and decorations generated!');
|
for (let i = validPositions.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[validPositions[i], validPositions[j]] = [validPositions[j], validPositions[i]];
|
||||||
|
}
|
||||||
|
|
||||||
// --- TILEMAP IMPLEMENTATION (Performance) ---
|
for (let i = 0; i < Math.min(25, validPositions.length); i++) {
|
||||||
if (this.map) this.map.destroy();
|
const pos = validPositions[i];
|
||||||
this.map = this.scene.make.tilemap({
|
let treeType = 'tree_green_new';
|
||||||
tileWidth: this.iso.tileWidth, // 48
|
const rand = Math.random();
|
||||||
tileHeight: this.iso.tileHeight, // 24
|
if (rand < 0.15) treeType = 'tree_blue_new';
|
||||||
width: this.width,
|
else if (rand < 0.25) treeType = 'tree_dead_new';
|
||||||
height: this.height,
|
|
||||||
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC
|
|
||||||
});
|
|
||||||
|
|
||||||
// 48x32 tileset
|
this.addDecoration(pos.x, pos.y, treeType);
|
||||||
const tileset = this.map.addTilesetImage('terrain_tileset', 'terrain_tileset', 48, 32);
|
treeCount++;
|
||||||
this.layer = this.map.createBlankLayer('Ground', tileset, this.offsetX, this.offsetY);
|
}
|
||||||
|
for (let i = 25; i < Math.min(50, validPositions.length); i++) {
|
||||||
|
const pos = validPositions[i];
|
||||||
|
// Uporabi uporabnikove kamne
|
||||||
|
const rockType = Math.random() > 0.5 ? 'rock_1' : 'rock_2';
|
||||||
|
this.addDecoration(pos.x, pos.y, rockType);
|
||||||
|
rockCount++;
|
||||||
|
}
|
||||||
|
|
||||||
for (let y = 0; y < this.height; y++) {
|
const flowerNoise = new PerlinNoise(Date.now() + 3000);
|
||||||
for (let x = 0; x < this.width; x++) {
|
for (let y = 5; y < this.height - 5; y++) {
|
||||||
const t = this.tiles[y][x];
|
for (let x = 5; x < this.width - 5; x++) {
|
||||||
const typeDef = Object.values(this.terrainTypes).find(tt => tt.name === t.type);
|
if (isFarm(x, y) || isCity(x, y)) continue;
|
||||||
if (typeDef) {
|
|
||||||
this.layer.putTileAt(typeDef.index, x, y);
|
const tile = this.tiles[y][x];
|
||||||
|
const val = flowerNoise.noise(x * 0.12, y * 0.12);
|
||||||
|
if (val > 0.85 && tile.type.includes('grass')) {
|
||||||
|
this.addDecoration(x, y, 'flowers_new');
|
||||||
|
flowerCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.layer.setDepth(0); // Ground level
|
|
||||||
|
|
||||||
|
const roomSize = 5;
|
||||||
|
const roomsAcross = Math.floor(CITY_SIZE / roomSize);
|
||||||
|
|
||||||
|
for (let ry = 0; ry < roomsAcross; ry++) {
|
||||||
|
for (let rx = 0; rx < roomsAcross; rx++) {
|
||||||
|
if (Math.random() < 0.75) {
|
||||||
|
const gx = CITY_START_X + rx * roomSize;
|
||||||
|
const gy = CITY_START_Y + ry * roomSize;
|
||||||
|
this.placeStructure(gx, gy, 'ruin_room');
|
||||||
|
} else {
|
||||||
|
const gx = CITY_START_X + rx * roomSize + 2;
|
||||||
|
const gy = CITY_START_Y + ry * roomSize + 2;
|
||||||
|
const rockType = Math.random() > 0.5 ? 'rock_1' : 'rock_2';
|
||||||
|
this.addDecoration(gx, gy, rockType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ Teren generiran: ${treeCount} dreves, ${rockCount} kamnov.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
damageDecoration(x, y, amount) {
|
damageDecoration(x, y, amount) {
|
||||||
@@ -299,33 +345,115 @@ class TerrainSystem {
|
|||||||
return decor.type;
|
return decor.type;
|
||||||
}
|
}
|
||||||
|
|
||||||
placeStructure(x, y, structureType) {
|
init(offsetX, offsetY) {
|
||||||
if (this.decorationsMap.has(`${x},${y}`)) return false;
|
this.offsetX = offsetX;
|
||||||
|
this.offsetY = offsetY;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTile(x, y, type) {
|
||||||
|
if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
|
||||||
|
this.tiles[y][x].type = type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
placeStructure(gridX, gridY, type) {
|
||||||
|
if (type === 'ruin') {
|
||||||
|
for (let y = 0; y < 6; y++) {
|
||||||
|
for (let x = 0; x < 6; x++) {
|
||||||
|
if (Math.random() > 0.6) this.addDecoration(gridX + x, gridY + y, 'fence');
|
||||||
|
this.setTile(gridX + x, gridY + y, 'stone');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (type === 'arena') {
|
||||||
|
const size = 12;
|
||||||
|
for (let y = 0; y < size; y++) {
|
||||||
|
for (let x = 0; x < size; x++) {
|
||||||
|
const tx = gridX + x;
|
||||||
|
const ty = gridY + y;
|
||||||
|
|
||||||
|
this.setTile(tx, ty, 'stone');
|
||||||
|
if (x === 0 || x === size - 1 || y === 0 || y === size - 1) {
|
||||||
|
if (!(x === Math.floor(size / 2) && y === size - 1)) {
|
||||||
|
this.addDecoration(tx, ty, 'fence');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.addDecoration(gridX + 6, gridY + 6, 'gravestone');
|
||||||
|
}
|
||||||
|
if (type === 'ruin_room') {
|
||||||
|
for (let y = 0; y < 5; y++) {
|
||||||
|
for (let x = 0; x < 5; x++) {
|
||||||
|
const tx = gridX + x;
|
||||||
|
const ty = gridY + y;
|
||||||
|
if (x > 0 && x < 4 && y > 0 && y < 4) {
|
||||||
|
this.setTile(tx, ty, 'ruins');
|
||||||
|
}
|
||||||
|
if (x === 0 || x === 4 || y === 0 || y === 4) {
|
||||||
|
const isCenter = (x === 2 || y === 2);
|
||||||
|
if (isCenter && Math.random() > 0.5) continue;
|
||||||
|
if (Math.random() > 0.3) {
|
||||||
|
this.addDecoration(tx, ty, 'fence');
|
||||||
|
} else {
|
||||||
|
// User rocks in ruins
|
||||||
|
if (Math.random() > 0.5) {
|
||||||
|
const rType = Math.random() > 0.5 ? 'rock_1' : 'rock_2';
|
||||||
|
this.addDecoration(tx, ty, rType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addDecoration(gridX, gridY, type) {
|
||||||
|
if (gridX < 0 || gridX >= this.width || gridY < 0 || gridY >= this.height) return;
|
||||||
|
|
||||||
|
const key = `${gridX},${gridY}`;
|
||||||
|
if (this.decorationsMap.has(key)) return;
|
||||||
|
|
||||||
|
let scale = 1.0;
|
||||||
|
|
||||||
|
if (type === 'rock_1' || type === 'rock_2') scale = 1.5; // Povečano (bilo 0.5)
|
||||||
|
else if (type === 'tree_green_new' || type === 'tree_blue_new' || type === 'tree_dead_new') scale = 0.04;
|
||||||
|
else if (type === 'flowers_new') scale = 0.02;
|
||||||
|
else if (type === 'fence') scale = 0.025;
|
||||||
|
else if (type === 'gravestone') scale = 0.03;
|
||||||
|
else if (type === 'hill_sprite') scale = 0.025;
|
||||||
|
else {
|
||||||
|
// Old Assets (Low Res)
|
||||||
|
if (type.includes('tree')) scale = 1.2 + Math.random() * 0.4;
|
||||||
|
else if (type.includes('rock')) scale = 0.8;
|
||||||
|
else scale = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
const decorData = {
|
const decorData = {
|
||||||
gridX: x,
|
gridX: gridX,
|
||||||
gridY: y,
|
gridY: gridY,
|
||||||
type: structureType,
|
type: type,
|
||||||
id: `${x},${y}`,
|
id: key,
|
||||||
maxHp: 5,
|
maxHp: 10,
|
||||||
hp: 5
|
hp: 10,
|
||||||
|
scale: scale
|
||||||
};
|
};
|
||||||
this.decorations.push(decorData);
|
this.decorations.push(decorData);
|
||||||
this.decorationsMap.set(decorData.id, decorData);
|
this.decorationsMap.set(key, decorData);
|
||||||
const tile = this.getTile(x, y);
|
|
||||||
if (tile) tile.hasDecoration = true;
|
if (this.tiles[gridY] && this.tiles[gridY][gridX]) {
|
||||||
this.lastCullX = -9999;
|
this.tiles[gridY][gridX].hasDecoration = true;
|
||||||
return true;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setTileType(x, y, typeName) {
|
setTileType(x, y, typeName) {
|
||||||
if (!this.tiles[y] || !this.tiles[y][x]) return;
|
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].type = typeName;
|
||||||
// Tilemap update
|
|
||||||
if (this.layer) {
|
const key = `${x},${y}`;
|
||||||
this.layer.putTileAt(typeDef.index, x, y);
|
if (this.visibleTiles.has(key)) {
|
||||||
|
const sprite = this.visibleTiles.get(key);
|
||||||
|
sprite.setTexture(typeName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,7 +461,6 @@ class TerrainSystem {
|
|||||||
const key = `${x},${y}`;
|
const key = `${x},${y}`;
|
||||||
this.cropsMap.set(key, cropData);
|
this.cropsMap.set(key, cropData);
|
||||||
this.tiles[y][x].hasCrop = true;
|
this.tiles[y][x].hasCrop = true;
|
||||||
this.lastCullX = -9999;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
removeCrop(x, y) {
|
removeCrop(x, y) {
|
||||||
@@ -358,11 +485,6 @@ class TerrainSystem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init(offsetX, offsetY) {
|
|
||||||
this.offsetX = offsetX;
|
|
||||||
this.offsetY = offsetY;
|
|
||||||
}
|
|
||||||
|
|
||||||
getTile(x, y) {
|
getTile(x, y) {
|
||||||
if (this.tiles[y] && this.tiles[y][x]) {
|
if (this.tiles[y] && this.tiles[y][x]) {
|
||||||
return this.tiles[y][x];
|
return this.tiles[y][x];
|
||||||
@@ -371,10 +493,10 @@ class TerrainSystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateCulling(camera) {
|
updateCulling(camera) {
|
||||||
// Culling for Decorations & Crops (Tiles controlled by Tilemap)
|
|
||||||
const view = camera.worldView;
|
const view = camera.worldView;
|
||||||
let buffer = 200;
|
let buffer = 200;
|
||||||
if (this.scene.settings && this.scene.settings.viewDistance === 'LOW') buffer = 50;
|
if (this.scene.settings && this.scene.settings.viewDistance === 'LOW') buffer = 50;
|
||||||
|
|
||||||
const left = view.x - buffer - this.offsetX;
|
const left = view.x - buffer - this.offsetX;
|
||||||
const top = view.y - buffer - this.offsetY;
|
const top = view.y - buffer - this.offsetY;
|
||||||
const right = view.x + view.width + buffer - this.offsetX;
|
const right = view.x + view.width + buffer - this.offsetX;
|
||||||
@@ -395,14 +517,29 @@ class TerrainSystem {
|
|||||||
const startY = Math.max(0, minGridY);
|
const startY = Math.max(0, minGridY);
|
||||||
const endY = Math.min(this.height, maxGridY);
|
const endY = Math.min(this.height, maxGridY);
|
||||||
|
|
||||||
|
const neededTileKeys = new Set();
|
||||||
const neededDecorKeys = new Set();
|
const neededDecorKeys = new Set();
|
||||||
const neededCropKeys = new Set();
|
const neededCropKeys = new Set();
|
||||||
|
|
||||||
|
const voxelOffset = 12;
|
||||||
|
|
||||||
for (let y = startY; y < endY; y++) {
|
for (let y = startY; y < endY; y++) {
|
||||||
for (let x = startX; x < endX; x++) {
|
for (let x = startX; x < endX; x++) {
|
||||||
const key = `${x},${y}`;
|
const key = `${x},${y}`;
|
||||||
|
const tile = this.tiles[y][x];
|
||||||
|
|
||||||
|
if (tile) {
|
||||||
|
neededTileKeys.add(key);
|
||||||
|
if (!this.visibleTiles.has(key)) {
|
||||||
|
const sprite = this.tilePool.get();
|
||||||
|
sprite.setTexture(tile.type);
|
||||||
|
const screenPos = this.iso.toScreen(x, y);
|
||||||
|
sprite.setPosition(screenPos.x + this.offsetX, screenPos.y + this.offsetY);
|
||||||
|
sprite.setDepth(this.iso.getDepth(x, y));
|
||||||
|
this.visibleTiles.set(key, sprite);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// DECORATIONS
|
|
||||||
const decor = this.decorationsMap.get(key);
|
const decor = this.decorationsMap.get(key);
|
||||||
if (decor) {
|
if (decor) {
|
||||||
neededDecorKeys.add(key);
|
neededDecorKeys.add(key);
|
||||||
@@ -410,37 +547,35 @@ class TerrainSystem {
|
|||||||
const sprite = this.decorationPool.get();
|
const sprite = this.decorationPool.get();
|
||||||
const screenPos = this.iso.toScreen(x, y);
|
const screenPos = this.iso.toScreen(x, y);
|
||||||
|
|
||||||
sprite.setPosition(screenPos.x + this.offsetX, screenPos.y + this.offsetY);
|
sprite.setPosition(screenPos.x + this.offsetX, screenPos.y + this.offsetY - voxelOffset);
|
||||||
|
|
||||||
// Origin adjusted for volumetric sprites
|
|
||||||
// Trees/Rocks usually look best with origin (0.5, 0.9) to sit on the ground
|
|
||||||
if (decor.type.includes('house') || decor.type.includes('market') || decor.type.includes('structure')) {
|
if (decor.type.includes('house') || decor.type.includes('market') || decor.type.includes('structure')) {
|
||||||
sprite.setOrigin(0.5, 0.8);
|
sprite.setOrigin(0.5, 0.8);
|
||||||
} else {
|
} else {
|
||||||
sprite.setOrigin(0.5, 0.9);
|
sprite.setOrigin(0.5, 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Texture & Scale
|
|
||||||
sprite.setTexture(decor.type);
|
sprite.setTexture(decor.type);
|
||||||
sprite.setScale(decor.scale || 1.0);
|
sprite.setScale(decor.scale || 1.0);
|
||||||
|
|
||||||
|
if (decor.alpha !== undefined) {
|
||||||
|
sprite.setAlpha(decor.alpha);
|
||||||
|
}
|
||||||
|
|
||||||
sprite.setDepth(this.iso.getDepth(x, y) + 1);
|
sprite.setDepth(this.iso.getDepth(x, y) + 1);
|
||||||
this.visibleDecorations.set(key, sprite);
|
this.visibleDecorations.set(key, sprite);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CROPS
|
|
||||||
const crop = this.cropsMap.get(key);
|
const crop = this.cropsMap.get(key);
|
||||||
if (crop) {
|
if (crop) {
|
||||||
neededCropKeys.add(key);
|
neededCropKeys.add(key);
|
||||||
if (!this.visibleCrops.has(key)) {
|
if (!this.visibleCrops.has(key)) {
|
||||||
const sprite = this.cropPool.get();
|
const sprite = this.cropPool.get();
|
||||||
const screenPos = this.iso.toScreen(x, y);
|
const screenPos = this.iso.toScreen(x, y);
|
||||||
sprite.setPosition(screenPos.x + this.offsetX, screenPos.y + this.offsetY);
|
sprite.setPosition(screenPos.x + this.offsetX, screenPos.y + this.offsetY - voxelOffset);
|
||||||
sprite.setTexture(`crop_stage_${crop.stage}`);
|
sprite.setTexture(`crop_stage_${crop.stage}`);
|
||||||
// Crop origin
|
|
||||||
sprite.setOrigin(0.5, 1);
|
sprite.setOrigin(0.5, 1);
|
||||||
// Crop depth
|
|
||||||
sprite.setDepth(this.iso.getDepth(x, y) + 0.5);
|
sprite.setDepth(this.iso.getDepth(x, y) + 0.5);
|
||||||
this.visibleCrops.set(key, sprite);
|
this.visibleCrops.set(key, sprite);
|
||||||
}
|
}
|
||||||
@@ -448,7 +583,13 @@ class TerrainSystem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup
|
for (const [key, sprite] of this.visibleTiles) {
|
||||||
|
if (!neededTileKeys.has(key)) {
|
||||||
|
sprite.setVisible(false);
|
||||||
|
this.tilePool.release(sprite);
|
||||||
|
this.visibleTiles.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
for (const [key, sprite] of this.visibleDecorations) {
|
for (const [key, sprite] of this.visibleDecorations) {
|
||||||
if (!neededDecorKeys.has(key)) {
|
if (!neededDecorKeys.has(key)) {
|
||||||
sprite.setVisible(false);
|
sprite.setVisible(false);
|
||||||
|
|||||||
@@ -233,6 +233,11 @@ class WeatherSystem {
|
|||||||
|
|
||||||
// Depth just above overlay (-1000)
|
// Depth just above overlay (-1000)
|
||||||
this.rainEmitter.setDepth(-990);
|
this.rainEmitter.setDepth(-990);
|
||||||
|
|
||||||
|
// Play Sound
|
||||||
|
if (this.scene.soundManager) {
|
||||||
|
this.scene.soundManager.playRainSound();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clearWeather() {
|
clearWeather() {
|
||||||
@@ -240,6 +245,11 @@ class WeatherSystem {
|
|||||||
this.rainEmitter.destroy();
|
this.rainEmitter.destroy();
|
||||||
this.rainEmitter = null;
|
this.rainEmitter = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop Sound
|
||||||
|
if (this.scene.soundManager) {
|
||||||
|
this.scene.soundManager.stopRainSound();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Getters for Other Systems ---
|
// --- Getters for Other Systems ---
|
||||||
@@ -260,4 +270,9 @@ class WeatherSystem {
|
|||||||
const hour = this.gameTime;
|
const hour = this.gameTime;
|
||||||
return hour >= 7 && hour < 18;
|
return hour >= 7 && hour < 18;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isHordeNight() {
|
||||||
|
// Every 3rd night is a Horde Night
|
||||||
|
return this.dayCount > 0 && this.dayCount % 3 === 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
44
src/utils/Compression.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// Utility class for string compression (LZW algorithm)
|
||||||
|
// Used to reduce save file size in localStorage
|
||||||
|
class Compression {
|
||||||
|
static compress(s) {
|
||||||
|
if (s == null) return "";
|
||||||
|
var dict = {}, data = (s + "").split(""), out = [], currChar, phrase = data[0], code = 256;
|
||||||
|
for (var i = 1; i < data.length; i++) {
|
||||||
|
currChar = data[i];
|
||||||
|
if (dict[phrase + currChar] != null) {
|
||||||
|
phrase += currChar;
|
||||||
|
} else {
|
||||||
|
out.push(phrase.length > 1 ? dict[phrase] : phrase.charCodeAt(0));
|
||||||
|
dict[phrase + currChar] = code;
|
||||||
|
code++;
|
||||||
|
phrase = currChar;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.push(phrase.length > 1 ? dict[phrase] : phrase.charCodeAt(0));
|
||||||
|
for (var i = 0; i < out.length; i++) {
|
||||||
|
out[i] = String.fromCharCode(out[i]);
|
||||||
|
}
|
||||||
|
return out.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
static decompress(s) {
|
||||||
|
if (s == null) return "";
|
||||||
|
if (s == "") return null;
|
||||||
|
var dict = {}, data = (s + "").split(""), currChar = data[0], oldPhrase = currChar, out = [currChar], code = 256, phrase;
|
||||||
|
for (var i = 1; i < data.length; i++) {
|
||||||
|
var currCode = data[i].charCodeAt(0);
|
||||||
|
if (currCode < 256) {
|
||||||
|
phrase = data[i];
|
||||||
|
} else {
|
||||||
|
phrase = dict[currCode] ? dict[currCode] : (oldPhrase + oldPhrase.charAt(0));
|
||||||
|
}
|
||||||
|
out.push(phrase);
|
||||||
|
currChar = phrase.charAt(0);
|
||||||
|
dict[code] = oldPhrase + currChar;
|
||||||
|
code++;
|
||||||
|
oldPhrase = phrase;
|
||||||
|
}
|
||||||
|
return out.join("");
|
||||||
|
}
|
||||||
|
}
|
||||||
99
src/utils/Pathfinding.js
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
class Pathfinding {
|
||||||
|
constructor(scene) {
|
||||||
|
this.scene = scene;
|
||||||
|
// Offsets for 4 directions (Isometric grid)
|
||||||
|
this.dirs = [
|
||||||
|
{ x: 0, y: -1 }, // Up
|
||||||
|
{ x: 0, y: 1 }, // Down
|
||||||
|
{ x: -1, y: 0 }, // Left
|
||||||
|
{ x: 1, y: 0 } // Right
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
findPath(startX, startY, endX, endY, limit = 50) {
|
||||||
|
// Simple A* implementation
|
||||||
|
const startNode = { x: startX, y: startY, g: 0, h: 0, f: 0, parent: null };
|
||||||
|
const openList = [startNode];
|
||||||
|
const closedSet = new Set();
|
||||||
|
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
while (openList.length > 0 && count < limit) {
|
||||||
|
count++;
|
||||||
|
|
||||||
|
// Sort by F cost (can be optimized with PriorityQueue)
|
||||||
|
openList.sort((a, b) => a.f - b.f);
|
||||||
|
const currentNode = openList.shift();
|
||||||
|
|
||||||
|
// Reached destination? (or close enough)
|
||||||
|
if (currentNode.x === endX && currentNode.y === endY) {
|
||||||
|
return this.reconstructPath(currentNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = `${currentNode.x},${currentNode.y}`;
|
||||||
|
closedSet.add(key);
|
||||||
|
|
||||||
|
// Explore neighbors
|
||||||
|
for (const dir of this.dirs) {
|
||||||
|
const neighborX = currentNode.x + dir.x;
|
||||||
|
const neighborY = currentNode.y + dir.y;
|
||||||
|
|
||||||
|
if (closedSet.has(`${neighborX},${neighborY}`)) continue;
|
||||||
|
|
||||||
|
// Check collision
|
||||||
|
if (!this.isValidMove(neighborX, neighborY)) continue;
|
||||||
|
|
||||||
|
const gScore = currentNode.g + 1;
|
||||||
|
const hScore = Math.abs(neighborX - endX) + Math.abs(neighborY - endY);
|
||||||
|
const fScore = gScore + hScore;
|
||||||
|
|
||||||
|
// Check if already in openList with lower G
|
||||||
|
const existing = openList.find(n => n.x === neighborX && n.y === neighborY);
|
||||||
|
if (existing) {
|
||||||
|
if (gScore < existing.g) {
|
||||||
|
existing.g = gScore;
|
||||||
|
existing.f = fScore;
|
||||||
|
existing.parent = currentNode;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
openList.push({ x: neighborX, y: neighborY, g: gScore, h: hScore, f: fScore, parent: currentNode });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit reached or no path - return simplified direction if possible
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
reconstructPath(node) {
|
||||||
|
const path = [];
|
||||||
|
let curr = node;
|
||||||
|
while (curr.parent) {
|
||||||
|
path.unshift({ x: curr.x, y: curr.y });
|
||||||
|
curr = curr.parent;
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
isValidMove(x, y) {
|
||||||
|
const terrain = this.scene.terrainSystem;
|
||||||
|
if (!terrain) return true;
|
||||||
|
|
||||||
|
// Bounds
|
||||||
|
if (x < 0 || y < 0 || x >= terrain.width || y >= terrain.height) return false;
|
||||||
|
|
||||||
|
// Tile Type
|
||||||
|
const tile = terrain.tiles[y][x];
|
||||||
|
if (tile.type.name === 'water') return false;
|
||||||
|
|
||||||
|
// Decorations collision
|
||||||
|
const key = `${x},${y}`;
|
||||||
|
if (terrain.decorationsMap.has(key)) {
|
||||||
|
const decor = terrain.decorationsMap.get(key);
|
||||||
|
const solidTypes = ['tree', 'stone', 'bush', 'wall', 'ruin', 'fence', 'house', 'gravestone'];
|
||||||
|
if (solidTypes.includes(decor.type)) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -268,18 +268,43 @@ class TextureGenerator {
|
|||||||
x.fillStyle = 'gray'; x.fillRect(8, 12, 16, 4);
|
x.fillStyle = 'gray'; x.fillRect(8, 12, 16, 4);
|
||||||
c.refresh();
|
c.refresh();
|
||||||
}
|
}
|
||||||
|
if (!scene.textures.exists('item_hoe')) {
|
||||||
|
const c = scene.textures.createCanvas('item_hoe', 32, 32);
|
||||||
|
const x = c.getContext();
|
||||||
|
x.fillStyle = 'brown'; x.fillRect(14, 10, 4, 18); // Ročaj
|
||||||
|
x.fillStyle = 'gray';
|
||||||
|
x.fillRect(10, 8, 12, 4); // Rezilo motike
|
||||||
|
c.refresh();
|
||||||
|
}
|
||||||
|
if (!scene.textures.exists('item_sword')) {
|
||||||
|
const c = scene.textures.createCanvas('item_sword', 32, 32);
|
||||||
|
const x = c.getContext();
|
||||||
|
x.fillStyle = 'brown'; x.fillRect(14, 10, 4, 14); // Ročaj
|
||||||
|
x.fillStyle = 'gray';
|
||||||
|
x.fillRect(12, 8, 8, 12); // Rezilo meča
|
||||||
|
x.fillStyle = 'gold'; x.fillRect(14, 21, 4, 2); // Guard
|
||||||
|
c.refresh();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static createItemSprites(scene) {
|
static createItemSprites(scene) {
|
||||||
// Placeholder item generation
|
// Placeholder item generation
|
||||||
const items = ['wood', 'stone', 'seed', 'item_bone']; // Ensure item_bone is here
|
const items = [
|
||||||
items.forEach(it => {
|
{ name: 'wood', color: '#8B4513' }, // Rjava
|
||||||
|
{ name: 'stone', color: '#808080' }, // Siva
|
||||||
|
{ name: 'seeds', color: '#90EE90' }, // Svetlo zelena
|
||||||
|
{ name: 'wheat', color: '#FFD700' }, // Zlata
|
||||||
|
{ name: 'item_bone', color: '#F5F5DC' } // Beige
|
||||||
|
];
|
||||||
|
items.forEach(item => {
|
||||||
|
const it = typeof item === 'string' ? item : item.name;
|
||||||
|
const color = typeof item === 'string' ? 'gold' : item.color;
|
||||||
const k = (it.startsWith('item_')) ? it : 'item_' + it;
|
const k = (it.startsWith('item_')) ? it : 'item_' + it;
|
||||||
if (!scene.textures.exists(k)) {
|
if (!scene.textures.exists(k)) {
|
||||||
const c = scene.textures.createCanvas(k, 32, 32);
|
const c = scene.textures.createCanvas(k, 32, 32);
|
||||||
const x = c.getContext();
|
const x = c.getContext();
|
||||||
x.clearRect(0, 0, 32, 32);
|
x.clearRect(0, 0, 32, 32);
|
||||||
x.fillStyle = 'gold';
|
x.fillStyle = color;
|
||||||
x.beginPath(); x.arc(16, 16, 10, 0, Math.PI * 2); x.fill();
|
x.beginPath(); x.arc(16, 16, 10, 0, Math.PI * 2); x.fill();
|
||||||
c.refresh();
|
c.refresh();
|
||||||
}
|
}
|
||||||
|
|||||||
124
src/workers/pathfinding.worker.js
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
/* eslint-disable no-restricted-globals */
|
||||||
|
|
||||||
|
// Preprost A* Pathfinding v Web Workerju
|
||||||
|
let grid = [];
|
||||||
|
let width = 0;
|
||||||
|
let height = 0;
|
||||||
|
|
||||||
|
self.onmessage = function (e) {
|
||||||
|
const { type, payload, id } = e.data;
|
||||||
|
|
||||||
|
if (type === 'UPDATE_GRID') {
|
||||||
|
grid = payload.grid;
|
||||||
|
width = payload.width;
|
||||||
|
height = payload.height;
|
||||||
|
// console.log('🕷️ Worker: Grid updated', width, height);
|
||||||
|
}
|
||||||
|
else if (type === 'FIND_PATH') {
|
||||||
|
if (!grid || grid.length === 0) {
|
||||||
|
self.postMessage({ type: 'PATH_FOUND', id, path: [] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { startX, startY, endX, endY } = payload;
|
||||||
|
const path = findPath(startX, startY, endX, endY);
|
||||||
|
self.postMessage({ type: 'PATH_FOUND', id, path });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function findPath(startX, startY, endX, endY) {
|
||||||
|
// Preverimo meje
|
||||||
|
if (startX < 0 || startX >= width || startY < 0 || startY >= height) return [];
|
||||||
|
if (endX < 0 || endX >= width || endY < 0 || endY >= height) return [];
|
||||||
|
|
||||||
|
// Če cilj ni prehoden, vrni prazno (ali najdi najbližjo točko - za zdaj prazno)
|
||||||
|
if (!isWalkable(endX, endY)) return [];
|
||||||
|
|
||||||
|
const openSet = [];
|
||||||
|
const closedSet = new Set();
|
||||||
|
const cameFrom = new Map();
|
||||||
|
|
||||||
|
const gScore = new Map();
|
||||||
|
const fScore = new Map();
|
||||||
|
|
||||||
|
const startKey = `${startX},${startY}`;
|
||||||
|
const endKey = `${endX},${endY}`;
|
||||||
|
|
||||||
|
openSet.push({ x: startX, y: startY, f: heuristic(startX, startY, endX, endY) });
|
||||||
|
gScore.set(startKey, 0);
|
||||||
|
fScore.set(startKey, heuristic(startX, startY, endX, endY));
|
||||||
|
|
||||||
|
let iterations = 0;
|
||||||
|
|
||||||
|
while (openSet.length > 0) {
|
||||||
|
iterations++;
|
||||||
|
if (iterations > 1000) return []; // Safety break
|
||||||
|
|
||||||
|
// Najdi vozlišče z najnižjim fScore
|
||||||
|
openSet.sort((a, b) => a.f - b.f);
|
||||||
|
const current = openSet.shift();
|
||||||
|
const currentKey = `${current.x},${current.y}`;
|
||||||
|
|
||||||
|
if (current.x === endX && current.y === endY) {
|
||||||
|
return reconstructPath(cameFrom, current);
|
||||||
|
}
|
||||||
|
|
||||||
|
closedSet.add(currentKey);
|
||||||
|
|
||||||
|
const neighbors = getNeighbors(current.x, current.y);
|
||||||
|
for (const neighbor of neighbors) {
|
||||||
|
const neighborKey = `${neighbor.x},${neighbor.y}`;
|
||||||
|
if (closedSet.has(neighborKey)) continue;
|
||||||
|
|
||||||
|
const tentativeGScore = (gScore.get(currentKey) || 0) + 1;
|
||||||
|
|
||||||
|
if (tentativeGScore < (gScore.get(neighborKey) || Infinity)) {
|
||||||
|
cameFrom.set(neighborKey, current);
|
||||||
|
gScore.set(neighborKey, tentativeGScore);
|
||||||
|
const f = tentativeGScore + heuristic(neighbor.x, neighbor.y, endX, endY);
|
||||||
|
fScore.set(neighborKey, f);
|
||||||
|
|
||||||
|
if (!openSet.some(n => n.x === neighbor.x && n.y === neighbor.y)) {
|
||||||
|
openSet.push({ x: neighbor.x, y: neighbor.y, f: f });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return []; // Pot ni najdena
|
||||||
|
}
|
||||||
|
|
||||||
|
function isWalkable(x, y) {
|
||||||
|
if (x < 0 || x >= width || y < 0 || y >= height) return false;
|
||||||
|
// Predpostavljamo 1D array: y * width + x. 0 = prehodno, 1 = ovira.
|
||||||
|
return grid[y * width + x] === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNeighbors(x, y) {
|
||||||
|
const neighbors = [];
|
||||||
|
const dirs = [[0, 1], [0, -1], [1, 0], [-1, 0]]; // Samo 4 smeri (Manhattan)
|
||||||
|
|
||||||
|
for (const [dx, dy] of dirs) {
|
||||||
|
const nx = x + dx;
|
||||||
|
const ny = y + dy;
|
||||||
|
if (isWalkable(nx, ny)) {
|
||||||
|
neighbors.push({ x: nx, y: ny });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return neighbors;
|
||||||
|
}
|
||||||
|
|
||||||
|
function heuristic(x1, y1, x2, y2) {
|
||||||
|
return Math.abs(x1 - x2) + Math.abs(y1 - y2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reconstructPath(cameFrom, current) {
|
||||||
|
const totalPath = [current];
|
||||||
|
let currentKey = `${current.x},${current.y}`;
|
||||||
|
|
||||||
|
while (cameFrom.has(currentKey)) {
|
||||||
|
current = cameFrom.get(currentKey);
|
||||||
|
currentKey = `${current.x},${current.y}`;
|
||||||
|
totalPath.unshift(current);
|
||||||
|
}
|
||||||
|
return totalPath;
|
||||||
|
}
|
||||||