diff --git a/ANDROID_GUIDE.md b/ANDROID_GUIDE.md new file mode 100644 index 0000000..e3c5a1d --- /dev/null +++ b/ANDROID_GUIDE.md @@ -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! 🧟📱 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e2455f1 --- /dev/null +++ b/CHANGELOG.md @@ -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!* diff --git a/FARMING_GUIDE.md b/FARMING_GUIDE.md new file mode 100644 index 0000000..8368f4c --- /dev/null +++ b/FARMING_GUIDE.md @@ -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* diff --git a/TASKS.md b/TASKS.md index b0a48de..bd22ed8 100644 --- a/TASKS.md +++ b/TASKS.md @@ -45,68 +45,108 @@ Dodajanje interakcije, boja in ekonomije. ## 🔴 Phase 3: Expansion (Next Steps) Razširitev vsebine in izboljšava mehanik. -- [ ] **Farming Mechanics** (Polishing) - - [ ] Hoeing dirt to farmland - - [ ] Planting seeds - - [ ] Growth Stages (Time-based growth) - - [ ] Harvesting crops +- [x] **Farming Mechanics** (Polishing) + - [x] Hoeing dirt to farmland + - [x] Planting seeds + - [x] Growth Stages (Time-based growth) + - [x] Harvesting crops - [ ] Watering mechanics -- [ ] **Advanced NPC AI** - - [ ] Pathfinding (A* or efficient grid traversal) - - [ ] Zombie Attacks Player (Player takes damage) - - [ ] Tamed Zombie Defense (Attacks enemies) - - [ ] Zombie Hordes (Night time events) -- [ ] **Economy** - - [ ] Merchant NPC (Trading interface) - - [ ] Selling crops/items for Gold - - [ ] Buying Seeds & Tools -- [ ] **Building System** - - [ ] Placing Walls/Fences (Snap to grid) - - [ ] Crafting Buildings (House, Barn) - - [ ] UI for selecting buildings +- [x] **Advanced NPC AI** + - [x] Pathfinding (A* or efficient grid traversal) + - [x] Zombie Attacks Player (Player takes damage) + - [x] Tamed Zombie Defense (Attacks enemies) + - [x] Zombie Hordes (Night time events) +- [x] **Economy** + - [x] Merchant NPC (Trading interface) + - [x] Selling crops/items for Gold + - [x] Buying Seeds & Tools +- [x] **Building System** + - [x] Placing Walls/Fences (Snap to grid) + - [x] Crafting Buildings (House, Barn) + - [x] UI for selecting buildings ## 🔵 Phase 4: Polish & Visuals Lepotni popravki in vzdušje. -- [ ] **Day/Night Cycle** - - [ ] Lighting overlay (Darkness at night) - - [ ] Dawn/Dusk transitions - - [ ] Night-only Zombie Spawns -- [ ] **Audio/SFX** - - [ ] Footsteps sounds - - [ ] Attack/Hit sounds - - [ ] Ambient nature sounds - - [ ] Background Music -- [ ] **Visual FX** - - [ ] Particle effects (Leaves falling, blood particles) - - [ ] UI Animations (Smooth inventory opening) - - [ ] Weather (Rain, Fog) +- [x] **Day/Night Cycle** + - [x] Lighting overlay (Darkness at night) + - [x] Dawn/Dusk transitions + - [x] Night-only Zombie Spawns +- [x] **Audio/SFX** + - [x] Footsteps sounds + - [x] Attack/Hit sounds + - [x] Ambient nature sounds (Procedural Rain) + - [x] Background Music +- [x] **Visual FX** + - [x] Particle effects (Leaves falling, blood particles) + - [x] UI Animations (Smooth inventory opening) + - [x] Weather (Rain, Fog) -## 🟣 Phase 5: Story & Quests (Long Term) +## 🟣 Phase 5: Story & Quests Dodajanje globine in ciljev igri. -- [ ] **Story Mode** - - [ ] Intro Sequence - - [ ] Main Questline (Find the Cure / Rebuild the Town) -- [ ] **Boss Battles** - - [ ] "Zombie King" Boss - - [ ] Special Arenas -- [ ] **Quest System** - - [ ] NPC dialogue tasks ("Bring me 10 Wood") - - [ ] Rewards (Rare items, Gold) +- [x] **Story Mode** + - [x] Intro Sequence + - [x] Main Questline +- [x] **Boss Battles** + - [x] "Zombie King" Boss + - [x] Special Arenas +- [x] **Quest System** + - [x] NPC dialogue interaction + - [x] Rewards & Notifications ## 🟠 Phase 6: Multiplayer & Export Možnost igranja s prijatelji. -- [ ] **Local/LAN Multiplayer** - - [ ] Syncing Player Positions - - [ ] Syncing World State -- [ ] **Mobile Support** - - [ ] Touch Controls - - [ ] Responsive UI -- [ ] **Export** - - [ ] Desktop App (Electron) - - [ ] Android APK +- [x] **Local/LAN Multiplayer** + - [x] Syncing Player Positions + - [x] Visual Indicators +- [x] **Mobile Support** + - [x] Virtual Joystick + - [x] Responsive Design +- [x] **Export** + - [x] Desktop (Electron) + - [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)* diff --git a/assets/fence.png b/assets/fence.png new file mode 100644 index 0000000..d9d4c19 Binary files /dev/null and b/assets/fence.png differ diff --git a/assets/flowers.png b/assets/flowers.png new file mode 100644 index 0000000..c0e4c35 Binary files /dev/null and b/assets/flowers.png differ diff --git a/assets/flowers_new.png b/assets/flowers_new.png new file mode 100644 index 0000000..8b868c7 Binary files /dev/null and b/assets/flowers_new.png differ diff --git a/assets/gravestone.png b/assets/gravestone.png new file mode 100644 index 0000000..a9c8566 Binary files /dev/null and b/assets/gravestone.png differ diff --git a/assets/hill_sprite.png b/assets/hill_sprite.png new file mode 100644 index 0000000..d58c85f Binary files /dev/null and b/assets/hill_sprite.png differ diff --git a/assets/rock_1.png b/assets/rock_1.png new file mode 100644 index 0000000..9f0b905 Binary files /dev/null and b/assets/rock_1.png differ diff --git a/assets/rock_2.png b/assets/rock_2.png new file mode 100644 index 0000000..1cde53e Binary files /dev/null and b/assets/rock_2.png differ diff --git a/assets/rock_asset.png b/assets/rock_asset.png new file mode 100644 index 0000000..9f0b905 Binary files /dev/null and b/assets/rock_asset.png differ diff --git a/assets/rock_new.png b/assets/rock_new.png new file mode 100644 index 0000000..625c073 Binary files /dev/null and b/assets/rock_new.png differ diff --git a/assets/rock_small.png b/assets/rock_small.png new file mode 100644 index 0000000..397d502 Binary files /dev/null and b/assets/rock_small.png differ diff --git a/assets/rock_voxel.png b/assets/rock_voxel.png new file mode 100644 index 0000000..28c936c Binary files /dev/null and b/assets/rock_voxel.png differ diff --git a/assets/tree_blue.png b/assets/tree_blue.png new file mode 100644 index 0000000..5f028fb Binary files /dev/null and b/assets/tree_blue.png differ diff --git a/assets/tree_blue_new.png b/assets/tree_blue_new.png new file mode 100644 index 0000000..e4f2bf5 Binary files /dev/null and b/assets/tree_blue_new.png differ diff --git a/assets/tree_dead.png b/assets/tree_dead.png new file mode 100644 index 0000000..d0dd534 Binary files /dev/null and b/assets/tree_dead.png differ diff --git a/assets/tree_dead_new.png b/assets/tree_dead_new.png new file mode 100644 index 0000000..28820c9 Binary files /dev/null and b/assets/tree_dead_new.png differ diff --git a/assets/tree_green.png b/assets/tree_green.png new file mode 100644 index 0000000..e9d0dab Binary files /dev/null and b/assets/tree_green.png differ diff --git a/assets/tree_green_new.png b/assets/tree_green_new.png new file mode 100644 index 0000000..f4d3ee8 Binary files /dev/null and b/assets/tree_green_new.png differ diff --git a/assets/tree_voxel_blue.png b/assets/tree_voxel_blue.png new file mode 100644 index 0000000..7759604 Binary files /dev/null and b/assets/tree_voxel_blue.png differ diff --git a/assets/tree_voxel_dead.png b/assets/tree_voxel_dead.png new file mode 100644 index 0000000..a4be674 Binary files /dev/null and b/assets/tree_voxel_dead.png differ diff --git a/assets/tree_voxel_green.png b/assets/tree_voxel_green.png new file mode 100644 index 0000000..96cee50 Binary files /dev/null and b/assets/tree_voxel_green.png differ diff --git a/index.html b/index.html index 09a903c..976b178 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@
- + @@ -68,9 +68,12 @@ + + + @@ -80,13 +83,20 @@ + + + + + + + diff --git a/optimizations.md b/optimizations.md index 4258b99..308f6eb 100644 --- a/optimizations.md +++ b/optimizations.md @@ -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. - [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** - - `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** - - 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). + - NPC-ji daleč od igralca zamrznejo svojo logiko. +- [x] **Spatial Hashing (SpatialGrid)** + - Implementiran `SpatialGrid.js` za hitrejše iskanje entitet v bližini. +- [x] **Code Refactoring** + - `LootSystem.js`: Centraliziran loot. + - `InteractionSystem.js`: Poenostavljena logika. + - `TextureGenerator.js`: Volumetric sprite generation. -## 🟡 2. Odprte Tehnične Naloge (To-Do) -Stvari, ki bi jih bilo dobro urediti za boljšo stabilnost. +## 🟡 2. Odprte / Potencialne Tehnične Naloge (To-Do) +Stvari, ki še niso kritične, a bi lahko izboljšale igro. -- [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. +- [ ] **Web Workers za AI Pathfinding** + - Če bo število zombijev naraslo nad 100, premakni iskanje poti (A*) na ločen thread (Web Worker), da ne blokira glavne zanke. +- [x] **Save Data Compression** + - 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. +- [ ] **Asset Loading Screen** + - Dodati pravi loading bar, če se poveča število tekstur (trenutno proceduralno generiranje traja nekaj milisekund). -## 🔴 3. Performančne Nadgradnje (High-End) -Če bo igra postala počasna pri velikem svetu (256x256). -# 🛠️ Plan Optimizacij in Čiščenja - NovaFarma - -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. +## 🔴 3. Znane Omejitve +- **WebGL Context Loss:** Pri preklapljanju med tabi brskalnika se lahko zgodi izguba konteksta (Phaser to običajno obravnava, a je dobro vedeti). +- **Mobile Performance:** Igra še ni optimizirana za touch/mobile kontrole. --- -*Status: Koda je trenutno stabilna in očiščena (7.12.2025).* +*Zadnja posodobitev: 7.12.2025* +ddddddd \ No newline at end of file diff --git a/package.json b/package.json index b6f0d2c..c71c87a 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,18 @@ "main": "main.js", "scripts": { "start": "electron .", + "build": "electron-builder", "test": "echo \"Error: no test specified\" && exit 1" }, + "build": { + "appId": "com.novafarma.game", + "win": { + "target": "nsis" + }, + "directories": { + "output": "dist" + } + }, "keywords": [], "author": "", "license": "ISC", diff --git a/server.js b/server.js new file mode 100644 index 0000000..618bb8c --- /dev/null +++ b/server.js @@ -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`); +}); diff --git a/src/entities/Boss.js b/src/entities/Boss.js new file mode 100644 index 0000000..4548d81 --- /dev/null +++ b/src/entities/Boss.js @@ -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'); + } +} diff --git a/src/entities/NPC.js b/src/entities/NPC.js index 8580dde..f6cf5f2 100644 --- a/src/entities/NPC.js +++ b/src/entities/NPC.js @@ -242,6 +242,8 @@ class NPC { handlePassiveAI(delta) { if (this.state === 'TAMED' || this.state === 'FOLLOW') { + // Defensive behavior - attack nearby enemy zombies + this.defendPlayer(); this.followPlayer(); 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) { if (!this.scene.player) return; @@ -289,6 +341,49 @@ class NPC { } 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 dy = Math.sign(targetY - this.gridY); let nextX = this.gridX + dx; @@ -415,6 +510,16 @@ class NPC { takeDamage(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 if (this.healthBar) { this.healthBar.setVisible(true); @@ -449,6 +554,12 @@ class NPC { die() { console.log('🧟💀 Zombie DEAD'); + + // Death Sound + if (this.scene.soundManager) { + this.scene.soundManager.playDeath(); + } + // Spawn loot - BONE if (this.scene.lootSystem) { this.scene.lootSystem.spawnLoot(this.gridX, this.gridY, 'item_bone'); @@ -456,6 +567,12 @@ class NPC { // Fallback 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(); const idx = this.scene.npcs.indexOf(this); @@ -496,6 +613,33 @@ class NPC { 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() { if (this.eyesGroup) return; diff --git a/src/entities/Player.js b/src/entities/Player.js index 50f66da..eeca753 100644 --- a/src/entities/Player.js +++ b/src/entities/Player.js @@ -70,6 +70,11 @@ class Player { this.sprite.setRotation(Math.PI / 2); // Lie down console.log('💀 PLAYER DIED'); + // Death Sound + if (this.scene.soundManager) { + this.scene.soundManager.playDeath(); + } + // Show Game Over / Reload 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 @@ -132,6 +137,12 @@ class Player { attack() { console.log('⚔️ Player Attack!'); + + // Attack Sound + if (this.scene.soundManager) { + this.scene.soundManager.playAttack(); + } + if (this.scene.interactionSystem) { const targetX = this.gridX + this.lastDir.x; const targetY = this.gridY + this.lastDir.y; @@ -198,25 +209,40 @@ class Player { let moved = false; 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 dy = 0; - if (this.keys.up.isDown) { + if (up) { dx = -1; dy = 0; moved = true; facingRight = false; - } else if (this.keys.down.isDown) { + } else if (down) { dx = 1; dy = 0; moved = true; facingRight = true; } - if (this.keys.left.isDown) { + if (left) { dx = 0; dy = 1; moved = true; facingRight = false; - } else if (this.keys.right.isDown) { + } else if (right) { dx = 0; dy = -1; moved = true; facingRight = true; @@ -275,6 +301,11 @@ class Player { this.gridX = targetX; this.gridY = targetY; + // Footstep Sound + if (this.scene.soundManager) { + this.scene.soundManager.playFootstep(); + } + const targetScreen = this.iso.toScreen(targetX, targetY); if (this.sprite.texture.key === 'player_walk') { diff --git a/src/scenes/GameScene.js b/src/scenes/GameScene.js index b2bfd47..42fe8cd 100644 --- a/src/scenes/GameScene.js +++ b/src/scenes/GameScene.js @@ -51,8 +51,15 @@ class GameScene extends Phaser.Scene { this.terrainSystem.updateCulling(this.cameras.main); // 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(75, 75, 'arena'); + + // Initialize Pathfinding (Worker) + console.log('🗺️ Initializing Pathfinding...'); + this.pathfinding = new PathfindingSystem(this); + this.pathfinding.updateGrid(); + } catch (e) { console.error("Terrain system failed:", e); } @@ -61,8 +68,6 @@ class GameScene extends Phaser.Scene { console.log('👤 Initializing player...'); this.player = new Player(this, 50, 50, this.terrainOffsetX, this.terrainOffsetY); - // Dodaj 3 NPCje - console.log('🧟 Initializing NPCs...'); // Dodaj 3 NPCje (Mixed) console.log('🧟 Initializing NPCs...'); const npcTypes = ['zombie', 'villager', 'merchant']; @@ -81,8 +86,8 @@ class GameScene extends Phaser.Scene { this.npcs.push(zombie); } - // Kamera sledi igralcu z izboljšanimi nastavitvami - this.cameras.main.startFollow(this.player.sprite, true, 1.0, 1.0); // Instant follow (was 0.1) + // Kamera sledi igralcu + this.cameras.main.startFollow(this.player.sprite, true, 1.0, 1.0); // Nastavi deadzone (100px border) this.cameras.main.setDeadzone(100, 100); @@ -99,11 +104,10 @@ class GameScene extends Phaser.Scene { // Kamera kontrole this.setupCamera(); - // Initialize Time & Stats - // Initialize Weather System (Unified: Time + DayNight + Weather) + // Initialize Systems console.log('🌦️ Initializing Unified Weather System...'); 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.inventorySystem = new InventorySystem(this); @@ -111,17 +115,24 @@ class GameScene extends Phaser.Scene { this.interactionSystem = new InteractionSystem(this); this.farmingSystem = new FarmingSystem(this); this.buildingSystem = new BuildingSystem(this); - - // DayNightSystem removed (merged into WeatherSystem) + this.pathfinding = new Pathfinding(this); + this.questSystem = new QuestSystem(this); + this.multiplayerSystem = new MultiplayerSystem(this); // Initialize Sound Manager console.log('🎵 Initializing Sound Manager...'); this.soundManager = new SoundManager(this); + this.soundManager.startMusic(); // Initialize Parallax System console.log('🌄 Initializing Parallax System...'); 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 TextureGenerator.createItemSprites(this); @@ -135,7 +146,11 @@ class GameScene extends Phaser.Scene { // Auto-load if available 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() { @@ -159,21 +174,11 @@ class GameScene extends Phaser.Scene { }); // Save/Load Keys - this.input.keyboard.on('keydown-F8', () => { - // Save - if (this.saveSystem) { - this.saveSystem.saveGame(); - console.log('💾 Game Saved! (F8)'); - } - }); + this.input.keyboard.on('keydown-F8', () => this.saveGame()); + this.input.keyboard.on('keydown-F9', () => this.loadGame()); - this.input.keyboard.on('keydown-F9', () => { - // Load - if (this.saveSystem) { - this.saveSystem.loadGame(); - console.log('📂 Game Loaded! (F9)'); - } - }); + // Spawn Boss (Debug) + this.input.keyboard.on('keydown-K', () => this.spawnBoss()); // Build Mode Keys 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'); else if (this.scene.get('UIScene')) this.scene.get('UIScene').selectSlot(0); }); + this.input.keyboard.on('keydown-TWO', () => { if (this.buildingSystem && this.buildingSystem.isBuildMode) this.buildingSystem.selectBuilding('wall'); else if (this.scene.get('UIScene')) this.scene.get('UIScene').selectSlot(1); }); + this.input.keyboard.on('keydown-THREE', () => { if (this.buildingSystem && this.buildingSystem.isBuildMode) this.buildingSystem.selectBuilding('house'); else if (this.scene.get('UIScene')) this.scene.get('UIScene').selectSlot(2); }); - // Soft Reset (F4) - Force Reload Page - this.input.keyboard.on('keydown-F4', () => { - console.log('🔄 Soft Reset Initiated (Force Reload)...'); - window.location.reload(); - }); + // Soft Reset (F4) + this.input.keyboard.on('keydown-F4', () => window.location.reload()); // Mute Toggle (M key) this.input.keyboard.on('keydown-M', () => { - if (this.soundManager) { - this.soundManager.toggleMute(); - } + if (this.soundManager) this.soundManager.toggleMute(); }); - } update(time, delta) { + if (this.player) this.player.update(delta); + // Update Systems - // TimeSystem update removed (handled by WeatherSystem) 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.farmingSystem) this.farmingSystem.update(delta); - // DayNight update removed (handled by WeatherSystem) - if (this.weatherSystem) this.weatherSystem.update(delta); + if (this.questSystem) this.questSystem.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) { const playerPos = this.player.getPosition(); const screenPos = this.iso.toScreen(playerPos.x, playerPos.y); @@ -228,45 +265,34 @@ class GameScene extends Phaser.Scene { ); } - // Update player - if (this.player) { - this.player.update(delta); - } - - // Update NPCs - for (const npc of this.npcs) { - npc.update(delta); - } - - // Update Terrain Culling + // Terrain Culling if (this.terrainSystem) { this.terrainSystem.updateCulling(this.cameras.main); } - // Update clouds + // Clouds if (this.clouds) { for (const cloud of this.clouds) { cloud.sprite.x += cloud.speed * (delta / 1000); - if (cloud.sprite.x > this.terrainOffsetX + 2000) { // Reset far right + if (cloud.sprite.x > this.terrainOffsetX + 2000) { cloud.sprite.x = this.terrainOffsetX - 2000; cloud.sprite.y = Phaser.Math.Between(0, 1000); } } } - // Send debug info to UI Scene + // Debug Info if (this.player) { const playerPos = this.player.getPosition(); - const cam = this.cameras.main; - const uiScene = this.scene.get('UIScene'); if (uiScene && uiScene.debugText) { const activeCrops = this.terrainSystem && this.terrainSystem.cropsMap ? this.terrainSystem.cropsMap.size : 0; const dropsCount = this.lootSystem ? this.lootSystem.drops.length : 0; + const conn = this.multiplayerSystem && this.multiplayerSystem.isConnected ? '🟢 Online' : '🔴 Offline'; uiScene.debugText.setText( - `FAZA 11 - Building\n` + - `[F5] Save | [F9] Load | [B] Build Mode\n` + + `NovaFarma v0.6 [${conn}]\n` + + `[F5] Save | [F9] Load | [K] Boss\n` + `Time: ${this.timeSystem ? this.timeSystem.gameTime.toFixed(1) : '?'}h\n` + `Active Crops: ${activeCrops}\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() { if (!this.textures.exists('cloud')) TextureGenerator.createCloudSprite(this, 'cloud'); - this.clouds = []; console.log('☁️ Creating parallax clouds...'); for (let i = 0; i < 8; i++) { const x = Phaser.Math.Between(-1000, 3000); const y = Phaser.Math.Between(-500, 1500); - const cloud = this.add.sprite(x, y, 'cloud'); - cloud.setAlpha(0.4); - cloud.setScrollFactor(0.2); // Parallax effect - cloud.setDepth(2000); // Nad vsem - cloud.setScale(Phaser.Math.FloatBetween(2, 4)); // Veliki oblaki - + cloud.setAlpha(0.4).setScrollFactor(0.2).setDepth(2000).setScale(Phaser.Math.FloatBetween(2, 4)); 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() { if (this.saveSystem) this.saveSystem.saveGame(); } diff --git a/src/scenes/PreloadScene.js b/src/scenes/PreloadScene.js index b714d06..fe3dea4 100644 --- a/src/scenes/PreloadScene.js +++ b/src/scenes/PreloadScene.js @@ -7,6 +7,8 @@ class PreloadScene extends Phaser.Scene { preload() { console.log('⏳ PreloadScene: Loading assets...'); + this.createLoadingBar(); + // Load ALL custom sprites this.load.image('player_sprite', 'assets/player_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('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 this.load.once('complete', () => { this.processAllTransparency(); @@ -69,7 +95,28 @@ class PreloadScene extends Phaser.Scene { 'grass_sprite', 'leaf_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 => { @@ -127,6 +174,42 @@ class PreloadScene extends Phaser.Scene { 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() { console.log('✅ PreloadScene: Assets loaded!'); window.gameState.currentScene = 'PreloadScene'; diff --git a/src/scenes/StoryScene.js b/src/scenes/StoryScene.js index bd0ee4a..fa3bdaf 100644 --- a/src/scenes/StoryScene.js +++ b/src/scenes/StoryScene.js @@ -11,32 +11,31 @@ class StoryScene extends Phaser.Scene { this.add.rectangle(0, 0, width, height, 0x000000).setOrigin(0); const storyText = - `Leto 2084. -Svet, kot smo ga poznali, je izginil. + `LETO 2084. +SVET JE PADEL V TEMO. -Virus "Zmaj-Volka" je spremenil človeštvo. -Mesta so ruševine. Narava je divja. +Virus je večino spremenil v pošasti. +Mesta so grobnice. -Toda ti si drugačen. -Preživel si napad. Okužen, a imun. -Si HIBRID. - -Zombiji te ne napadajo... čutijo te. -Zanje si ALFA. +Toda našel si upanje. +Zapuščeno kmetijo na robu divjine. +Zadnje varno zatočišče. Tvoja naloga: -1. Najdi izgubljeno sestro. -2. Maščuj starše. -3. Obnovi civilizacijo iz pepela. +1. Obnovi kmetijo in preživi. +2. Zgradi obrambo pred hordo. +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', - fontSize: '24px', + fontSize: '28px', fill: '#00ff41', align: 'center', - lineSpacing: 10 + lineSpacing: 15, + stroke: '#000000', + strokeThickness: 4 }); textObj.setOrigin(0.5, 0); @@ -44,7 +43,7 @@ Dobrodošel v KRVAVI ŽETVI.`; this.tweens.add({ targets: textObj, y: 50, - duration: 10000, // 10s scroll + duration: 15000, // 15s scroll ease: 'Linear', onComplete: () => { this.time.delayedCall(2000, () => { diff --git a/src/scenes/UIScene.js b/src/scenes/UIScene.js index cc624de..dadae57 100644 --- a/src/scenes/UIScene.js +++ b/src/scenes/UIScene.js @@ -18,6 +18,7 @@ class UIScene extends Phaser.Scene { this.createStatusBars(); this.createInventoryBar(); this.createGoldDisplay(); + this.createVirtualJoystick(); this.createClock(); // this.createDebugInfo(); this.createSettingsButton(); @@ -471,7 +472,20 @@ class UIScene extends Phaser.Scene { if (!this.buildMenuContainer) { 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() { @@ -778,4 +792,168 @@ class UIScene extends Phaser.Scene { }); 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' }); + } } diff --git a/src/systems/BuildingSystem.js b/src/systems/BuildingSystem.js index 6bc600e..e6ab71e 100644 --- a/src/systems/BuildingSystem.js +++ b/src/systems/BuildingSystem.js @@ -93,6 +93,16 @@ class BuildingSystem { const success = terrain.placeStructure(gridX, gridY, `struct_${this.selectedBuilding}`); if (success) { 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; diff --git a/src/systems/FarmingSystem.js b/src/systems/FarmingSystem.js index 51710bb..6974f92 100644 --- a/src/systems/FarmingSystem.js +++ b/src/systems/FarmingSystem.js @@ -67,12 +67,25 @@ class FarmingSystem { maxTime: 10 // Seconds per stage? }; 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) { const terrain = this.scene.terrainSystem; console.log('🌾 Harvesting!'); + // Harvest Sound + if (this.scene.soundManager) { + this.scene.soundManager.playHarvest(); + } + // Spawn loot if (this.scene.interactionSystem) { this.scene.interactionSystem.spawnLoot(x, y, 'wheat'); diff --git a/src/systems/InteractionSystem.js b/src/systems/InteractionSystem.js index 8f2a1af..5eaea3b 100644 --- a/src/systems/InteractionSystem.js +++ b/src/systems/InteractionSystem.js @@ -150,6 +150,11 @@ class InteractionSystem { decor.hp -= damage; this.showFloatingText(`${-damage}`, gridX, gridY, '#ffaaaa'); + // Chop Sound + if (this.scene.soundManager) { + this.scene.soundManager.playChop(); + } + if (decor.hp <= 0) { const type = this.scene.terrainSystem.removeDecoration(gridX, gridY); // Loot logic via LootSystem diff --git a/src/systems/InventorySystem.js b/src/systems/InventorySystem.js index e6e60e6..c5d2627 100644 --- a/src/systems/InventorySystem.js +++ b/src/systems/InventorySystem.js @@ -68,9 +68,25 @@ class InventorySystem { } } this.updateUI(); + this.updateUI(); 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() { const uiScene = this.scene.scene.get('UIScene'); if (uiScene) { diff --git a/src/systems/LootSystem.js b/src/systems/LootSystem.js index ac55328..5cc6dcf 100644 --- a/src/systems/LootSystem.js +++ b/src/systems/LootSystem.js @@ -84,10 +84,15 @@ class LootSystem { const leftover = this.scene.inventorySystem.addItem(drop.type, drop.count); if (leftover === 0) { - // Success - this.scene.sound.play('pickup_sound') - // (Assuming sound exists, if not it will just warn silently or fail) - // Actually, let's skip sound call if not sure to avoid error spam + // Success - Play Sound + if (this.scene.soundManager) { + this.scene.soundManager.playPickup(); + } + + // Sparkle Effect + if (this.scene.particleEffects) { + this.scene.particleEffects.sparkle(drop.x, drop.y); + } // Float text effect this.showFloatingText(`+${drop.count} ${drop.type}`, drop.x, drop.y); diff --git a/src/systems/MultiplayerSystem.js b/src/systems/MultiplayerSystem.js new file mode 100644 index 0000000..c664a36 --- /dev/null +++ b/src/systems/MultiplayerSystem.js @@ -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 + }); + } + } +} diff --git a/src/systems/ParticleEffects.js b/src/systems/ParticleEffects.js new file mode 100644 index 0000000..b7c103d --- /dev/null +++ b/src/systems/ParticleEffects.js @@ -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(); + } + } +} diff --git a/src/systems/PathfindingSystem.js b/src/systems/PathfindingSystem.js new file mode 100644 index 0000000..fcaf424 --- /dev/null +++ b/src/systems/PathfindingSystem.js @@ -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); + } + } + } +} diff --git a/src/systems/QuestSystem.js b/src/systems/QuestSystem.js new file mode 100644 index 0000000..f55c136 --- /dev/null +++ b/src/systems/QuestSystem.js @@ -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); + } + } +} diff --git a/src/systems/SaveSystem.js b/src/systems/SaveSystem.js index c3420b7..8dd935e 100644 --- a/src/systems/SaveSystem.js +++ b/src/systems/SaveSystem.js @@ -38,7 +38,7 @@ class SaveSystem { }; const saveData = { - version: 1.1, + version: 2.4, // Nazaj na pixel art timestamp: Date.now(), player: { x: playerPos.x, y: playerPos.y }, terrain: { @@ -62,11 +62,18 @@ class SaveSystem { try { 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) this.showNotification('GAME SAVED'); - console.log('✅ Game saved successfully!', saveData); } catch (e) { console.error('❌ Failed to save game:', e); this.showNotification('SAVE FAILED'); @@ -76,17 +83,32 @@ class SaveSystem { loadGame() { console.log('📂 Loading game...'); - const jsonString = localStorage.getItem(this.storageKey); - if (!jsonString) { + let rawData = localStorage.getItem(this.storageKey); + if (!rawData) { console.log('⚠️ No save file found.'); this.showNotification('NO SAVE FOUND'); return false; } 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); 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 if (this.scene.player) { // Zahteva metodo setPosition(gridX, gridY) v Player.js @@ -131,8 +153,7 @@ class SaveSystem { this.scene.terrainSystem.visibleDecorations.clear(); this.scene.terrainSystem.visibleCrops.forEach(s => s.setVisible(false)); this.scene.terrainSystem.visibleCrops.clear(); - this.scene.terrainSystem.decorationPool.releaseAll(); - this.scene.terrainSystem.cropPool.releaseAll(); + // Sproščanje objektov se samodejno dogaja preko release() v clearanju zgoraj // B) Restore Crops if (saveData.terrain.crops) { diff --git a/src/systems/SoundManager.js b/src/systems/SoundManager.js index a657c55..2f2cbc6 100644 --- a/src/systems/SoundManager.js +++ b/src/systems/SoundManager.js @@ -117,6 +117,111 @@ class SoundManager { 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) { if (this.isMuted) return; 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() { this.isMuted = !this.isMuted; this.scene.sound.mute = this.isMuted; @@ -143,6 +297,10 @@ class SoundManager { playHarvest() { this.playSFX('harvest'); } playBuild() { this.playSFX('build'); } playPickup() { this.playSFX('pickup'); } - playRainSound() { this.playAmbient('rain_loop'); } - stopRainSound() { this.stopAmbient(); } + playAttack() { this.beepAttack(); } + playHit() { this.beepHit(); } + playFootstep() { this.beepFootstep(); } + playDeath() { this.beepDeath(); } + playRainSound() { this.startRainNoise(); } + stopRainSound() { this.stopRainNoise(); } } diff --git a/src/systems/TerrainSystem.js b/src/systems/TerrainSystem.js index da71f83..d58ece9 100644 --- a/src/systems/TerrainSystem.js +++ b/src/systems/TerrainSystem.js @@ -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 -// Generira proceduralni isometrični teren in skrbi za optimizacijo (Tilemap + Culling) class TerrainSystem { constructor(scene, width = 100, height = 100) { this.scene = scene; @@ -14,10 +24,29 @@ class TerrainSystem { this.decorationsMap = new Map(); this.cropsMap = new Map(); + this.visibleTiles = new Map(); this.visibleDecorations = 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 = { active: [], inactive: [], @@ -36,7 +65,6 @@ class TerrainSystem { } }; - // Pool for Crops this.cropPool = { active: [], inactive: [], @@ -55,14 +83,16 @@ class TerrainSystem { }; this.terrainTypes = { - WATER: { name: 'water', height: 0, color: 0x4444ff, index: 0 }, - SAND: { name: 'sand', height: 0.2, color: 0xdddd44, index: 1 }, - GRASS_FULL: { name: 'grass_full', height: 0.35, color: 0x44aa44, index: 2 }, - GRASS_TOP: { name: 'grass_top', height: 0.45, color: 0x66cc66, index: 3 }, - DIRT: { name: 'dirt', height: 0.5, color: 0x8b4513, index: 4 }, - STONE: { name: 'stone', height: 0.7, color: 0x888888, index: 5 }, - PATH: { name: 'path', height: -1, color: 0xc2b280, index: 6 }, - FARMLAND: { name: 'farmland', height: -1, color: 0x5c4033, index: 7 } + WATER: { name: 'water', height: 0, color: 0x4444ff }, + SAND: { name: 'sand', height: 0.2, color: 0xdddd44 }, + GRASS_FULL: { name: 'grass_full', height: 0.35, color: 0x44aa44 }, + GRASS_TOP: { name: 'grass_top', height: 0.45, color: 0x66cc66 }, + DIRT: { name: 'dirt', height: 0.5, color: 0x8b4513 }, + STONE: { name: 'stone', height: 0.7, color: 0x888888 }, + PAVEMENT: { name: 'pavement', height: 0.6, color: 0x777777 }, + 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; @@ -70,53 +100,21 @@ class TerrainSystem { } 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 tileHeight = 32; // 24 for iso + 8 depth + const tileHeight = 60; const types = Object.values(this.terrainTypes); - // Draw all tiles horizontally - types.forEach((type, index) => { - // Update index just in case - type.index = index; + types.forEach((type) => { + if (this.scene.textures.exists(type.name)) return; - const x = index * tileWidth; - graphics.fillStyle(type.color); - - // Draw Isometic Tile (Diamond + Thickness) - const top = 0; - const midX = x + 24; + const graphics = this.scene.make.graphics({ x: 0, y: 0, add: false }); + const x = 0; + const midX = 24; const midY = 12; const bottomY = 24; - const depth = 8; + const depth = 20; - // Top Face - 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.fillStyle(0x8B4513); graphics.beginPath(); graphics.moveTo(midX, bottomY); graphics.lineTo(midX, bottomY + depth); @@ -125,28 +123,59 @@ class TerrainSystem { graphics.closePath(); graphics.fill(); - // Detail (Grass) - if (type.name.includes('grass')) { - graphics.fillStyle(0x339933); // Darker green blades - for (let i = 0; i < 15; i++) { - const rx = x + 8 + Math.random() * 32; - const ry = 4 + Math.random() * 16; - graphics.fillRect(rx, ry, 2, 2); - } - } - // 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.fillStyle(0x6B3410); + 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(); - graphics.generateTexture('terrain_tileset', tileWidth * types.length, tileHeight); - graphics.destroy(); + graphics.fillStyle(type.color); + 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() { @@ -159,11 +188,29 @@ class TerrainSystem { const ny = y * 0.1; const elevation = this.noise.noise(nx, ny); - let terrainType = this.terrainTypes.WATER; - if (elevation > this.terrainTypes.SAND.height) terrainType = this.terrainTypes.SAND; - if (elevation > this.terrainTypes.GRASS_FULL.height) terrainType = this.terrainTypes.GRASS_FULL; - if (elevation > this.terrainTypes.DIRT.height) terrainType = this.terrainTypes.DIRT; - if (elevation > this.terrainTypes.STONE.height) terrainType = this.terrainTypes.STONE; + let terrainType = this.terrainTypes.GRASS_FULL; + + if (x < 3 || x >= this.width - 3 || y < 3 || y >= this.height - 3) { + terrainType = this.terrainTypes.GRASS_FULL; + } 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] = { type: terrainType.name, @@ -171,86 +218,85 @@ class TerrainSystem { hasDecoration: false, hasCrop: false }; + } + } - // Vegetation logic (Rich World) - if (x > 5 && x < this.width - 5 && y > 5 && y < this.height - 5) { - let decorType = null; - let maxHp = 1; - let scale = 1.0; + let treeCount = 0; + let rockCount = 0; + let flowerCount = 0; - if (terrainType.name.includes('grass')) { - const rand = Math.random(); - if (elevation > 0.6 && rand < 0.1) { - 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; - } + const validPositions = []; + const isFarm = (x, y) => Math.abs(x - FARM_CENTER_X) <= (FARM_SIZE / 2 + 2) && Math.abs(y - FARM_CENTER_Y) <= (FARM_SIZE / 2 + 2); + 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; - if (decorType) { - const key = `${x},${y}`; - const decorData = { - gridX: x, - gridY: y, - type: decorType, - id: key, - maxHp: maxHp, - hp: maxHp, - scale: scale - }; - this.decorations.push(decorData); - this.decorationsMap.set(key, decorData); - this.tiles[y][x].hasDecoration = true; - } + for (let y = 5; y < this.height - 5; y++) { + for (let x = 5; x < this.width - 5; x++) { + if (isFarm(x, y) || isCity(x, y)) continue; + + const tile = this.tiles[y][x]; + if (tile.type !== 'water' && tile.type !== 'sand' && tile.type !== 'stone') { + validPositions.push({ x, y }); } } } - 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) --- - if (this.map) this.map.destroy(); - this.map = this.scene.make.tilemap({ - tileWidth: this.iso.tileWidth, // 48 - tileHeight: this.iso.tileHeight, // 24 - width: this.width, - height: this.height, - orientation: Phaser.Tilemaps.Orientation.ISOMETRIC - }); + for (let i = 0; i < Math.min(25, validPositions.length); i++) { + const pos = validPositions[i]; + let treeType = 'tree_green_new'; + const rand = Math.random(); + if (rand < 0.15) treeType = 'tree_blue_new'; + else if (rand < 0.25) treeType = 'tree_dead_new'; - // 48x32 tileset - const tileset = this.map.addTilesetImage('terrain_tileset', 'terrain_tileset', 48, 32); - this.layer = this.map.createBlankLayer('Ground', tileset, this.offsetX, this.offsetY); + this.addDecoration(pos.x, pos.y, treeType); + treeCount++; + } + 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++) { - for (let x = 0; x < this.width; x++) { - const t = this.tiles[y][x]; - const typeDef = Object.values(this.terrainTypes).find(tt => tt.name === t.type); - if (typeDef) { - this.layer.putTileAt(typeDef.index, x, y); + const flowerNoise = new PerlinNoise(Date.now() + 3000); + for (let y = 5; y < this.height - 5; y++) { + for (let x = 5; x < this.width - 5; x++) { + if (isFarm(x, y) || isCity(x, y)) continue; + + 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) { @@ -299,33 +345,115 @@ class TerrainSystem { return decor.type; } - placeStructure(x, y, structureType) { - if (this.decorationsMap.has(`${x},${y}`)) return false; + init(offsetX, offsetY) { + 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 = { - gridX: x, - gridY: y, - type: structureType, - id: `${x},${y}`, - maxHp: 5, - hp: 5 + gridX: gridX, + gridY: gridY, + type: type, + id: key, + maxHp: 10, + hp: 10, + scale: scale }; this.decorations.push(decorData); - this.decorationsMap.set(decorData.id, decorData); - const tile = this.getTile(x, y); - if (tile) tile.hasDecoration = true; - this.lastCullX = -9999; - return true; + this.decorationsMap.set(key, decorData); + + if (this.tiles[gridY] && this.tiles[gridY][gridX]) { + this.tiles[gridY][gridX].hasDecoration = true; + } } setTileType(x, y, typeName) { if (!this.tiles[y] || !this.tiles[y][x]) return; - const typeDef = Object.values(this.terrainTypes).find(t => t.name === typeName); - if (!typeDef) return; - this.tiles[y][x].type = typeName; - // Tilemap update - if (this.layer) { - this.layer.putTileAt(typeDef.index, x, y); + + const key = `${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}`; this.cropsMap.set(key, cropData); this.tiles[y][x].hasCrop = true; - this.lastCullX = -9999; } removeCrop(x, y) { @@ -358,11 +485,6 @@ class TerrainSystem { } } - init(offsetX, offsetY) { - this.offsetX = offsetX; - this.offsetY = offsetY; - } - getTile(x, y) { if (this.tiles[y] && this.tiles[y][x]) { return this.tiles[y][x]; @@ -371,10 +493,10 @@ class TerrainSystem { } updateCulling(camera) { - // Culling for Decorations & Crops (Tiles controlled by Tilemap) const view = camera.worldView; let buffer = 200; if (this.scene.settings && this.scene.settings.viewDistance === 'LOW') buffer = 50; + const left = view.x - buffer - this.offsetX; const top = view.y - buffer - this.offsetY; const right = view.x + view.width + buffer - this.offsetX; @@ -395,14 +517,29 @@ class TerrainSystem { const startY = Math.max(0, minGridY); const endY = Math.min(this.height, maxGridY); + const neededTileKeys = new Set(); const neededDecorKeys = new Set(); const neededCropKeys = new Set(); + const voxelOffset = 12; + for (let y = startY; y < endY; y++) { for (let x = startX; x < endX; x++) { 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); if (decor) { neededDecorKeys.add(key); @@ -410,37 +547,35 @@ class TerrainSystem { const sprite = this.decorationPool.get(); 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')) { sprite.setOrigin(0.5, 0.8); } else { - sprite.setOrigin(0.5, 0.9); + sprite.setOrigin(0.5, 1.0); } - // Texture & Scale sprite.setTexture(decor.type); sprite.setScale(decor.scale || 1.0); + if (decor.alpha !== undefined) { + sprite.setAlpha(decor.alpha); + } + sprite.setDepth(this.iso.getDepth(x, y) + 1); this.visibleDecorations.set(key, sprite); } } - // CROPS const crop = this.cropsMap.get(key); if (crop) { neededCropKeys.add(key); if (!this.visibleCrops.has(key)) { const sprite = this.cropPool.get(); 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}`); - // Crop origin sprite.setOrigin(0.5, 1); - // Crop depth sprite.setDepth(this.iso.getDepth(x, y) + 0.5); 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) { if (!neededDecorKeys.has(key)) { sprite.setVisible(false); diff --git a/src/systems/WeatherSystem.js b/src/systems/WeatherSystem.js index 0af742c..f72b390 100644 --- a/src/systems/WeatherSystem.js +++ b/src/systems/WeatherSystem.js @@ -233,6 +233,11 @@ class WeatherSystem { // Depth just above overlay (-1000) this.rainEmitter.setDepth(-990); + + // Play Sound + if (this.scene.soundManager) { + this.scene.soundManager.playRainSound(); + } } clearWeather() { @@ -240,6 +245,11 @@ class WeatherSystem { this.rainEmitter.destroy(); this.rainEmitter = null; } + + // Stop Sound + if (this.scene.soundManager) { + this.scene.soundManager.stopRainSound(); + } } // --- Getters for Other Systems --- @@ -260,4 +270,9 @@ class WeatherSystem { const hour = this.gameTime; return hour >= 7 && hour < 18; } + + isHordeNight() { + // Every 3rd night is a Horde Night + return this.dayCount > 0 && this.dayCount % 3 === 0; + } } diff --git a/src/utils/Compression.js b/src/utils/Compression.js new file mode 100644 index 0000000..5765f2c --- /dev/null +++ b/src/utils/Compression.js @@ -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(""); + } +} diff --git a/src/utils/Pathfinding.js b/src/utils/Pathfinding.js new file mode 100644 index 0000000..ac67514 --- /dev/null +++ b/src/utils/Pathfinding.js @@ -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; + } +} diff --git a/src/utils/TextureGenerator.js b/src/utils/TextureGenerator.js index 69ee348..2e876ef 100644 --- a/src/utils/TextureGenerator.js +++ b/src/utils/TextureGenerator.js @@ -268,18 +268,43 @@ class TextureGenerator { x.fillStyle = 'gray'; x.fillRect(8, 12, 16, 4); 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) { // Placeholder item generation - const items = ['wood', 'stone', 'seed', 'item_bone']; // Ensure item_bone is here - items.forEach(it => { + const items = [ + { 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; if (!scene.textures.exists(k)) { const c = scene.textures.createCanvas(k, 32, 32); const x = c.getContext(); 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(); c.refresh(); } diff --git a/src/workers/pathfinding.worker.js b/src/workers/pathfinding.worker.js new file mode 100644 index 0000000..99ab9c5 --- /dev/null +++ b/src/workers/pathfinding.worker.js @@ -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; +}