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