From 98059a265985ce2f8782bc0fb8cff9cebee61186 Mon Sep 17 00:00:00 2001 From: NovaFarma Dev Date: Sun, 7 Dec 2025 13:16:04 +0100 Subject: [PATCH] popravek zombijo --- TASKS.md | 112 +++++++++ optimizations.md | 97 +++----- src/entities/NPC.js | 54 ++++- src/entities/Player.js | 46 +++- src/game.js | 40 ++++ src/scenes/UIScene.js | 17 +- src/systems/TerrainSystem.js | 422 +++++++++------------------------- src/utils/ErrorHandler.js | 107 +++++++++ src/utils/TextureGenerator.js | 67 ++++++ 9 files changed, 577 insertions(+), 385 deletions(-) create mode 100644 TASKS.md create mode 100644 src/utils/ErrorHandler.js diff --git a/TASKS.md b/TASKS.md new file mode 100644 index 0000000..b0a48de --- /dev/null +++ b/TASKS.md @@ -0,0 +1,112 @@ +# 🗺️ Task Map & Roadmap - NovaFarma + +## 🟢 Phase 1: Core Systems (Foundation) +Vzpostavitev temeljev igre, sveta in igralca. + +- [x] **Project Setup** (Vite, Phaser, Project Structure) +- [x] **Isometric Engine** (IsometricUtils, Grid conversion, Depth sorting) +- [x] **Terrain System** + - [x] Perlin Noise Generation (Grass, Dirt, Water) + - [x] Decoration Placement (Trees, Stones, Bushes) + - [x] Decoration Rendering (Correct depth, scale, variety) +- [x] **Player Entity** + - [x] Isometric Movement (WASD) + - [x] Animations (Idle, Walk) + - [x] Directional Facing (8-way or 4-way) + - [x] Tool Handling (Visual sword/axe/pickaxe in hand) + +## 🟡 Phase 2: Gameplay Mechanics (Current Focus) +Dodajanje interakcije, boja in ekonomije. + +- [x] **Interaction System** + - [x] Mouse Click Interaction (Precise grid selection) + - [x] Keyboard Interaction ('E' Key for ease of use) + - [x] Distance Checking (Range limits) +- [x] **NPC System (Zombies)** + - [x] Spawning & Rendering + - [x] Basic AI (Random Walk, Chase Player) + - [x] **Combat:** + - [x] Player Attack (Spacebar / Click with Weapon) + - [x] Zombie Health & Damage (Take Damage, Red Flash) + - [x] Death & Loot Drops (Skeleton/Bone drop) + - [x] **Taming:** + - [x] Interact to Tame (Empty hand / 'E') + - [x] Visuals (Hearts, Blue Eyes for tamed zombie) + - [x] Command Logic (Follow/Stay basics) +- [x] **Inventory & UI** + - [x] Inventory UI (Slots, Selection) + - [x] Crafting Menu ('C' Key) + - [x] Resource Gathering (Wood from trees, Stone from rocks) +- [x] **Save System** + - [x] LocalStorage Integration + - [x] Auto-load on start + - [x] Manual Save (F5) + +## 🔴 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 + - [ ] 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 + +## 🔵 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) + +## 🟣 Phase 5: Story & Quests (Long Term) +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) + +## 🟠 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 + +--- +*Last Updated: 2025-12-07* diff --git a/optimizations.md b/optimizations.md index 30a24f7..e1b70b9 100644 --- a/optimizations.md +++ b/optimizations.md @@ -1,71 +1,42 @@ -# Plan Optimizacij za NovaFarma +# 🛠️ Plan Optimizacij in Čiščenja - NovaFarma -Cilj: Izboljšati delovanje igre na počasnejših računalnikih (low-end devices) in zagotoviti stabilnih 60 FPS. +Datoteka namenjena tehničnim izboljšavam kode, refaktoringu in performančnim popravkom. -## 1. Rendering (Grafika) +## 🟢 1. Opravljene Optimizacije (Completed) +Stvari, ki so bile uspešno implementirane in izboljšale delovanje. -### A. Prehod na Phaser Tilemap ali Blitter (Visoka prioriteta) -Trenutno `TerrainSystem` uporablja individualne `Phaser.GameObjects.Sprite` za vsako ploščico (tile) na tleh. Pri mapi 100x100 to pomeni 10.000 spritov, kar je veliko breme za CPU/GPU, tudi s cullingom. -* **Rešitev:** Uporabiti `Phaser.Tilemaps` za izometrični pogled ali `Phaser.GameObjects.Blitter`. Blitter je izjemno hiter za renderiranje velikega števila enakih tekstur. -* **Pričakovan prihranek:** Ogromen (CPU overhead za game objekte). -s -### B. Optimizacija Depth Sorting (Srednja prioriteta) -Trenutno se globina (z-index) računa in nastavlja za vsak `Sprite` (igralec, NPC, tiles, decorations) pogosto vsak frame. -* **Rešitev:** - * Statične objekte (drevesa, kamni, tile) sortiraj samo enkrat ob generiranju ali ko pridejo v kader. - * Dinamične objekte (igralec, NPC) sortiraj vsak frame, ampak samo glede na sosednje objekte. - * Uporabi "Display Layers" (Containerje) za ločevanje tal in objektov, da se tla nikoli ne sortirajo proti igralcu (tla so vedno spodaj). +- [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). -### C. Zmanjšanje Draw Calls (Weather System) -Sistem vremena trenutno uporablja `Graphics` objekt, kjer riše vsako kapljo posebej s `strokePath` v JS zanki. -* **Rešitev:** - * Uporaba **Shaderjev (GLSL)** za dež in meglo. To prestavi delo na GPU in je praktično "zastonj" za CPU. - * Alternativa: Uporaba `Phaser.GameObjects.Particles` za dež. +## 🟡 2. Odprte Tehnične Naloge (To-Do) +Stvari, ki bi jih bilo dobro urediti za boljšo stabilnost. -## 2. Logika in Procesiranje +- [ ] **Global Error Handling** + - Ujeti napake, ki se zgodijo med igranjem (npr. manjkajoča metoda), in preprečiti sesutje igre (kot se je zgodilo z `handleDecorationClick`). +- [ ] **Centraliziran Loot Manager** + - Trenutno `InteractionSystem` upravlja z lootom. Bolje bi bilo imeti ločen `LootSystem` ali `ItemDropManager`, ki skrbi za fiziko dropov, pobiranje in despawn. +- [ ] **Z-Sorting (Depth) Optimizacija** + - Globina se še vedno nastavlja pogosto. Lahko bi optimizirali tako, da se statični objekti sortirajo samo enkrat. -### A. Izboljšan Culling (Visoka prioriteta) -Trenutno `updateCulling` teče vsak frame (ali pogosto). Zanka gre čez vse tile (10.000) in preverja distance. -* **Rešitev:** - * **Spatial Hashing / Grid Partitioning:** Razdeli svet na "Chunke" (npr. 10x10 tileov). Renderiraj samo chunke, ki se dotikajo kamere. - * Posodobi culling samo, ko se kamera premakne za določeno razdaljo (npr. 50px), ne vsak frame. +## 🔴 3. Performančne Nadgradnje (High-End) +Če bo igra postala počasna pri velikem svetu (256x256). -### B. Throttling AI in Posodobitev (Srednja prioriteta) -NPC-ji se posodabljajo vsak frame (60-krat na sekundo). -* **Rešitev:** - * Posodabljaj logiko NPC-jev (iskanje poti, odločanje) samo vsakih 10-20 framov ali 100ms. - * Animacije se še vedno vrtijo gladko, ampak "možgani" delajo počasneje. - * Oddaljeni NPC-ji (izven ekrana) se sploh ne posodabljajo ali pa zelo redko. +- [ ] **Phaser Blitter / Tilemap** + - Trenutno je svet sestavljen iz tisočev spritov. Prehod na `Phaser.Blitter` ali `Tilemap` bi drastično zmanjšal porabo RAM-a in CPU-ja. +- [ ] **Spatial Hashing za Kolizijo** + - Namesto preverjanja razdalje do vsakega NPC-ja uporabiti prostorsko mrežo (Spatial Grid) za hitrejše iskanje sosedov. +- [ ] **Web Workers za AI** + - Prestavi pathfinding (iskanje poti) na ločen thread (Worker), da ne blokira glavne igre. -### C. Fizika -Če uporabljamo Arcade Physics za vse kolizije, je lahko počasno. -* **Rešitev:** Za statične objekte (drevesa, zidovi) uporabljaj preprosto preverjanje mreže (`grid[x][y].isSolid`), namesto da vsako drevo ustvari fizikalno telo. - -## 3. Upravljanje Spomina (Memory) - -### A. Object Pooling (Že delno implementirano) -Zagotoviti, da se za VSE pogoste objekte uporablja pool: -* Projektili (če bodo). -* Partikli. -* UI elementi (floating text damage). -* Inventory icons. - -### B. Texture Management -* Zagotoviti, da so vse teksture v **Sprite Atlasu** (Texture Atlas) za zmanjšanje "Texture Swaps" na GPU. -* Unloadati asete, ki niso v uporabi (npr. med prehodi scen, čeprav za to igro verjetno ni kritično). - -## 4. Settings (Nastavitve za uporabnika) - -Dodati meni "Settings", kjer lahko uporabnik izbere: -* **View Distance:** Koliko tile-ov daleč se renderira. -* **Weather Effects:** On/Off/Simple (brez delcev). -* **Shadows:** On/Off. -* **Particles:** High/Low. - -## Akcijski Plan (Koraki) - -1. **Korak 1 (Takoj):** [DONE] Implementiraj "Throttling" za `updateCulling`. -2. **Korak 2:** [DONE] Prepiši `WeatherSystem` za uporabo Phaser Particles. -3. **Korak 3:** [DONE] Implementiraj "Throttling" in Distance Culling za NPC AI. -4. **Korak 4:** [DONE] Implementacija `Phaser.Blitter` za teren (Faza 4 fix). -5. **Korak 5:** [DONE] Povezava Settings z Weather in Terrain sistemi. +--- +*Status: Koda je trenutno stabilna in očiščena (7.12.2025).* diff --git a/src/entities/NPC.js b/src/entities/NPC.js index 69e7c52..ebf6f6d 100644 --- a/src/entities/NPC.js +++ b/src/entities/NPC.js @@ -316,6 +316,7 @@ class NPC { if (this.attackCooldownTimer > 0) return; this.attackCooldownTimer = 1500; + // Attack Animation (Jump) if (this.sprite) { this.scene.tweens.add({ targets: this.sprite, @@ -324,9 +325,12 @@ class NPC { }); } - if (this.scene.statsSystem) { + // Apply Damage to Player + if (this.scene.player && this.scene.player.takeDamage) { + console.log('🧟 Zombie ATTACKS Player!'); + this.scene.player.takeDamage(10); + } else if (this.scene.statsSystem) { this.scene.statsSystem.takeDamage(10); - this.scene.cameras.main.shake(100, 0.005); } } @@ -441,6 +445,52 @@ class NPC { if (idx > -1) this.scene.npcs.splice(idx, 1); } + tame() { + if (this.state === 'TAMED' || this.type !== 'zombie') return; + + this.state = 'TAMED'; + console.log('🧟❤️ Zombie TAMED!'); + + // Visual Feedback + const headX = this.sprite.x; + const headY = this.sprite.y - 40; + + const heart = this.scene.add.text(headX, headY, '❤️', { fontSize: '20px' }); + heart.setOrigin(0.5); + heart.setDepth(this.sprite.depth + 100); + + this.scene.tweens.add({ + targets: heart, + y: headY - 30, + alpha: 0, + duration: 1500, + onComplete: () => heart.destroy() + }); + + // Change color slightly to indicate friend + this.sprite.setTint(0xAAFFAA); + + // Hide Health Bar if tamed (optional) + if (this.healthBarBg) { + this.healthBarBg.setVisible(false); + this.healthBar.setVisible(false); + } + + this.addTamedEyes(); + } + + addTamedEyes() { + if (this.eyesGroup) return; + + this.eyesGroup = this.scene.add.container(this.sprite.x, this.sprite.y); + this.eyesGroup.setDepth(this.sprite.depth + 1); + + const eyeL = this.scene.add.rectangle(-4, -54, 4, 4, 0x00FFFF); // Cyan eyes + const eyeR = this.scene.add.rectangle(4, -54, 4, 4, 0x00FFFF); + + this.eyesGroup.add([eyeL, eyeR]); + } + destroy() { if (this.sprite) this.sprite.destroy(); if (this.healthBar) this.healthBar.destroy(); diff --git a/src/entities/Player.js b/src/entities/Player.js index 1c50205..665b293 100644 --- a/src/entities/Player.js +++ b/src/entities/Player.js @@ -21,6 +21,11 @@ class Player { this.direction = 'down'; this.lastDir = { x: 0, y: 1 }; // Default south + // Stats + this.hp = 100; + this.maxHp = 100; + this.isDead = false; + // Kreira sprite this.createSprite(); @@ -29,13 +34,52 @@ class Player { // Space za napad this.scene.input.keyboard.on('keydown-SPACE', () => { - this.attack(); + if (!this.isDead) this.attack(); }); // Začetna pozicija this.updatePosition(); } + takeDamage(amount) { + if (this.isDead) return; + + this.hp -= amount; + console.log(`Player HP: ${this.hp}`); + + // Visual Feedback + this.scene.cameras.main.shake(100, 0.01); + this.sprite.setTint(0xff0000); + this.scene.time.delayedCall(200, () => { + if (!this.isDead) this.sprite.clearTint(); + }); + + // Update UI (če obstaja StatsSystem ali UI) + if (this.scene.statsSystem) { + // Scene specific stats update if linked + } + + if (this.hp <= 0) { + this.die(); + } + } + + die() { + this.isDead = true; + this.sprite.setTint(0x555555); + this.sprite.setRotation(Math.PI / 2); // Lie down + console.log('💀 PLAYER DIED'); + + // 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 + }).setOrigin(0.5).setDepth(20000); + + this.scene.time.delayedCall(3000, () => { + window.location.reload(); + }); + } + createSprite() { // Prefer animated sprite if available let texKey = 'player_walk'; // Spritesheet diff --git a/src/game.js b/src/game.js index 16d29ce..db5d7ed 100644 --- a/src/game.js +++ b/src/game.js @@ -1,3 +1,43 @@ +// --- Global Error Handler --- +class ErrorHandler { + static init() { + window.onerror = (message, source, lineno, colno, error) => { + ErrorHandler.showError(message, source, lineno, colno, error); + return false; + }; + window.addEventListener('unhandledrejection', (event) => { + ErrorHandler.showError('Unhandled Promise Rejection', '', 0, 0, event.reason); + }); + console.log('🛡️ Global Error Handler Initialized'); + } + + static showError(message, source, lineno, colno, error) { + console.error('🔥 CRITICAL ERROR:', message); + if (document.getElementById('error-overlay')) return; + + const div = document.createElement('div'); + div.id = 'error-overlay'; + div.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(20,0,0,0.95);color:#ffaaaa;z-index:999999;display:flex;flex-direction:column;justify-content:center;align-items:center;font-family:monospace;padding:20px;text-align:center;'; + + const stack = error && error.stack ? error.stack : ''; + + div.innerHTML = ` +

☠️ OOPS! GAME CRASHED ☠️

+
+ ${message}
+ ${source}:${lineno}:${colno}

+ ${stack} +
+
+ + +
+ `; + document.body.appendChild(div); + } +} +ErrorHandler.init(); + // Phaser Game Configuration const config = { type: Phaser.CANVAS, // Canvas renderer za pixel-perfect ostrino diff --git a/src/scenes/UIScene.js b/src/scenes/UIScene.js index f5c4d18..9e34955 100644 --- a/src/scenes/UIScene.js +++ b/src/scenes/UIScene.js @@ -448,8 +448,21 @@ class UIScene extends Phaser.Scene { } update() { - // Here we could update bars based on player stats - // if (this.gameScene && this.gameScene.player) { ... } + if (!this.gameScene) return; + + // Sync HP Bar + if (this.gameScene.player && this.healthBar) { + const hp = this.gameScene.player.hp !== undefined ? this.gameScene.player.hp : 100; + const maxHp = this.gameScene.player.maxHp || 100; + this.setBarValue(this.healthBar, (hp / maxHp) * 100); + } + + // Sync Hunger/Thirst (if stats system exists) + if (this.gameScene.statsSystem && this.hungerBar && this.thirstBar) { + const stats = this.gameScene.statsSystem; + this.setBarValue(this.hungerBar, stats.hunger); + this.setBarValue(this.thirstBar, stats.thirst); + } } toggleBuildMenu(isVisible) { if (!this.buildMenuContainer) { diff --git a/src/systems/TerrainSystem.js b/src/systems/TerrainSystem.js index c52c765..36183ad 100644 --- a/src/systems/TerrainSystem.js +++ b/src/systems/TerrainSystem.js @@ -25,9 +25,6 @@ class TerrainSystem { this.lastCullX = -9999; this.lastCullY = -9999; - // Object Pools - // Tiles will use Blitter, so no Sprite Pool needed for them. - this.blitters = new Map(); // Key: textureKey, Value: Blitter Object // Object Pools this.tilePool = new ObjectPool( () => { @@ -104,6 +101,23 @@ class TerrainSystem { return baseType; } + // Helper za določanje tipa terena glede na noise vrednost + getTerrainType(value) { + if (value < this.terrainTypes.WATER.threshold) return this.terrainTypes.WATER; + if (value < this.terrainTypes.SAND.threshold) return this.terrainTypes.SAND; + if (value < this.terrainTypes.GRASS_FULL.threshold) return this.terrainTypes.GRASS_FULL; // Fallback grass + if (value < this.terrainTypes.GRASS_TOP.threshold) return this.terrainTypes.GRASS_TOP; + if (value < this.terrainTypes.DIRT.threshold) return this.terrainTypes.DIRT; + return this.terrainTypes.STONE; + } + + getTile(x, y) { + if (x >= 0 && x < this.width && y >= 0 && y < this.height) { + return this.tiles[y][x]; + } + return null; + } + // Generiraj teksture za tiles (da ne uporabljamo Počasnih Graphics objektov) createTileTextures() { console.log('🎨 Creating tile textures...'); @@ -178,45 +192,6 @@ class TerrainSystem { graphics.closePath(); graphics.fillPath(); - // 4. Add Minecraft-style texture pattern on top - if (type.name === 'grass_full' || type.name === 'grass_top') { - // Grass texture: Random pixel pattern - graphics.fillStyle(0x4a9d3f, 0.3); // Slightly darker green - for (let i = 0; i < 8; i++) { - const px = Math.random() * tileW; - const py = Math.random() * tileH; - graphics.fillRect(px, py, 2, 2); - } - } else if (type.name === 'dirt') { - // Dirt texture: Darker spots - graphics.fillStyle(0x6d5838, 0.4); - for (let i = 0; i < 6; i++) { - const px = Math.random() * tileW; - const py = Math.random() * tileH; - graphics.fillRect(px, py, 3, 3); - } - } else if (type.name === 'stone') { - // Stone texture: Gray spots - graphics.fillStyle(0x666666, 0.3); - for (let i = 0; i < 10; i++) { - const px = Math.random() * tileW; - const py = Math.random() * tileH; - graphics.fillRect(px, py, 2, 1); - } - } else if (type.name === 'path') { - // Path texture: Small gravel stones - graphics.fillStyle(0x7a5e42, 0.5); // Darker brown - for (let i = 0; i < 12; i++) { - const px = Math.random() * tileW; - const py = Math.random() * tileH; - graphics.fillRect(px, py, 2, 2); - } - } - - // 5. Crisp black outline for block definition - graphics.lineStyle(1, 0x000000, 0.3); - graphics.strokePath(); - // Generate texture graphics.generateTexture(key, tileW, tileH + thickness); graphics.destroy(); @@ -227,27 +202,21 @@ class TerrainSystem { } createGravestoneSprite() { - // Extract gravestone from objects_pack (approx position in atlas) - // Gravestone appears to be around position row 4, column 4-5 in the pack const canvas = document.createElement('canvas'); - const size = 32; // Approximate sprite size + const size = 32; canvas.width = size; canvas.height = size; const ctx = canvas.getContext('2d', { willReadFrequently: true }); - const sourceTexture = this.scene.textures.get('objects_pack'); - const sourceImg = sourceTexture.getSourceImage(); - - // Extract gravestone (cross tombstone) - estimated coords - // Adjust these values based on actual sprite sheet layout - const sourceX = 240; // Approximate X position - const sourceY = 160; // Approximate Y position - - ctx.drawImage(sourceImg, sourceX, sourceY, size, size, 0, 0, size, size); - - // Create texture - this.scene.textures.addCanvas('gravestone', canvas); - console.log('✅ Gravestone sprite extracted!'); + if (this.scene.textures.exists('objects_pack')) { + const sourceTexture = this.scene.textures.get('objects_pack'); + const sourceImg = sourceTexture.getSourceImage(); + const sourceX = 240; + const sourceY = 160; + ctx.drawImage(sourceImg, sourceX, sourceY, size, size, 0, 0, size, size); + this.scene.textures.addCanvas('gravestone', canvas); + console.log('✅ Gravestone sprite extracted!'); + } } // Generiraj teren (data only) @@ -257,90 +226,51 @@ class TerrainSystem { // Zagotovi teksture this.createTileTextures(); - // DELETED Blitter Init - - // Zagotovi decoration teksture - check for custom sprites first - if (!this.scene.textures.exists('flower')) { - TextureGenerator.createFlowerSprite(this.scene, 'flower'); - } - - // Bush - use custom stone sprite if available + if (!this.scene.textures.exists('flower')) TextureGenerator.createFlowerSprite(this.scene, 'flower'); if (this.scene.textures.exists('stone_sprite')) { - // Use stone_sprite for bushes (rocks) - if (!this.scene.textures.exists('bush')) { - this.scene.textures.addImage('bush', this.scene.textures.get('stone_sprite').getSourceImage()); - } + if (!this.scene.textures.exists('bush')) this.scene.textures.addImage('bush', this.scene.textures.get('stone_sprite').getSourceImage()); } else if (!this.scene.textures.exists('bush')) { TextureGenerator.createBushSprite(this.scene, 'bush'); } - // Tree - use custom tree sprite if available if (this.scene.textures.exists('tree_sprite')) { - if (!this.scene.textures.exists('tree')) { - this.scene.textures.addImage('tree', this.scene.textures.get('tree_sprite').getSourceImage()); - } + if (!this.scene.textures.exists('tree')) this.scene.textures.addImage('tree', this.scene.textures.get('tree_sprite').getSourceImage()); } else if (!this.scene.textures.exists('tree')) { TextureGenerator.createTreeSprite(this.scene, 'tree'); } - // Gravestone - extract from objects_pack if (this.scene.textures.exists('objects_pack') && !this.scene.textures.exists('gravestone')) { this.createGravestoneSprite(); } - // Crop textures for (let i = 1; i <= 4; i++) { - if (!this.scene.textures.exists(`crop_stage_${i}`)) - TextureGenerator.createCropSprite(this.scene, `crop_stage_${i}`, i); + if (!this.scene.textures.exists(`crop_stage_${i}`)) TextureGenerator.createCropSprite(this.scene, `crop_stage_${i}`, i); } this.decorationsMap.clear(); this.decorations = []; this.cropsMap.clear(); - // Generiraj tile podatke for (let y = 0; y < this.height; y++) { this.tiles[y] = []; for (let x = 0; x < this.width; x++) { const noiseValue = this.noise.getNormalized(x, y, 0.05, 4); - - // Elevation (druga Perlin noise layer za hribe) let elevation = this.noise.getNormalized(x, y, 0.03, 3); - - // Get terrain type based on BOTH noise and elevation (Y-layer) let terrainType = this.getTerrainTypeByElevation(noiseValue, elevation); - // === PATH GENERATION === - // Use a separate noise layer for paths (higher frequency for winding roads) const pathNoise = this.noise.getNormalized(x, y, 0.08, 2); - - // Create minimal paths - if noise is in a very specific narrow band - // Avoid water (0.3 threshold) and edges if (pathNoise > 0.48 && pathNoise < 0.52 && noiseValue > 0.35) { terrainType = this.terrainTypes.PATH; } - // === FLOATING ISLAND EDGE === - const edgeDistance = 2; // Tiles from edge (tighter border) - const isNearEdge = x < edgeDistance || x >= this.width - edgeDistance || - y < edgeDistance || y >= this.height - edgeDistance; + const edgeDistance = 2; + const isNearEdge = x < edgeDistance || x >= this.width - edgeDistance || y < edgeDistance || y >= this.height - edgeDistance; + const isEdge = x === 0 || x === this.width - 1 || y === 0 || y === this.height - 1; - const isEdge = x === 0 || x === this.width - 1 || - y === 0 || y === this.height - 1; + if (isEdge) terrainType = this.terrainTypes.STONE; - // Override terrain type at edges - if (isEdge) { - terrainType = this.terrainTypes.STONE; // Cliff wall (stone edge) - } else if (isNearEdge) { - // Keep Y-layer system active - } - - // Flatten edges (cliff drop-off for floating island effect) - if (isEdge) { - elevation = 0; // Flat cliff wall - } else if (isNearEdge) { - elevation = Math.max(0, elevation - 0.2); // Slight dip near edge - } + if (isEdge) elevation = 0; + else if (isNearEdge) elevation = Math.max(0, elevation - 0.2); this.tiles[y][x] = { gridX: x, @@ -348,45 +278,32 @@ class TerrainSystem { type: terrainType.name, texture: terrainType.texture, height: noiseValue, - elevation: elevation, // 0-1 (0=low, 1=high) - yLayer: terrainType.yLayer, // Y-stacking layer + elevation: elevation, + yLayer: terrainType.yLayer, hasDecoration: false, hasCrop: false }; - // Generacija dekoracij (shranimo v data, ne ustvarjamo sprite-ov še) if (x > 5 && x < this.width - 5 && y > 5 && y < this.height - 5) { let decorType = null; let maxHp = 1; - let scale = 1.0; // Default scale + let scale = 1.0; - if (terrainType.name.includes('grass')) { // Check ANY grass type + if (terrainType.name.includes('grass')) { const rand = Math.random(); - - // Na hribih več kamnov if (elevation > 0.6 && rand < 0.05) { - decorType = 'bush'; // Kamni + decorType = 'bush'; maxHp = 5; - } else if (rand < 0.025) { // Reduced to 2.5% trees (optimum balance) + } else if (rand < 0.025) { decorType = 'tree'; maxHp = 5; - - // TREE SIZING LOGIC - // 20% Normal (1.0) - // 60% Medium (0.6-0.8) - // 20% Tiny (0.2) const sizeRand = Math.random(); - if (sizeRand < 0.2) { - scale = 0.25; // 20% Tiny (0.25 visible enough) - } else if (sizeRand < 0.8) { - scale = 0.6 + Math.random() * 0.2; // 60% Scale 0.6-0.8 - } else { - scale = 1.0; // 20% Full size - } - + if (sizeRand < 0.2) scale = 0.25; + else if (sizeRand < 0.8) scale = 0.6 + Math.random() * 0.2; + else scale = 1.0; } else if (rand < 0.03) { - decorType = 'gravestone'; // 💀 Nagrobniki - maxHp = 10; // Težje uničiti + decorType = 'gravestone'; + maxHp = 10; } else if (rand < 0.08) { decorType = 'flower'; maxHp = 1; @@ -405,37 +322,29 @@ class TerrainSystem { id: key, maxHp: maxHp, hp: maxHp, - scale: scale // Save scale + scale: scale }; - this.decorations.push(decorData); this.decorationsMap.set(key, decorData); - this.tiles[y][x].hasDecoration = true; } } } } - console.log('✅ Terrain and decorations data generated!'); + console.log('✅ Terrain and decorations generated!'); } - // DAMAGE / INTERACTION LOGIC damageDecoration(x, y, amount) { const key = `${x},${y}`; const decor = this.decorationsMap.get(key); - if (!decor) return false; - decor.hp -= amount; - // Visual feedback (flash red) if (this.visibleDecorations.has(key)) { const sprite = this.visibleDecorations.get(key); sprite.setTint(0xff0000); this.scene.time.delayedCall(100, () => sprite.clearTint()); - - // Shake effect? this.scene.tweens.add({ targets: sprite, x: sprite.x + 2, @@ -449,17 +358,14 @@ class TerrainSystem { this.removeDecoration(x, y); return 'destroyed'; } - return 'hit'; } removeDecoration(x, y) { const key = `${x},${y}`; const decor = this.decorationsMap.get(key); - if (!decor) return; - // Remove visual if (this.visibleDecorations.has(key)) { const sprite = this.visibleDecorations.get(key); sprite.setVisible(false); @@ -467,60 +373,38 @@ class TerrainSystem { this.visibleDecorations.delete(key); } - // Remove data this.decorationsMap.delete(key); - - // Remove from array (slow but needed for save compat for now) const index = this.decorations.indexOf(decor); if (index > -1) this.decorations.splice(index, 1); + if (this.tiles[y] && this.tiles[y][x]) this.tiles[y][x].hasDecoration = false; - // Update tile flag - if (this.tiles[y] && this.tiles[y][x]) { - this.tiles[y][x].hasDecoration = false; - } - - return decor.type; // Return type for dropping loot + return decor.type; } placeStructure(x, y, structureType) { if (this.decorationsMap.has(`${x},${y}`)) return false; - const decorData = { gridX: x, gridY: y, - type: structureType, // 'struct_fence', etc. + type: structureType, id: `${x},${y}`, maxHp: 5, hp: 5 }; - - // Add to data this.decorations.push(decorData); this.decorationsMap.set(decorData.id, decorData); - - // Update tile const tile = this.getTile(x, y); if (tile) tile.hasDecoration = true; - - // Force Visual Update immediately? - // updateCulling will catch it on next frame, but to be safe: - this.lastCullX = -9999; // Force update - + this.lastCullX = -9999; return true; } - // --- Dynamic Tile Modification --- - setTileType(x, y, typeName) { if (!this.tiles[y] || !this.tiles[y][x]) return; - const typeDef = Object.values(this.terrainTypes).find(t => t.name === typeName); if (!typeDef) return; - this.tiles[y][x].type = typeName; this.tiles[y][x].texture = typeDef.texture; - - // Force visual update if visible const key = `${x},${y}`; if (this.visibleTiles.has(key)) { const sprite = this.visibleTiles.get(key); @@ -532,15 +416,12 @@ class TerrainSystem { const key = `${x},${y}`; this.cropsMap.set(key, cropData); this.tiles[y][x].hasCrop = true; - this.tiles[y][x].hasCrop = true; - // updateCulling loop will pick it up on next frame but we force it this.lastCullX = -9999; } removeCrop(x, y) { const key = `${x},${y}`; if (this.cropsMap.has(key)) { - // Remove visual if (this.visibleCrops.has(key)) { const sprite = this.visibleCrops.get(key); sprite.setVisible(false); @@ -560,35 +441,16 @@ class TerrainSystem { } } - // Initialize rendering (called once) init(offsetX, offsetY) { this.offsetX = offsetX; this.offsetY = offsetY; } - // Update culling (called every frame) updateCulling(camera) { - // Throttling Optimization - TEMPORARILY DISABLED FOR DEBUG - // const dist = Phaser.Math.Distance.Between(camera.scrollX, camera.scrollY, this.lastCullX, this.lastCullY); - // if (dist < 50) return; - - // Debug log once - if (!this.hasLogged) { - console.log('UpdateCulling running. Camera:', camera.scrollX, camera.scrollY); - this.hasLogged = true; - } - - this.lastCullX = camera.scrollX; - this.lastCullY = camera.scrollY; - - // ... (rest of setup) - + // Simple Culling const view = camera.worldView; - // Optimization: Adjust buffer based on View Distance setting let buffer = 200; - if (this.scene.settings && this.scene.settings.viewDistance === 'LOW') { - buffer = 50; - } + if (this.scene.settings && this.scene.settings.viewDistance === 'LOW') buffer = 50; const left = view.x - buffer - this.offsetX; const top = view.y - buffer - this.offsetY; const right = view.x + view.width + buffer - this.offsetX; @@ -604,12 +466,6 @@ class TerrainSystem { const minGridY = Math.floor(Math.min(p1.y, p2.y, p3.y, p4.y)); const maxGridY = Math.ceil(Math.max(p1.y, p2.y, p3.y, p4.y)); - // Debug bounds once - if (!this.hasLoggedBounds) { - console.log('Culling Bounds:', minGridX, maxGridX, minGridY, maxGridY); - this.hasLoggedBounds = true; - } - const startX = Math.max(0, minGridX); const endX = Math.min(this.width, maxGridX); const startY = Math.max(0, minGridY); @@ -621,119 +477,68 @@ class TerrainSystem { for (let y = startY; y < endY; y++) { for (let x = startX; x < endX; x++) { - if (x >= 0 && x < this.width && y >= 0 && y < this.height) { - const key = `${x},${y}`; - neededKeys.add(key); + // TILES + const key = `${x},${y}`; + neededKeys.add(key); - // Tile Logic - if (!this.visibleTiles.has(key)) { - const tilePos = this.iso.toScreen(x, y); - const tileData = this.tiles[y][x]; + if (!this.visibleTiles.has(key)) { + const tile = this.tiles[y][x]; + const screenPos = this.iso.toScreen(x, y); + const sprite = this.tilePool.get(); - // Get from Pool - const sprite = this.tilePool.get(); - sprite.setTexture(tileData.texture); + sprite.setPosition(screenPos.x + this.offsetX, screenPos.y + this.offsetY); + sprite.setTexture(tile.texture); + sprite.setDepth(this.iso.getDepth(x, y)); + this.visibleTiles.set(key, sprite); + } - // Elevation effect - const elevationOffset = tileData.elevation * -25; - sprite.setPosition( - tilePos.x + this.offsetX, - tilePos.y + this.offsetY + elevationOffset - ); + // DECORATIONS + const decor = this.decorationsMap.get(key); + if (decor) { + neededDecorKeys.add(key); + if (!this.visibleDecorations.has(key)) { + const sprite = this.decorationPool.get(); + const screenPos = this.iso.toScreen(x, y); - // Senčenje - if (tileData.type.includes('grass')) { - let brightness = 1.0; - if (tileData.elevation > 0.5) { - brightness = 1.0 + (tileData.elevation - 0.5) * 1.0; - } else { - brightness = 0.7 + tileData.elevation * 0.6; - } - sprite.setTint(Phaser.Display.Color.GetColor( - Math.min(255, Math.floor(92 * brightness)), - Math.min(255, Math.floor(184 * brightness)), - Math.min(255, Math.floor(92 * brightness)) - )); + sprite.setPosition(screenPos.x + this.offsetX, screenPos.y + this.offsetY); + // Fix for house sprite + if (decor.type.includes('house_sprite') || decor.type.includes('market_sprite')) { + sprite.setTexture(decor.type); + sprite.setScale(decor.scale || 1.0); } else { - sprite.clearTint(); + sprite.setTexture(decor.type); + sprite.setScale(decor.scale || 1); } - // FIXED DEPTH FOR DEBUG - sprite.setDepth(-1000); - sprite.setVisible(true); + // Origin adjusted for buildings + if (decor.type.includes('house') || decor.type.includes('market')) { + sprite.setOrigin(0.5, 0.8); // Adjusted origin for buildings + } else { + sprite.setOrigin(0.5, 1); + } - this.visibleTiles.set(key, sprite); + sprite.setDepth(this.iso.getDepth(x, y) + 1); // Above tile + this.visibleDecorations.set(key, sprite); } + } - // Crop Logic - if (this.tiles[y][x].hasCrop) { - neededCropKeys.add(key); - if (!this.visibleCrops.has(key)) { - const cropData = this.cropsMap.get(key); - if (cropData) { - const cropPos = this.iso.toScreen(x, y); - const tileData = this.tiles[y][x]; - const elevationOffset = tileData.elevation * -25; - - const sprite = this.cropPool.get(); - sprite.setTexture(`crop_stage_${cropData.stage}`); - sprite.setPosition( - cropPos.x + this.offsetX, - cropPos.y + this.offsetY + this.iso.tileHeight / 2 + elevationOffset - ); - // Crop depth = Y pos - const depth = this.iso.getDepth(x, y); - sprite.setDepth(depth); - - this.visibleCrops.set(key, sprite); - } - } - } - - // Decoration Logic - if (this.tiles[y][x].hasDecoration) { - neededDecorKeys.add(key); - - if (!this.visibleDecorations.has(key)) { - // Fast lookup from map - const decor = this.decorationsMap.get(key); - const tileData = this.tiles[y][x]; - const elevationOffset = tileData.elevation * -25; - - if (decor) { - const decorPos = this.iso.toScreen(x, y); - const sprite = this.decorationPool.get(); - sprite.setTexture(decor.type); - - // Apply same elevation offset as tile - sprite.setPosition( - decorPos.x + this.offsetX, - decorPos.y + this.offsetY + this.iso.tileHeight / 2 + elevationOffset - ); - - const depth = this.iso.getDepth(x, y); - // Depth strategy: Base of object sorting. - // Add small offset based on type if needed, but mainly use Y - sprite.setDepth(depth); - - // Apply scale if present - if (decor.scale) sprite.setScale(decor.scale); - else sprite.setScale(1); - - sprite.flipX = (x + y) % 2 === 0; - - // Sprites are just visual now, interaction handled by InteractionSystem via grid - // sprite.setInteractive(...) removed to fix conflicts - - 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.setTexture(`crop_stage_${crop.stage}`); + sprite.setDepth(this.iso.getDepth(x, y) + 0.5); + this.visibleCrops.set(key, sprite); } } } } - // Cleanup invisible tiles + // Cleanup tiles for (const [key, sprite] of this.visibleTiles) { if (!neededKeys.has(key)) { sprite.setVisible(false); @@ -742,7 +547,7 @@ class TerrainSystem { } } - // Cleanup invisible decorations + // Cleanup decorations for (const [key, sprite] of this.visibleDecorations) { if (!neededDecorKeys.has(key)) { sprite.setVisible(false); @@ -751,7 +556,7 @@ class TerrainSystem { } } - // Cleanup visible crops + // Cleanup crops for (const [key, sprite] of this.visibleCrops) { if (!neededCropKeys.has(key)) { sprite.setVisible(false); @@ -760,21 +565,4 @@ class TerrainSystem { } } } - - // Helper functions - getTerrainType(value) { - for (const type of Object.values(this.terrainTypes)) { - if (value < type.threshold) { - return type; - } - } - return this.terrainTypes.STONE; - } - - getTile(gridX, gridY) { - if (gridX >= 0 && gridX < this.width && gridY >= 0 && gridY < this.height) { - return this.tiles[gridY][gridX]; - } - return null; - } } diff --git a/src/utils/ErrorHandler.js b/src/utils/ErrorHandler.js new file mode 100644 index 0000000..5656cc1 --- /dev/null +++ b/src/utils/ErrorHandler.js @@ -0,0 +1,107 @@ +// Global Error Handler +// Ujame kritične napake in prikaže prijazen "Crash Screen" namesto belega zaslona. + +export class ErrorHandler { + static init() { + window.onerror = (message, source, lineno, colno, error) => { + ErrorHandler.showError(message, source, lineno, colno, error); + // return true; // Če vrnemo true, zavremo izpis v konzolo (ne želimo tega) + return false; + }; + + window.addEventListener('unhandledrejection', (event) => { + ErrorHandler.showError('Unhandled Promise Rejection', '', 0, 0, event.reason); + }); + + console.log('🛡️ Global Error Handler Initialized'); + } + + static showError(message, source, lineno, colno, error) { + console.error('🔥 CRITICAL ERROR HAUGHT:', message); + + // Prepreči podvajanje overlayev + if (document.getElementById('error-overlay')) return; + + // Ustvari overlay element + const overlay = document.createElement('div'); + overlay.id = 'error-overlay'; + overlay.style.cssText = ` + position: fixed; + top: 0; left: 0; width: 100%; height: 100%; + background-color: rgba(20, 0, 0, 0.95); + color: #ffaaaa; + z-index: 999999; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-family: 'Consolas', 'Courier New', monospace; + text-align: center; + padding: 20px; + box-sizing: border-box; + `; + + const title = document.createElement('h1'); + title.innerText = '💀 OOPS! GAME CRASHED 💀'; + title.style.color = '#ff4444'; + title.style.marginBottom = '20px'; + + const msgBox = document.createElement('div'); + msgBox.innerText = `${message}\n\nFile: ${source}\nLine: ${lineno}:${colno}\n\n${error ? error.stack : ''}`; + msgBox.style.cssText = ` + background: rgba(0,0,0,0.5); + padding: 15px; + border: 1px solid #ff4444; + max-width: 800px; + max-height: 300px; + overflow: auto; + white-space: pre-wrap; + text-align: left; + font-size: 12px; + margin-bottom: 30px; + color: #fff; + `; + + const btnContainer = document.createElement('div'); + + const btnReload = document.createElement('button'); + btnReload.innerText = '🔄 RELOAD GAME'; + btnReload.style.cssText = ` + padding: 15px 30px; + font-size: 18px; + font-weight: bold; + background: #44aa44; + color: white; + border: none; + cursor: pointer; + border-radius: 5px; + margin-right: 15px; + `; + btnReload.onclick = () => window.location.reload(); + + const btnIgnore = document.createElement('button'); + btnIgnore.innerText = 'IGNORE & TRY TO CONTINUE'; + btnIgnore.style.cssText = ` + padding: 15px 30px; + font-size: 14px; + background: #666; + color: white; + border: none; + cursor: pointer; + border-radius: 5px; + `; + btnIgnore.onclick = () => { + overlay.remove(); + console.log('⚠️ User ignored critical error.'); + }; + + btnContainer.appendChild(btnReload); + btnContainer.appendChild(btnIgnore); + + overlay.appendChild(title); + overlay.appendChild(msgBox); + overlay.appendChild(btnContainer); + + document.body.appendChild(overlay); + } +} diff --git a/src/utils/TextureGenerator.js b/src/utils/TextureGenerator.js index 29df766..a65a26c 100644 --- a/src/utils/TextureGenerator.js +++ b/src/utils/TextureGenerator.js @@ -571,6 +571,49 @@ class TextureGenerator { ctx.lineTo(32, 40); ctx.fill(); + } else if (type === 'market') { + // Market Stall + // Wooden base + ctx.fillStyle = '#8B4513'; + ctx.fillRect(4, 30, 56, 34); // Big base + + // Counter + ctx.fillStyle = '#DEB887'; // Burlywood + ctx.fillRect(0, 40, 64, 10); + + // Posts + ctx.fillStyle = '#5C4033'; + ctx.fillRect(4, 10, 4, 30); + ctx.fillRect(56, 10, 4, 30); + + // Striped Roof + ctx.fillStyle = '#FF0000'; // Red + ctx.beginPath(); + ctx.moveTo(32, 0); + ctx.lineTo(64, 15); + ctx.lineTo(0, 15); + ctx.fill(); + + // White stripes + ctx.fillStyle = '#FFFFFF'; + ctx.beginPath(); + ctx.moveTo(32, 0); + ctx.lineTo(40, 15); + ctx.lineTo(32, 15); + ctx.fill(); + + ctx.beginPath(); + ctx.moveTo(32, 0); + ctx.lineTo(24, 15); + ctx.lineTo(16, 15); + ctx.fill(); + + // Goods (Apples/Items) + ctx.fillStyle = '#00FF00'; + ctx.beginPath(); ctx.arc(10, 38, 4, 0, Math.PI * 2); ctx.fill(); + ctx.fillStyle = '#FF0000'; + ctx.beginPath(); ctx.arc(20, 38, 4, 0, Math.PI * 2); ctx.fill(); + } else if (type === 'ruin') { // Isometric Ruin @@ -922,5 +965,29 @@ class TextureGenerator { canvas.refresh(); } + + // 7. BONE + if (!scene.textures.exists('item_bone')) { + const size = 32; + const canvas = scene.textures.createCanvas('item_bone', size, size); + const ctx = canvas.getContext(); + ctx.clearRect(0, 0, size, size); + + // Bone shape + ctx.strokeStyle = '#E8E8E8'; // Off-white + ctx.lineWidth = 6; + ctx.lineCap = 'round'; + ctx.beginPath(); + ctx.moveTo(10, 10); + ctx.lineTo(22, 22); + ctx.stroke(); + + // Knobs + ctx.fillStyle = '#E8E8E8'; + ctx.beginPath(); ctx.arc(10, 10, 4, 0, Math.PI * 2); ctx.fill(); + ctx.beginPath(); ctx.arc(22, 22, 4, 0, Math.PI * 2); ctx.fill(); + + canvas.refresh(); + } } }