This commit is contained in:
2025-12-07 21:31:44 +01:00
parent 4a0ca267ea
commit 974141c08c
52 changed files with 2485 additions and 397 deletions

57
ANDROID_GUIDE.md Normal file
View File

@@ -0,0 +1,57 @@
# 📱 Building NovaFarma for Android
This guide explains how to pack your NovaFarma game into an Android APK using **Capacitor**.
## Prerequisites
1. **Node.js** (Already installed)
2. **Android Studio** (Download and install from [developer.android.com](https://developer.android.com/studio))
- Ensure you install the **Android SDK** and create a **Virtual Device** (AVD) or enable USB debugging on your Android phone.
## Steps
### 1. Install Capacitor
In your project terminal (Game Folder), run:
```bash
npm install @capacitor/core @capacitor/cli @capacitor/android
```
### 2. Initialize Capacitor
Initialize the project config. Since your project serves files directly from the root:
```bash
npx cap init NovaFarma com.novafarma.game --web-dir .
```
### 3. Add Android Platform
This creates the native Android project folder.
```bash
npx cap add android
```
### 4. Sync Project
This copies your web assets (html, css, js) into the Android native project.
```bash
npx cap sync
```
### 5. Open in Android Studio
```bash
npx cap open android
```
This will launch Android Studio with your project loaded.
### 6. Run on Device
- Connect your Android phone via USB (Developer Mode enabled) OR start an Emulator in Android Studio.
- Click the green **Run (Play)** button in the top toolbar.
- The game should launch on your device!
### 7. Export APK (For Sharing)
To create a standalone file you can send to friends:
- Go to **Build > Build Bundle(s) / APK(s) > Build APK(s)**.
- Once finished, a notification will appear. Click "Locate" to find your `app-debug.apk`.
## 🎮 Mobile Controls
We have already enabled **Virtual Joystick** support in the `UIScene`.
- A joystick appears on the bottom-left of the screen.
- Use it to control the character without a keyboard!
Happy Slaying! 🧟📱

49
CHANGELOG.md Normal file
View File

@@ -0,0 +1,49 @@
# NovaFarma Changelog
## [v0.6.0] - 2025-12-07 (Massive Update)
### New Features 🚀
- **Boss Battles:**
- Added `Boss.js` entity (Zombie King) with unique stats and visuals.
- Implemented Boss Skills: `smashAttack` and `summonMinions`.
- Added Boss Spawning via debug key 'K' and special events.
- Added "Horde Warning" visual effect.
- **Mobile & Touch Support 📱:**
- Implemented **Virtual Joystick** in `UIScene.js` for movement on touch devices.
- Added `user-scalable=no` to `index.html` for better mobile experience.
- Created `ANDROID_GUIDE.md` with instructions for building APKs via Capacitor.
- **Multiplayer (Local/LAN) 🌐:**
- Created `server.js` (Node.js + Socket.io) for backend.
- Created `MultiplayerSystem.js` for frontend synchronization.
- Implemented Player Position Sync (seeing other players move).
- Added Visual Indicators (Name tags, connection status).
- **Quest System Overhaul 📜:**
- Added **NPC Dialogue Interaction** ('E' key opens quest dialog).
- Added "Quest Givers" (Villager gives farming quests, Merchant gives defense quests).
- Improved UI with "Accept/Decline" popup.
- Added `interact()` method to NPCs.
- **World Generation & Structures 🌍:**
- Added **Special Arenas** generation in `TerrainSystem.js`.
- Added `placeStructure` method for spawning precrafted areas (Ruins, Arenas).
- Restored and optimized procedural vegetation (Trees, Rocks, Flowers).
- **Export & Build 📦:**
- Configured `package.json` with `electron-builder` for creating Windows .exe.
- Added build scripts (`npm run build`).
### Fixes 🔧
- Fixed `InventorySystem` missing methods (`getItemCount`, `addGold`).
- Fixed `TerrainSystem` vegetation generation bug.
- Fixed `NPC` death logic and doubled code.
- Fixed `UIScene` duplicate calls.
### Technical 💻
- **New Files:** `Boss.js`, `MultiplayerSystem.js`, `server.js`, `ANDROID_GUIDE.md`, `CHANGELOG.md`.
- **Updated Systems:** `GameScene`, `TerrainSystem`, `QuestSystem`, `UIScene`, `NPC`.
---
*Ready for Gameplay Testing & Distribution!*

56
FARMING_GUIDE.md Normal file
View File

@@ -0,0 +1,56 @@
# 🌾 Farming System Guide
## Kako uporabljati Farming sistem
### 1. Pridobi orodja in semena
V inventoryju že imaš:
- 🪓 **Axe** (Sekira) - za sekanje dreves
- ⛏️ **Pickaxe** (Kramp) - za kopanje kamenja
- 🚜 **Hoe** (Motika) - za kopanje njive
- 🌱 **Seeds** (Semena) - 5x za začetek
### 2. Kopaj njivo
1. **Izberi motiko** (Hoe) v inventoryju
2. **Klikni na travo ali zemljo** (grass/dirt tile)
3. Tile se spremeni v **farmland** (rjava njiva)
### 3. Zasadi semena
1. **Izberi seeds** v inventoryju
2. **Klikni na farmland** tile
3. Posajeno seme se prikaže (stage 1)
### 4. Rast
- Crops **rastejo samodejno** čez čas
- **Stopnje rasti:**
- Stage 1: 🌱 Seme (Seeds)
- Stage 2: 🌿 Mladica
- Stage 3: 🌾 Raste
- Stage 4: ✨ Zrelo (Ripe) - ready to harvest!
- Stage 5: 💀 Ovene (Withered)
- **Čas:** Vsaka stopnja traja ~5 sekund (za testiranje)
### 5. Pobiranje pridelkov
1. **Klikni na zrelo rastlino** (stage 4, zlata)
2. Dobiš:
- 🌾 **Wheat** (Pšenica) - hrana/prodaja
- 🌱 **Seeds** (1-2x) - za ponovno sajenje
3. Farmland ostane pripravljena za novo sejanje
## 🎮 Osnovni proces
```
Grass/Dirt → (Hoe) → Farmland → (Seeds) → Growing → Harvest → Repeat
```
## ⏰ Časovni sistem
- Growth je vezan na **realni čas** (5s per stage = 20s za polno rast)
- V prihodnosti: integracija z Day/Night ciklom
## 💡 Nasveti
- **Posadi več naenkrat**: Več kot sadiš, več dobiš!
- **Zberi semena**: Harvest daje nazaj 1-2 semena
- **Prodaj pridelek**: Wheat lahko prodaš merchantu za zlato
- **Ohrani farmland**: Po harvestu ostane pripravljena za novo sejanje
---
*Last Updated: 2025-12-07*

142
TASKS.md
View File

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

BIN
assets/fence.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 KiB

BIN
assets/flowers.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
assets/flowers_new.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 KiB

BIN
assets/gravestone.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 KiB

BIN
assets/hill_sprite.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 KiB

BIN
assets/rock_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
assets/rock_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
assets/rock_asset.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
assets/rock_new.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 KiB

BIN
assets/rock_small.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

BIN
assets/rock_voxel.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 KiB

BIN
assets/tree_blue.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

BIN
assets/tree_blue_new.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 KiB

BIN
assets/tree_dead.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

BIN
assets/tree_dead_new.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 KiB

BIN
assets/tree_green.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

BIN
assets/tree_green_new.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

BIN
assets/tree_voxel_blue.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 KiB

BIN
assets/tree_voxel_dead.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 489 KiB

BIN
assets/tree_voxel_green.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 556 KiB

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<!-- Suppress Electron Security Warning for Dev -->
<meta http-equiv="Content-Security-Policy"
content="script-src 'self' 'unsafe-inline' 'unsafe-eval' blob: data:; object-src 'self';">
@@ -68,9 +68,12 @@
<script src="src/utils/TextureGenerator.js"></script>
<script src="src/utils/ObjectPool.js"></script>
<script src="src/utils/SpatialGrid.js"></script>
<script src="src/utils/Pathfinding.js"></script>
<script src="src/utils/Compression.js"></script>
<!-- Systems -->
<script src="src/systems/TerrainSystem.js"></script>
<script src="src/systems/PathfindingSystem.js"></script>
<script src="src/systems/SaveSystem.js"></script>
<!-- TimeSystem merged into WeatherSystem -->
<script src="src/systems/StatsSystem.js"></script>
@@ -80,13 +83,20 @@
<script src="src/systems/FarmingSystem.js"></script>
<script src="src/systems/BuildingSystem.js"></script>
<script src="src/systems/WeatherSystem.js"></script>
<script src="src/systems/QuestSystem.js"></script>
<!-- DayNightSystem merged into WeatherSystem -->
<script src="src/systems/SoundManager.js"></script>
<script src="src/systems/ParallaxSystem.js"></script>
<script src="src/systems/ParticleEffects.js"></script>
<!-- Multiplayer -->
<script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
<script src="src/systems/MultiplayerSystem.js"></script>
<!-- Entities -->
<script src="src/entities/Player.js"></script>
<script src="src/entities/NPC.js"></script>
<script src="src/entities/Boss.js"></script>
<!-- Game Files -->
<script src="src/scenes/BootScene.js"></script>

View File

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

View File

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

63
server.js Normal file
View File

@@ -0,0 +1,63 @@
const app = require('express')();
const server = require('http').createServer(app);
const io = require('socket.io')(server, {
cors: {
origin: "*", // Allow all origins for dev
methods: ["GET", "POST"]
}
});
const players = {};
io.on('connection', (socket) => {
console.log('Player connected:', socket.id);
// Initialize player data
players[socket.id] = {
id: socket.id,
x: 0,
y: 0,
anim: 'idle'
};
// Send the current list of players to the new player
socket.emit('currentPlayers', players);
// Notify other players about the new player
socket.broadcast.emit('newPlayer', players[socket.id]);
// Handle Disconnect
socket.on('disconnect', () => {
console.log('Player disconnected:', socket.id);
delete players[socket.id];
io.emit('playerDisconnected', socket.id);
});
// Handle Movement
socket.on('playerMovement', (data) => {
if (players[socket.id]) {
players[socket.id].x = data.x;
players[socket.id].y = data.y;
players[socket.id].anim = data.anim;
players[socket.id].flipX = data.flipX;
// Broadcast to others (excluding self)
socket.broadcast.emit('playerMoved', players[socket.id]);
}
});
// Handle World State (Simple Block Placement sync)
// Warning: No validation/persistence in this MVP
socket.on('worldAction', (action) => {
// action: { type: 'build', x: 10, y: 10, building: 'fence' }
socket.broadcast.emit('worldAction', action);
});
});
const PORT = 3000;
server.listen(PORT, () => {
console.log(`SERVER RUNNING on port ${PORT}`);
console.log(`To play multiplayer:`);
console.log(`1. Run 'node server.js' in a terminal`);
console.log(`2. Start the game clients`);
});

127
src/entities/Boss.js Normal file
View File

@@ -0,0 +1,127 @@
class Boss extends NPC {
constructor(scene, gridX, gridY) {
super(scene, gridX, gridY, scene.terrainOffsetX, scene.terrainOffsetY, 'boss');
this.maxHp = 500;
this.hp = this.maxHp;
this.moveSpeed = 70; // Slower than normal zombies
// Cooldowns
this.summonCooldown = 12000;
this.smashCooldown = 6000;
this.currentSkillTimer = 0;
}
createSprite() {
super.createSprite();
// Customize appearance
this.sprite.setScale(2.0); // Big Boss
this.sprite.setTint(0x6600cc); // Dark Purple
// Add a crown or something? (Text)
this.crown = this.scene.add.text(this.sprite.x, this.sprite.y - 80, '👑', { fontSize: '30px' });
this.crown.setOrigin(0.5);
this.crown.setDepth(this.sprite.depth + 100);
}
update(delta) {
super.update(delta);
// Update Crown position
if (this.crown && this.sprite) {
this.crown.setPosition(this.sprite.x, this.sprite.y - 80);
this.crown.setDepth(this.sprite.depth + 100);
}
if (this.state === 'CHASE' || this.state === 'WANDER') {
this.handleBossSkills(delta);
}
}
handleBossSkills(delta) {
if (!this.scene.player) return;
this.currentSkillTimer += delta;
const dist = Phaser.Math.Distance.Between(this.gridX, this.gridY, this.scene.player.gridX, this.scene.player.gridY);
// Smash Attack (AoE close range)
if (this.currentSkillTimer > this.smashCooldown && dist < 4) {
this.smashAttack();
this.currentSkillTimer = 0; // Reset timer partially? No, full reset for simpler logic
}
// Summon Minions (Long range or cooldown)
else if (this.currentSkillTimer > this.summonCooldown) {
this.summonMinions();
this.currentSkillTimer = 5000; // Give back some time so he doesn't wait full CD for smash
}
}
smashAttack() {
console.log('💥 BOSS SMASH!');
this.scene.cameras.main.shake(300, 0.01);
// Visual Warning
const warning = this.scene.add.circle(this.sprite.x, this.sprite.y, 150, 0xff0000, 0.4);
warning.setScale(0);
this.scene.tweens.add({
targets: warning,
scale: 1,
alpha: 0,
duration: 500,
onComplete: () => {
warning.destroy();
// Damage Logic
const playerPos = this.scene.player.getPosition();
const d = Phaser.Math.Distance.Between(this.sprite.x, this.sprite.y, playerPos.x, playerPos.y); // Pixel distance
if (d < 150) {
this.scene.player.takeDamage(30);
// Knockback
// todo
}
// Sound
if (this.scene.soundManager) this.scene.soundManager.playDeath(); // Placeholder for big boom
}
});
}
summonMinions() {
console.log('🧟🧟🧟 BOSS SUMMONS MINIONS!');
for (let i = 0; i < 3; i++) {
const angle = (Math.PI * 2 / 3) * i;
const dist = 3;
const sx = Math.floor(this.gridX + Math.cos(angle) * dist);
const sy = Math.floor(this.gridY + Math.sin(angle) * dist);
if (this.scene.terrainSystem.getTile(sx, sy)) {
// Spawn normal zombie
// We access GameScene's spawn method or create NPC directly
const zombie = new NPC(this.scene, sx, sy, this.scene.terrainOffsetX, this.scene.terrainOffsetY, 'zombie');
zombie.state = 'CHASE';
this.scene.npcs.push(zombie);
// Spawn Effect
if (this.scene.particleEffects) {
const iso = new IsometricUtils(48, 24);
const pos = iso.toScreen(sx, sy);
this.scene.particleEffects.bloodSplash(pos.x + 300, pos.y + 100); // hardcoded offset fix maybe needed
}
}
}
}
destroy() {
if (this.crown) this.crown.destroy();
super.destroy();
// Boss Drops
if (this.scene.lootSystem) {
this.scene.lootSystem.spawnLoot(this.gridX, this.gridY, 'item_gold', 50);
this.scene.lootSystem.spawnLoot(this.gridX, this.gridY, 'item_sword', 1); // Rare drop
}
// Boss Death Event?
this.scene.events.emit('boss_killed');
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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, () => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,129 @@
class MultiplayerSystem {
constructor(scene) {
this.scene = scene;
this.socket = null;
this.otherPlayers = {}; // Map socketId -> Sprite
this.isConnected = false;
// Try to connect
this.connect();
}
connect() {
if (typeof io === 'undefined') {
console.warn('⚠️ Socket.IO not found. Multiplayer disabled.');
console.warn('Please run: npm install socket.io-client OR include CDN.');
return;
}
console.log('🌐 Connecting to Multiplayer Server...');
// Connect to localhost:3000
this.socket = io('http://localhost:3000');
this.socket.on('connect', () => {
console.log('✅ Connected to Server! ID:', this.socket.id);
this.isConnected = true;
// Send initial pos
if (this.scene.player) {
const pos = this.scene.player.getPosition();
this.socket.emit('playerMovement', {
x: pos.x,
y: pos.y,
anim: 'idle',
flipX: false
});
}
});
this.socket.on('currentPlayers', (players) => {
Object.keys(players).forEach((id) => {
if (id === this.socket.id) return;
this.addOtherPlayer(players[id]);
});
});
this.socket.on('newPlayer', (playerInfo) => {
this.addOtherPlayer(playerInfo);
});
this.socket.on('playerDisconnected', (playerId) => {
this.removeOtherPlayer(playerId);
});
this.socket.on('playerMoved', (playerInfo) => {
if (this.otherPlayers[playerInfo.id]) {
const sprite = this.otherPlayers[playerInfo.id];
// Update target pos for interpolation (TODO: logic)
// For now direct teleport
// Convert grid to screen
const iso = new IsometricUtils(48, 24);
const screen = iso.toScreen(playerInfo.x, playerInfo.y);
sprite.setPosition(screen.x + this.scene.terrainOffsetX, screen.y + this.scene.terrainOffsetY);
sprite.setDepth(sprite.y);
// Anim/Flip logic could go here
if (playerInfo.flipX !== undefined) sprite.setFlipX(playerInfo.flipX);
}
});
this.socket.on('worldAction', (action) => {
// Handle world syncing
if (action.type === 'build' && this.scene.buildingSystem) {
// Hacky: place building remotely
// this.scene.terrainSystem.placeStructure(...)
}
});
}
addOtherPlayer(playerInfo) {
if (this.otherPlayers[playerInfo.id]) return;
console.log('👤 New Player Joined:', playerInfo.id);
// Use player sprite
const iso = new IsometricUtils(48, 24);
const screen = iso.toScreen(playerInfo.x, playerInfo.y);
const sprite = this.scene.add.sprite(
screen.x + this.scene.terrainOffsetX,
screen.y + this.scene.terrainOffsetY,
'player' // or player_idle
);
sprite.setOrigin(0.5, 1);
sprite.setScale(0.3); // Same as local player
// Add name tag
const text = this.scene.add.text(0, -50, 'Player', { fontSize: '12px', fill: '#ffffff' });
text.setOrigin(0.5);
// Container? For now just sprite, text handling is complex
this.otherPlayers[playerInfo.id] = sprite;
}
removeOtherPlayer(playerId) {
if (this.otherPlayers[playerId]) {
console.log('👋 Player Left:', playerId);
this.otherPlayers[playerId].destroy();
delete this.otherPlayers[playerId];
}
}
update(delta) {
if (!this.isConnected || !this.socket || !this.scene.player) return;
// Rate limit: send 10 times second? Or every frame?
// Let's send only if moved
const player = this.scene.player;
if (player.isMoving) {
this.socket.emit('playerMovement', {
x: player.gridX,
y: player.gridY,
anim: 'walk',
flipX: player.sprite.flipX // Accessing internal sprite
});
}
}
}

View File

@@ -0,0 +1,105 @@
// ParticleEffects System
// Proceduralno generiranje particle efektov
class ParticleEffects {
constructor(scene) {
this.scene = scene;
this.createParticleTextures();
}
createParticleTextures() {
// Blood particle
if (!this.scene.textures.exists('blood_particle')) {
const graphics = this.scene.make.graphics({ x: 0, y: 0, add: false });
graphics.fillStyle(0xff0000, 1);
graphics.fillCircle(2, 2, 2);
graphics.generateTexture('blood_particle', 4, 4);
graphics.destroy();
}
// Leaf particle
if (!this.scene.textures.exists('leaf_particle')) {
const graphics = this.scene.make.graphics({ x: 0, y: 0, add: false });
graphics.fillStyle(0x44aa44, 1);
graphics.fillRect(0, 0, 4, 6);
graphics.generateTexture('leaf_particle', 4, 6);
graphics.destroy();
}
// Sparkle particle
if (!this.scene.textures.exists('sparkle_particle')) {
const graphics = this.scene.make.graphics({ x: 0, y: 0, add: false });
graphics.fillStyle(0xffff00, 1);
graphics.fillCircle(2, 2, 2);
graphics.generateTexture('sparkle_particle', 4, 4);
graphics.destroy();
}
}
// Blood Splash on damage
bloodSplash(x, y) {
const emitter = this.scene.add.particles(x, y, 'blood_particle', {
speed: { min: 50, max: 150 },
angle: { min: 0, max: 360 },
scale: { start: 1, end: 0 },
lifespan: 500,
quantity: 8,
emitting: false
});
emitter.setDepth(10000);
emitter.explode();
this.scene.time.delayedCall(600, () => emitter.destroy());
}
// Falling Leaves ambient effect
createFallingLeaves() {
if (!this.scene.settings || this.scene.settings.particles === 'NONE') return;
const width = this.scene.scale.width;
const quantity = this.scene.settings.particles === 'LOW' ? 1 : 2;
const leavesEmitter = this.scene.add.particles(0, -20, 'leaf_particle', {
x: { min: 0, max: width },
y: -20,
speedY: { min: 30, max: 80 },
speedX: { min: -20, max: 20 },
quantity: quantity,
frequency: 2000, // Every 2 seconds
lifespan: 8000,
scale: { min: 0.5, max: 1.0 },
rotation: { min: 0, max: 360 },
angle: { min: -10, max: 10 },
alpha: { start: 0.8, end: 0 }
});
leavesEmitter.setDepth(-980); // Below UI, above world
this.leavesEmitter = leavesEmitter;
return leavesEmitter;
}
// Sparkles on item pickup
sparkle(x, y) {
const emitter = this.scene.add.particles(x, y, 'sparkle_particle', {
speed: { min: 20, max: 80 },
angle: { min: 0, max: 360 },
scale: { start: 1.5, end: 0 },
lifespan: 800,
quantity: 5,
emitting: false
});
emitter.setDepth(10000);
emitter.explode();
this.scene.time.delayedCall(900, () => emitter.destroy());
}
// Destroy all effects
destroy() {
if (this.leavesEmitter) {
this.leavesEmitter.destroy();
}
}
}

View File

@@ -0,0 +1,92 @@
class PathfindingSystem {
constructor(scene) {
this.scene = scene;
this.worker = null;
this.callbacks = new Map();
this.requestId = 0;
this.initialized = false;
try {
// Ustvarimo workerja
this.worker = new Worker('src/workers/pathfinding.worker.js');
this.worker.onmessage = this.handleMessage.bind(this);
console.log('✅ PathfindingWorker initialized.');
this.initialized = true;
} catch (err) {
console.error('❌ Failed to init PathfindingWorker:', err);
}
}
updateGrid() {
if (!this.initialized || !this.scene.terrainSystem) return;
const ts = this.scene.terrainSystem;
const width = ts.width;
const height = ts.height;
// Ustvarimo flat array (0 = prehodno, 1 = ovira)
// Uporabimo Uint8Array za učinkovitost prenosa
const grid = new Uint8Array(width * height);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let blocked = 0;
const tile = ts.tiles[y][x];
// 1. Voda in void
if (!tile || tile.type === 'water' || tile.type === 'void') {
blocked = 1;
} else {
// 2. Dekoracije (Ovire)
// Uporabimo že obstoječo logiko v TerrainSystemu (če obstaja) ali preverimo dekoracije
const key = `${x},${y}`;
const decor = ts.decorationsMap.get(key);
if (decor) {
const solidTypes = [
'tree', 'tree_green', 'tree_blue', 'tree_dead',
'tree_green_new', 'tree_blue_new', 'tree_dead_new',
'rock', 'rock_asset', 'rock_new', 'rock_small', 'rock_1', 'rock_2',
'wall', 'fence', 'house', 'gravestone'
];
// Preverimo substring za tipe (npr. 'tree' ujame 'tree_blue')
const isSolid = solidTypes.some(t => decor.type.includes(t));
if (isSolid) blocked = 1;
}
}
grid[y * width + x] = blocked;
}
}
this.worker.postMessage({
type: 'UPDATE_GRID',
payload: { grid, width, height }
});
// console.log('🗺️ Pathfinding Grid updated sent to worker.');
}
findPath(startX, startY, endX, endY, callback) {
if (!this.initialized) return;
const id = this.requestId++;
this.callbacks.set(id, callback);
this.worker.postMessage({
type: 'FIND_PATH',
id: id,
payload: { startX, startY, endX, endY }
});
}
handleMessage(e) {
const { type, id, path } = e.data;
if (type === 'PATH_FOUND') {
const callback = this.callbacks.get(id);
if (callback) {
callback(path);
this.callbacks.delete(id);
}
}
}
}

197
src/systems/QuestSystem.js Normal file
View File

@@ -0,0 +1,197 @@
class QuestSystem {
constructor(scene) {
this.scene = scene;
// Quest Definitions
this.questDB = {
'q1_start': {
id: 'q1_start',
title: 'Survival Basics',
description: 'Collect Wood and Stone to build your first defense.',
objectives: [
{ type: 'collect', item: 'wood', amount: 5, current: 0, done: false },
{ type: 'collect', item: 'stone', amount: 3, current: 0, done: false }
],
reward: { gold: 10, xp: 50 },
nextQuest: 'q2_farm',
giver: 'villager'
},
'q2_farm': {
id: 'q2_farm',
title: 'The Farmer',
description: 'Plant some seeds to grow food. You will need it.',
objectives: [
{ type: 'action', action: 'plant', amount: 3, current: 0, done: false }
],
reward: { gold: 20, item: 'wood', amount: 10 },
nextQuest: 'q3_defense',
giver: 'villager'
},
'q3_defense': {
id: 'q3_defense',
title: 'Fortification',
description: 'Build a Fence to keep zombies out.',
objectives: [
{ type: 'action', action: 'build_fence', amount: 2, current: 0, done: false }
],
reward: { gold: 50, item: 'sword', amount: 1 },
nextQuest: 'q4_slayer',
giver: 'merchant'
},
'q4_slayer': {
id: 'q4_slayer',
title: 'Zombie Slayer',
description: 'Kill 3 Zombies using your new sword.',
objectives: [
{ type: 'kill', target: 'zombie', amount: 3, current: 0, done: false }
],
reward: { gold: 100, item: 'gold', amount: 50 },
nextQuest: null,
giver: 'villager'
}
};
this.activeQuest = null;
this.completedQuests = [];
}
getAvailableQuest(npcType) {
const chain = ['q1_start', 'q2_farm', 'q3_defense', 'q4_slayer'];
let targetId = null;
for (const id of chain) {
if (!this.completedQuests.includes(id)) {
targetId = id;
break;
}
}
if (!targetId) return null;
if (this.activeQuest && this.activeQuest.id === targetId) return null;
const q = this.questDB[targetId];
if (q.giver === npcType) return q;
return null;
}
startQuest(id) {
if (this.completedQuests.includes(id)) return;
const template = this.questDB[id];
if (!template) return;
this.activeQuest = JSON.parse(JSON.stringify(template));
console.log(`📜 Quest Started: ${this.activeQuest.title}`);
this.updateUI();
// Notification
this.scene.events.emit('show-floating-text', {
x: this.scene.player.x,
y: this.scene.player.y - 50,
text: "Quest Accepted!",
color: '#FFFF00'
});
}
update(delta) {
if (!this.activeQuest) return;
let changed = false;
let allDone = true;
if (this.scene.inventorySystem) {
const inv = this.scene.inventorySystem;
for (const obj of this.activeQuest.objectives) {
if (obj.done) continue;
if (obj.type === 'collect') {
const count = inv.getItemCount(obj.item);
if (count !== obj.current) {
obj.current = count;
changed = true;
}
if (obj.current >= obj.amount) {
obj.done = true;
this.scene.events.emit('show-floating-text', { x: this.scene.player.x, y: this.scene.player.y, text: "Objective Complete!" });
}
}
}
}
for (const obj of this.activeQuest.objectives) {
if (!obj.done) allDone = false;
}
if (changed) this.updateUI();
if (allDone) {
this.completeQuest();
}
}
trackAction(actionType, amount = 1) {
if (!this.activeQuest) return;
let changed = false;
for (const obj of this.activeQuest.objectives) {
if (obj.done) continue;
if (obj.type === 'action' && obj.action === actionType) {
obj.current += amount;
changed = true;
if (obj.current >= obj.amount) {
obj.done = true;
changed = true;
}
}
if (obj.type === 'kill' && obj.target === actionType) {
obj.current += amount;
changed = true;
if (obj.current >= obj.amount) {
obj.done = true;
changed = true;
}
}
}
if (changed) this.updateUI();
}
completeQuest() {
console.log(`🏆 Quest Complete: ${this.activeQuest.title}`);
if (this.activeQuest.reward) {
const r = this.activeQuest.reward;
if (r.gold && this.scene.inventorySystem) {
this.scene.inventorySystem.addGold(r.gold);
}
if (r.item && this.scene.inventorySystem) {
this.scene.inventorySystem.addItem(r.item, r.amount || 1);
}
}
this.scene.events.emit('show-floating-text', {
x: this.scene.player.x,
y: this.scene.player.y - 50,
text: "Quest Complete!",
color: '#00FF00'
});
this.completedQuests.push(this.activeQuest.id);
const next = this.activeQuest.nextQuest;
this.activeQuest = null;
this.updateUI();
if (next) {
console.log('Next quest available at NPC.');
}
}
updateUI() {
const ui = this.scene.scene.get('UIScene');
if (ui && ui.updateQuestTracker) {
ui.updateQuestTracker(this.activeQuest);
}
}
}

View File

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

View File

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

View File

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

View File

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

44
src/utils/Compression.js Normal file
View File

@@ -0,0 +1,44 @@
// Utility class for string compression (LZW algorithm)
// Used to reduce save file size in localStorage
class Compression {
static compress(s) {
if (s == null) return "";
var dict = {}, data = (s + "").split(""), out = [], currChar, phrase = data[0], code = 256;
for (var i = 1; i < data.length; i++) {
currChar = data[i];
if (dict[phrase + currChar] != null) {
phrase += currChar;
} else {
out.push(phrase.length > 1 ? dict[phrase] : phrase.charCodeAt(0));
dict[phrase + currChar] = code;
code++;
phrase = currChar;
}
}
out.push(phrase.length > 1 ? dict[phrase] : phrase.charCodeAt(0));
for (var i = 0; i < out.length; i++) {
out[i] = String.fromCharCode(out[i]);
}
return out.join("");
}
static decompress(s) {
if (s == null) return "";
if (s == "") return null;
var dict = {}, data = (s + "").split(""), currChar = data[0], oldPhrase = currChar, out = [currChar], code = 256, phrase;
for (var i = 1; i < data.length; i++) {
var currCode = data[i].charCodeAt(0);
if (currCode < 256) {
phrase = data[i];
} else {
phrase = dict[currCode] ? dict[currCode] : (oldPhrase + oldPhrase.charAt(0));
}
out.push(phrase);
currChar = phrase.charAt(0);
dict[code] = oldPhrase + currChar;
code++;
oldPhrase = phrase;
}
return out.join("");
}
}

99
src/utils/Pathfinding.js Normal file
View File

@@ -0,0 +1,99 @@
class Pathfinding {
constructor(scene) {
this.scene = scene;
// Offsets for 4 directions (Isometric grid)
this.dirs = [
{ x: 0, y: -1 }, // Up
{ x: 0, y: 1 }, // Down
{ x: -1, y: 0 }, // Left
{ x: 1, y: 0 } // Right
];
}
findPath(startX, startY, endX, endY, limit = 50) {
// Simple A* implementation
const startNode = { x: startX, y: startY, g: 0, h: 0, f: 0, parent: null };
const openList = [startNode];
const closedSet = new Set();
let count = 0;
while (openList.length > 0 && count < limit) {
count++;
// Sort by F cost (can be optimized with PriorityQueue)
openList.sort((a, b) => a.f - b.f);
const currentNode = openList.shift();
// Reached destination? (or close enough)
if (currentNode.x === endX && currentNode.y === endY) {
return this.reconstructPath(currentNode);
}
const key = `${currentNode.x},${currentNode.y}`;
closedSet.add(key);
// Explore neighbors
for (const dir of this.dirs) {
const neighborX = currentNode.x + dir.x;
const neighborY = currentNode.y + dir.y;
if (closedSet.has(`${neighborX},${neighborY}`)) continue;
// Check collision
if (!this.isValidMove(neighborX, neighborY)) continue;
const gScore = currentNode.g + 1;
const hScore = Math.abs(neighborX - endX) + Math.abs(neighborY - endY);
const fScore = gScore + hScore;
// Check if already in openList with lower G
const existing = openList.find(n => n.x === neighborX && n.y === neighborY);
if (existing) {
if (gScore < existing.g) {
existing.g = gScore;
existing.f = fScore;
existing.parent = currentNode;
}
} else {
openList.push({ x: neighborX, y: neighborY, g: gScore, h: hScore, f: fScore, parent: currentNode });
}
}
}
// Limit reached or no path - return simplified direction if possible
return null;
}
reconstructPath(node) {
const path = [];
let curr = node;
while (curr.parent) {
path.unshift({ x: curr.x, y: curr.y });
curr = curr.parent;
}
return path;
}
isValidMove(x, y) {
const terrain = this.scene.terrainSystem;
if (!terrain) return true;
// Bounds
if (x < 0 || y < 0 || x >= terrain.width || y >= terrain.height) return false;
// Tile Type
const tile = terrain.tiles[y][x];
if (tile.type.name === 'water') return false;
// Decorations collision
const key = `${x},${y}`;
if (terrain.decorationsMap.has(key)) {
const decor = terrain.decorationsMap.get(key);
const solidTypes = ['tree', 'stone', 'bush', 'wall', 'ruin', 'fence', 'house', 'gravestone'];
if (solidTypes.includes(decor.type)) return false;
}
return true;
}
}

View File

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

View File

@@ -0,0 +1,124 @@
/* eslint-disable no-restricted-globals */
// Preprost A* Pathfinding v Web Workerju
let grid = [];
let width = 0;
let height = 0;
self.onmessage = function (e) {
const { type, payload, id } = e.data;
if (type === 'UPDATE_GRID') {
grid = payload.grid;
width = payload.width;
height = payload.height;
// console.log('🕷️ Worker: Grid updated', width, height);
}
else if (type === 'FIND_PATH') {
if (!grid || grid.length === 0) {
self.postMessage({ type: 'PATH_FOUND', id, path: [] });
return;
}
const { startX, startY, endX, endY } = payload;
const path = findPath(startX, startY, endX, endY);
self.postMessage({ type: 'PATH_FOUND', id, path });
}
};
function findPath(startX, startY, endX, endY) {
// Preverimo meje
if (startX < 0 || startX >= width || startY < 0 || startY >= height) return [];
if (endX < 0 || endX >= width || endY < 0 || endY >= height) return [];
// Če cilj ni prehoden, vrni prazno (ali najdi najbližjo točko - za zdaj prazno)
if (!isWalkable(endX, endY)) return [];
const openSet = [];
const closedSet = new Set();
const cameFrom = new Map();
const gScore = new Map();
const fScore = new Map();
const startKey = `${startX},${startY}`;
const endKey = `${endX},${endY}`;
openSet.push({ x: startX, y: startY, f: heuristic(startX, startY, endX, endY) });
gScore.set(startKey, 0);
fScore.set(startKey, heuristic(startX, startY, endX, endY));
let iterations = 0;
while (openSet.length > 0) {
iterations++;
if (iterations > 1000) return []; // Safety break
// Najdi vozlišče z najnižjim fScore
openSet.sort((a, b) => a.f - b.f);
const current = openSet.shift();
const currentKey = `${current.x},${current.y}`;
if (current.x === endX && current.y === endY) {
return reconstructPath(cameFrom, current);
}
closedSet.add(currentKey);
const neighbors = getNeighbors(current.x, current.y);
for (const neighbor of neighbors) {
const neighborKey = `${neighbor.x},${neighbor.y}`;
if (closedSet.has(neighborKey)) continue;
const tentativeGScore = (gScore.get(currentKey) || 0) + 1;
if (tentativeGScore < (gScore.get(neighborKey) || Infinity)) {
cameFrom.set(neighborKey, current);
gScore.set(neighborKey, tentativeGScore);
const f = tentativeGScore + heuristic(neighbor.x, neighbor.y, endX, endY);
fScore.set(neighborKey, f);
if (!openSet.some(n => n.x === neighbor.x && n.y === neighbor.y)) {
openSet.push({ x: neighbor.x, y: neighbor.y, f: f });
}
}
}
}
return []; // Pot ni najdena
}
function isWalkable(x, y) {
if (x < 0 || x >= width || y < 0 || y >= height) return false;
// Predpostavljamo 1D array: y * width + x. 0 = prehodno, 1 = ovira.
return grid[y * width + x] === 0;
}
function getNeighbors(x, y) {
const neighbors = [];
const dirs = [[0, 1], [0, -1], [1, 0], [-1, 0]]; // Samo 4 smeri (Manhattan)
for (const [dx, dy] of dirs) {
const nx = x + dx;
const ny = y + dy;
if (isWalkable(nx, ny)) {
neighbors.push({ x: nx, y: ny });
}
}
return neighbors;
}
function heuristic(x1, y1, x2, y2) {
return Math.abs(x1 - x2) + Math.abs(y1 - y2);
}
function reconstructPath(cameFrom, current) {
const totalPath = [current];
let currentKey = `${current.x},${current.y}`;
while (cameFrom.has(currentKey)) {
current = cameFrom.get(currentKey);
currentKey = `${current.x},${current.y}`;
totalPath.unshift(current);
}
return totalPath;
}