kockasta mapa

This commit is contained in:
2025-12-07 14:28:39 +01:00
parent 98059a2659
commit 045bf24792
11 changed files with 670 additions and 1249 deletions

View File

@@ -67,6 +67,7 @@
<script src="src/utils/IsometricUtils.js"></script>
<script src="src/utils/TextureGenerator.js"></script>
<script src="src/utils/ObjectPool.js"></script>
<script src="src/utils/SpatialGrid.js"></script>
<!-- Systems -->
<script src="src/systems/TerrainSystem.js"></script>
@@ -74,6 +75,7 @@
<!-- TimeSystem merged into WeatherSystem -->
<script src="src/systems/StatsSystem.js"></script>
<script src="src/systems/InventorySystem.js"></script>
<script src="src/systems/LootSystem.js"></script>
<script src="src/systems/InteractionSystem.js"></script>
<script src="src/systems/FarmingSystem.js"></script>
<script src="src/systems/BuildingSystem.js"></script>

View File

@@ -21,22 +21,54 @@ Stvari, ki so bile uspešno implementirane in izboljšale delovanje.
## 🟡 2. Odprte Tehnične Naloge (To-Do)
Stvari, ki bi jih bilo dobro urediti za boljšo stabilnost.
- [ ] **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.
- [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).
# 🛠️ 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).
- [ ] **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.
- [x] Phaser Blitter / Tilemap (Zamenjava 1000 spritov za teren z enim objektom)
- [x] **Spatial Hashing za Kolizijo**
- Implementiran `SpatialGrid.js`. Igralna scena zdaj uporablja mrežo za hitro iskanje NPC-jev v bližini (`InteractionSystem`, `NPC AI`), namesto da bi iterirala čez celo tabelo.
- [x] Phaser Blitter / Tilemap (Zamenjava 1000 spritov za teren z enim objektom)
- [ ] Web Workers za AI (Težje, ker JS nima shared memory, samo message passing)ding (iskanje poti) na ločen thread (Worker), da ne blokira glavne igre.
---
*Status: Koda je trenutno stabilna in očiščena (7.12.2025).*

View File

@@ -34,6 +34,11 @@ class NPC {
// Naključna začetna pavza
this.pauseTime = Math.random() * this.maxPauseTime;
// Register in SpatialGrid
if (this.scene.spatialGrid) {
this.scene.spatialGrid.add(this);
}
}
tame() {
@@ -221,6 +226,7 @@ class NPC {
if (this.isMoving) {
this.updateDepth();
this.updatePosition();
if (this.scene.spatialGrid) this.scene.spatialGrid.updateEntity(this);
return;
}
@@ -378,23 +384,31 @@ class NPC {
this.healthBarBg.y = this.sprite.y;
this.healthBar.x = this.sprite.x;
this.healthBar.y = this.sprite.y;
this.healthBarBg.setDepth(this.sprite.depth + 100);
this.healthBar.setDepth(this.sprite.depth + 101);
}
// EYES
if (this.eyesGroup) {
this.eyesGroup.setPosition(this.sprite.x, this.sprite.y);
this.eyesGroup.setDepth(this.sprite.depth + 2);
}
this.updateDepth();
}
updateDepth() {
if (this.sprite) {
if (!this.sprite) return;
if (this.lastDepthY === undefined || Math.abs(this.sprite.y - this.lastDepthY) > 0.1) {
this.sprite.setDepth(this.sprite.y);
this.lastDepthY = this.sprite.y;
// Update attached elements depth
if (this.healthBarBg) {
this.healthBarBg.setDepth(this.sprite.depth + 100);
this.healthBar.setDepth(this.sprite.depth + 101);
}
if (this.eyesGroup) {
this.eyesGroup.setDepth(this.sprite.depth + 2);
}
}
}
@@ -436,7 +450,10 @@ class NPC {
die() {
console.log('🧟💀 Zombie DEAD');
// Spawn loot - BONE
if (this.scene.interactionSystem && this.scene.interactionSystem.spawnLoot) {
if (this.scene.lootSystem) {
this.scene.lootSystem.spawnLoot(this.gridX, this.gridY, 'item_bone');
} else if (this.scene.interactionSystem && this.scene.interactionSystem.spawnLoot) {
// Fallback
this.scene.interactionSystem.spawnLoot(this.gridX, this.gridY, 'item_bone');
}
this.destroy();
@@ -492,6 +509,9 @@ class NPC {
}
destroy() {
if (this.scene.spatialGrid) {
this.scene.spatialGrid.remove(this);
}
if (this.sprite) this.sprite.destroy();
if (this.healthBar) this.healthBar.destroy();
if (this.healthBarBg) this.healthBarBg.destroy();

View File

@@ -319,9 +319,13 @@ class Player {
}
updateDepth() {
if (this.sprite) {
if (!this.sprite) return;
// Optimization: Create dirty check
if (this.lastDepthY === undefined || Math.abs(this.sprite.y - this.lastDepthY) > 0.1) {
this.sprite.setDepth(this.sprite.y);
if (this.handSprite) this.handSprite.setDepth(this.sprite.y + 1);
this.lastDepthY = this.sprite.y;
}
}

View File

@@ -17,6 +17,9 @@ class GameScene extends Phaser.Scene {
create() {
console.log('🎮 GameScene: Initialized!');
// Generate procedural textures
new TextureGenerator(this).generateAll();
window.gameState.currentScene = 'GameScene';
const width = this.cameras.main.width;
@@ -28,6 +31,9 @@ class GameScene extends Phaser.Scene {
// Initialize Isometric Utils
this.iso = new IsometricUtils();
// Initialize Spatial Grid
this.spatialGrid = new SpatialGrid(10);
// Inicializiraj terrain sistem - 100x100 mapa
console.log('🌍 Initializing terrain...');
try {
@@ -101,6 +107,7 @@ class GameScene extends Phaser.Scene {
this.statsSystem = new StatsSystem(this);
this.inventorySystem = new InventorySystem(this);
this.lootSystem = new LootSystem(this);
this.interactionSystem = new InteractionSystem(this);
this.farmingSystem = new FarmingSystem(this);
this.buildingSystem = new BuildingSystem(this);
@@ -205,6 +212,7 @@ class GameScene extends Phaser.Scene {
// 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.interactionSystem) this.interactionSystem.update(delta);
if (this.farmingSystem) this.farmingSystem.update(delta);
// DayNight update removed (handled by WeatherSystem)

View File

@@ -19,7 +19,7 @@ class UIScene extends Phaser.Scene {
this.createInventoryBar();
this.createGoldDisplay();
this.createClock();
this.createDebugInfo();
// this.createDebugInfo();
this.createSettingsButton();
// Resize event
@@ -430,21 +430,25 @@ class UIScene extends Phaser.Scene {
createDebugInfo() {
if (this.debugText) this.debugText.destroy();
if (this.debugBg) this.debugBg.destroy();
// Use scale height to position at bottom left (above inventory?) or top left
// Original was 10, 100 (top leftish). User said "manjkajo na dnu".
// Let's put it top left but ensure it is recreated.
// Actually, user said stats missing on top/bottom.
// Debug info is usually extra.
// Let's stick to simple recreation.
const x = this.scale.width - 170;
const y = 120; // Below Gold and Clock area
this.debugText = this.add.text(10, 100, '', {
// Background
this.debugBg = this.add.graphics();
this.debugBg.fillStyle(0x000000, 0.7);
this.debugBg.fillRect(x, y, 160, 70);
this.debugBg.setDepth(2999);
this.debugText = this.add.text(x + 10, y + 10, 'Waiting for stats...', {
fontSize: '12px',
fontFamily: 'monospace',
fill: '#00ff00', // Green as requested before? Or White? Sticking to Green from file.
fill: '#ffffff',
stroke: '#000000',
strokeThickness: 3
strokeThickness: 2
});
this.debugText.setDepth(3000);
}
update() {
@@ -457,7 +461,6 @@ class UIScene extends Phaser.Scene {
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);

View File

@@ -17,9 +17,6 @@ class InteractionSystem {
this.scene.input.keyboard.on('keydown-E', () => {
this.handleInteractKey();
});
// Loot Array
this.drops = [];
}
handleInteractKey() {
@@ -30,7 +27,9 @@ class InteractionSystem {
let nearest = null;
let minDist = 2.5; // Interaction range
for (const npc of this.scene.npcs) {
const candidates = this.scene.spatialGrid ? this.scene.spatialGrid.query(playerPos.x, playerPos.y) : this.scene.npcs;
for (const npc of candidates) {
const d = Phaser.Math.Distance.Between(playerPos.x, playerPos.y, npc.gridX, npc.gridY);
if (d < minDist) {
minDist = d;
@@ -84,8 +83,10 @@ class InteractionSystem {
}
// 3.5 Check for NPC Interaction
if (this.scene.npcs) {
for (const npc of this.scene.npcs) {
const candidates = this.scene.spatialGrid ? this.scene.spatialGrid.query(gridX, gridY) : this.scene.npcs;
if (candidates) {
for (const npc of candidates) {
// Increased radius to 1.8 to catch moving NPCs easier
if (Math.abs(npc.gridX - gridX) < 1.8 && Math.abs(npc.gridY - gridY) < 1.8) {
@@ -139,11 +140,9 @@ class InteractionSystem {
if (decor.type === 'tree') {
damage = (activeTool === 'axe') ? 3 : 1;
if (!isAttack && activeTool !== 'axe') return;
}
else if (decor.type === 'stone') {
damage = (activeTool === 'pickaxe') ? 3 : 1;
if (!isAttack && activeTool !== 'pickaxe') return;
}
else if (decor.type === 'bush') damage = 2;
@@ -153,17 +152,18 @@ class InteractionSystem {
if (decor.hp <= 0) {
const type = this.scene.terrainSystem.removeDecoration(gridX, gridY);
// Loot logic
// Loot logic via LootSystem
let loot = 'wood';
if (type === 'stone') loot = 'stone';
if (type === 'bush') loot = 'seeds'; // Maybe berries?
if (type === 'tree') {
this.spawnLoot(gridX, gridY, 'wood');
this.spawnLoot(gridX, gridY, 'wood');
this.spawnLoot(gridX, gridY, 'wood');
}
else {
this.spawnLoot(gridX, gridY, loot);
if (this.scene.lootSystem) {
if (type === 'tree') {
this.scene.lootSystem.spawnLoot(gridX, gridY, 'wood', 3);
}
else {
this.scene.lootSystem.spawnLoot(gridX, gridY, loot, 1);
}
}
} else {
@@ -192,42 +192,7 @@ class InteractionSystem {
this.scene.tweens.add({ targets: txt, y: txt.y - 40, alpha: 0, duration: 1000, onComplete: () => txt.destroy() });
}
spawnLoot(gridX, gridY, type) {
console.log(`🎁 Spawning ${type} at ${gridX},${gridY}`);
const screenPos = this.iso.toScreen(gridX, gridY);
const x = screenPos.x + this.scene.terrainOffsetX;
const y = screenPos.y + this.scene.terrainOffsetY;
let symbol = '?';
if (type === 'wood') symbol = '🪵';
if (type === 'stone') symbol = '🪨';
if (type === 'seeds') symbol = '🌱';
if (type === 'wheat') symbol = '🌾';
if (type === 'axe') symbol = '🪓';
if (type === 'item_bone') symbol = '🦴';
const drop = this.scene.add.text(x, y - 20, symbol, { fontSize: '20px' });
drop.setOrigin(0.5);
drop.setDepth(this.iso.getDepth(gridX, gridY) + 500);
this.scene.tweens.add({
targets: drop, y: y - 40, duration: 500, yoyo: true, ease: 'Sine.easeOut', repeat: -1
});
this.drops.push({ gridX, gridY, sprite: drop, type: type });
}
update() {
if (!this.scene.player) return;
const playerPos = this.scene.player.getPosition();
for (let i = this.drops.length - 1; i >= 0; i--) {
const drop = this.drops[i];
if (Math.abs(drop.gridX - playerPos.x) < 0.8 && Math.abs(drop.gridY - playerPos.y) < 0.8) {
if (this.scene.inventorySystem) this.scene.inventorySystem.addItem(drop.type, 1);
drop.sprite.destroy();
this.drops.splice(i, 1);
}
}
update(delta) {
// No logic needed here anymore (loot pickup handled by LootSystem)
}
}

118
src/systems/LootSystem.js Normal file
View File

@@ -0,0 +1,118 @@
class LootSystem {
constructor(scene) {
this.scene = scene;
this.drops = []; // Active loot drops
this.iso = scene.terrainSystem ? scene.terrainSystem.iso : null; // Reference to IsoUtils
// Settings
this.pickupRadius = 1.0; // Grid based
this.magnetRadius = 3.0; // Optional: fly towards player
}
spawnLoot(gridX, gridY, type, count = 1) {
if (!this.iso && this.scene.terrainSystem) this.iso = this.scene.terrainSystem.iso;
if (!this.iso) return; // Safety check
console.log(`🎁 Spawning ${count}x ${type} at ${gridX},${gridY}`);
const screenPos = this.iso.toScreen(gridX, gridY);
const x = screenPos.x + (this.scene.terrainOffsetX || 0);
const y = screenPos.y + (this.scene.terrainOffsetY || 0);
// Visual Symbol Mapping
let symbol = '?';
const symbols = {
'wood': '🪵', 'stone': '🪨', 'seeds': '🌱', 'wheat': '🌾',
'axe': '🪓', 'pickaxe': '⛏️', 'sword': '⚔️', 'hoe': '🚜',
'item_bone': '🦴', 'flower': '🌸'
};
if (symbols[type]) symbol = symbols[type];
// Create Sprite/Text
// Using Text for now as it supports emojis easily, but could be Sprite
const drop = this.scene.add.text(x, y - 20, symbol, { fontSize: '20px' });
drop.setOrigin(0.5);
drop.setDepth(this.iso.getDepth(gridX, gridY) + 500);
// Animation (Bobbing)
this.scene.tweens.add({
targets: drop,
y: y - 40,
duration: 600,
yoyo: true,
ease: 'Sine.easeInOut',
repeat: -1
});
// Add to list
this.drops.push({
gridX, gridY,
x, y,
sprite: drop,
type: type,
count: count,
spawnTime: Date.now()
});
}
update() {
if (!this.scene.player) return;
const playerPos = this.scene.player.getPosition();
// Loop backwards to allow removal
for (let i = this.drops.length - 1; i >= 0; i--) {
const drop = this.drops[i];
// Distance Check
const dist = Math.abs(drop.gridX - playerPos.x) + Math.abs(drop.gridY - playerPos.y); // Manhattan-ish
// Pickup Logic
if (dist < 0.8) {
this.collectLoot(drop, i);
}
// Magnet Logic (Visual fly to player) - Optional
else if (dist < 3.0) {
// Move visual slightly towards player?
// Implementing full physics/velocity might be overkill for now.
}
}
}
collectLoot(drop, index) {
if (this.scene.inventorySystem) {
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
// Float text effect
this.showFloatingText(`+${drop.count} ${drop.type}`, drop.x, drop.y);
// Remove
drop.sprite.destroy();
this.drops.splice(index, 1);
} else {
// Config full? Update count?
drop.count = leftover;
}
}
}
showFloatingText(text, x, y) {
const txt = this.scene.add.text(x, y - 50, text, {
fontSize: '14px', fill: '#ffff00', stroke: '#000', strokeThickness: 2
}).setOrigin(0.5).setDepth(20000);
this.scene.tweens.add({
targets: txt,
y: y - 100,
alpha: 0,
duration: 800,
onComplete: () => txt.destroy()
});
}
}

View File

@@ -10,276 +10,150 @@ class TerrainSystem {
this.noise = new PerlinNoise(Date.now());
this.tiles = [];
this.decorations = []; // Array za save/load compat
this.decorationsMap = new Map(); // Fast lookup key->decor
this.cropsMap = new Map(); // Store dynamic crops separately
// Render state monitoring
this.visibleTiles = new Map(); // Key: "x,y", Value: Sprite
this.visibleDecorations = new Map(); // Key: "x,y", Value: Sprite
this.visibleCrops = new Map(); // Key: "x,y", Value: Sprite
this.decorations = [];
this.decorationsMap = new Map();
this.cropsMap = new Map();
this.visibleTiles = new Map();
this.visibleDecorations = new Map();
this.visibleCrops = new Map();
// Pools
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: [],
get: () => {
if (this.decorationPool.inactive.length > 0) {
const s = this.decorationPool.inactive.pop();
s.setVisible(true);
s.clearTint();
return s;
}
return this.scene.add.sprite(0, 0, 'tree');
},
release: (sprite) => {
sprite.setVisible(false);
this.decorationPool.inactive.push(sprite);
}
};
this.cropPool = {
active: [],
inactive: [],
get: () => {
if (this.cropPool.inactive.length > 0) {
const s = this.cropPool.inactive.pop();
s.setVisible(true);
return s;
}
return this.scene.add.sprite(0, 0, 'crop_stage_1');
},
release: (sprite) => {
sprite.setVisible(false);
this.cropPool.inactive.push(sprite);
}
};
this.terrainTypes = {
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 },
PATH: { name: 'path', height: -1, color: 0xc2b280 },
FARMLAND: { name: 'farmland', height: -1, color: 0x5c4033 }
};
this.offsetX = 0;
this.offsetY = 0;
// Culling optimization
this.lastCullX = -9999;
this.lastCullY = -9999;
// Object Pools
this.tilePool = new ObjectPool(
() => {
const sprite = this.scene.add.image(0, 0, 'tile_grass');
sprite.setOrigin(0.5, 0); // Isometrični tiles imajo origin zgoraj/center ali po potrebi
return sprite;
},
(sprite) => {
sprite.setVisible(true);
sprite.setAlpha(1);
sprite.clearTint();
}
);
this.decorationPool = new ObjectPool(
() => {
const sprite = this.scene.add.sprite(0, 0, 'flower');
sprite.setOrigin(0.5, 1);
return sprite;
},
(sprite) => {
sprite.setVisible(true);
sprite.setAlpha(1);
sprite.clearTint(); // Reset damage tint
}
);
this.cropPool = new ObjectPool(
() => {
const sprite = this.scene.add.sprite(0, 0, 'crop_stage_1'); // Default texture logic needed
sprite.setOrigin(0.5, 1);
return sprite;
},
(sprite) => {
sprite.setVisible(true);
sprite.setAlpha(1);
}
);
// Tipi terena z threshold vrednostmi + Y-LAYER STACKING
this.terrainTypes = {
WATER: { threshold: 0.3, color: 0x2166aa, name: 'water', texture: 'tile_water', yLayer: -1 },
SAND: { threshold: 0.4, color: 0xf4e7c6, name: 'sand', texture: 'tile_sand', yLayer: 0 },
// Y-LAYER GRASS VARIANTS (A, B, C systém)
GRASS_FULL: { threshold: 0.50, color: 0x5cb85c, name: 'grass_full', texture: 'tile_grass_full', yLayer: 0 }, // A: Full grass
GRASS_TOP: { threshold: 0.60, color: 0x5cb85c, name: 'grass_top', texture: 'tile_grass_top', yLayer: 1 }, // B: Grass top, dirt sides
DIRT: { threshold: 0.75, color: 0x8b6f47, name: 'dirt', texture: 'tile_dirt', yLayer: 2 }, // C: Full dirt
STONE: { threshold: 1.0, color: 0x7d7d7d, name: 'stone', texture: 'tile_stone', yLayer: 3 },
PATH: { threshold: 999, color: 0x9b7653, name: 'path', texture: 'tile_path', yLayer: 0 }, // Pot/Road
FARMLAND: { threshold: 999, color: 0x4a3c2a, name: 'farmland', texture: 'tile_farmland', yLayer: 0 }
};
}
// Helper da dobi terrain type glede na elevation (Y-layer)
getTerrainTypeByElevation(noiseValue, elevation) {
// Osnovni terrain type iz noise
let baseType = this.getTerrainType(noiseValue);
// Če je grass, določi Y-layer variant glede na elevation
if (baseType.name.includes('grass') || baseType === this.terrainTypes.GRASS_FULL ||
baseType === this.terrainTypes.GRASS_TOP) {
if (elevation > 0.7) {
return this.terrainTypes.GRASS_FULL; // A: Najvišja plast (full grass)
} else if (elevation > 0.4) {
return this.terrainTypes.GRASS_TOP; // B: Srednja (grass top, dirt sides)
} else {
return this.terrainTypes.DIRT; // C: Nizka (full dirt)
}
}
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...');
// Flat Grid Look (No depth)
const tileWidth = 48;
const tileHeight = 24; // Just the diamond
const types = Object.values(this.terrainTypes);
for (const type of Object.values(this.terrainTypes)) {
const key = `tile_${type.name}`;
if (this.scene.textures.exists(key)) continue;
// Check for custom grass tile
if (type.name === 'grass' && this.scene.textures.exists('grass_tile')) {
this.scene.textures.addImage(key, this.scene.textures.get('grass_tile').getSourceImage());
type.texture = key;
continue; // Skip procedural generation
}
const tileW = this.iso.tileWidth;
const tileH = this.iso.tileHeight;
const thickness = 8; // Minimal thickness - nearly 2D
types.forEach((type) => {
if (this.scene.textures.exists(type.name)) return;
const graphics = this.scene.make.graphics({ x: 0, y: 0, add: false });
// Helper for colors
const baseColor = Phaser.Display.Color.IntegerToColor(type.color);
let darkColor, darkerColor;
const x = 0;
const top = 0;
const midX = 24;
const midY = 12;
const bottomY = 24;
// Y-LAYER STACKING: Different side colors based on layer
if (type.name === 'grass_full') {
// A: Full Grass Block - všechno zeleno
darkColor = 0x5cb85c; // Green (lighter)
darkerColor = 0x4a9d3f; // Green (darker)
} else if (type.name === 'grass_top') {
// B: Grass Top - zgoraj zeleno, stranice zemlja
darkColor = 0x8b6f47; // Dirt color (brown)
darkerColor = 0x5d4a2e; // Darker Dirt
} else if (type.name === 'dirt') {
// C: Full Dirt - vse rjavo
darkColor = 0x8b6f47; // Dirt
darkerColor = 0x654321; // Darker Dirt
} else {
// Standard block: Darken base color significantly
darkColor = Phaser.Display.Color.IntegerToColor(type.color).darken(30).color;
darkerColor = Phaser.Display.Color.IntegerToColor(type.color).darken(50).color;
graphics.fillStyle(type.color);
// Diamond Only
graphics.beginPath();
graphics.moveTo(midX, top);
graphics.lineTo(x + 48, midY);
graphics.lineTo(midX, bottomY);
graphics.lineTo(x, midY);
graphics.closePath();
graphics.fill();
// Grid Stroke (Black/Dark)
graphics.lineStyle(1, 0x000000, 0.3);
graphics.strokePath();
// Simple details
if (type.name.includes('grass')) {
graphics.fillStyle(0x339933);
for (let i = 0; i < 5; i++) {
const rx = x + 10 + Math.random() * 28;
const ry = 5 + Math.random() * 14;
graphics.fillRect(rx, ry, 2, 2);
}
}
// 1. Draw LEFT Side (Darker) - Minecraft volumetric effect
graphics.fillStyle(darkColor, 1);
graphics.beginPath();
graphics.moveTo(0, tileH / 2); // Left Corner
graphics.lineTo(tileW / 2, tileH); // Bottom Corner
graphics.lineTo(tileW / 2, tileH + thickness); // Bottom Corner (Lowered)
graphics.lineTo(0, tileH / 2 + thickness); // Left Corner (Lowered)
graphics.closePath();
graphics.fillPath();
// 2. Draw RIGHT Side (Darkest) - Strong shadow
graphics.fillStyle(darkerColor, 1);
graphics.beginPath();
graphics.moveTo(tileW / 2, tileH); // Bottom Corner
graphics.lineTo(tileW, tileH / 2); // Right Corner
graphics.lineTo(tileW, tileH / 2 + thickness); // Right Corner (Lowered)
graphics.lineTo(tileW / 2, tileH + thickness); // Bottom Corner (Lowered)
graphics.closePath();
graphics.fillPath();
// 3. Draw TOP Surface (bright)
graphics.fillStyle(type.color, 1);
graphics.beginPath();
graphics.moveTo(tileW / 2, 0); // Top
graphics.lineTo(tileW, tileH / 2); // Right
graphics.lineTo(tileW / 2, tileH); // Bottom
graphics.lineTo(0, tileH / 2); // Left
graphics.closePath();
graphics.fillPath();
// Generate texture
graphics.generateTexture(key, tileW, tileH + thickness);
graphics.generateTexture(type.name, tileWidth, tileHeight);
graphics.destroy();
// Update texture name in type def
type.texture = key;
}
});
}
createGravestoneSprite() {
const canvas = document.createElement('canvas');
const size = 32;
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d', { willReadFrequently: true });
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)
generate() {
console.log(`🌍 Generating terrain data: ${this.width}x${this.height}...`);
// Zagotovi teksture
this.createTileTextures();
if (!this.scene.textures.exists('flower')) TextureGenerator.createFlowerSprite(this.scene, 'flower');
if (this.scene.textures.exists('stone_sprite')) {
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');
}
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());
} else if (!this.scene.textures.exists('tree')) {
TextureGenerator.createTreeSprite(this.scene, 'tree');
}
if (this.scene.textures.exists('objects_pack') && !this.scene.textures.exists('gravestone')) {
this.createGravestoneSprite();
}
for (let i = 1; i <= 4; 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();
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);
let elevation = this.noise.getNormalized(x, y, 0.03, 3);
let terrainType = this.getTerrainTypeByElevation(noiseValue, elevation);
const nx = x * 0.1;
const ny = y * 0.1;
const elevation = this.noise.noise(nx, ny);
const pathNoise = this.noise.getNormalized(x, y, 0.08, 2);
if (pathNoise > 0.48 && pathNoise < 0.52 && noiseValue > 0.35) {
terrainType = this.terrainTypes.PATH;
}
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;
if (isEdge) terrainType = this.terrainTypes.STONE;
if (isEdge) elevation = 0;
else if (isNearEdge) elevation = Math.max(0, elevation - 0.2);
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;
this.tiles[y][x] = {
gridX: x,
gridY: y,
type: terrainType.name,
texture: terrainType.texture,
height: noiseValue,
elevation: elevation,
yLayer: terrainType.yLayer,
texture: terrainType.name,
hasDecoration: false,
hasCrop: false
};
@@ -291,24 +165,33 @@ class TerrainSystem {
if (terrainType.name.includes('grass')) {
const rand = Math.random();
if (elevation > 0.6 && rand < 0.05) {
if (elevation > 0.6 && rand < 0.1) {
decorType = 'bush';
maxHp = 5;
} else if (rand < 0.025) {
}
// Trees - Volumetric
else if (rand < 0.15) {
decorType = 'tree';
maxHp = 5;
const sizeRand = Math.random();
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) {
if (sizeRand < 0.2) scale = 0.8;
else if (sizeRand < 0.8) scale = 1.0 + Math.random() * 0.3;
else scale = 1.3;
}
// Rocks - Volumetric
else if (rand < 0.18) {
decorType = 'rock'; // 'rock' texture from TextureGenerator
maxHp = 8;
scale = 1.5; // Big rocks
}
else if (rand < 0.19) {
decorType = 'gravestone';
maxHp = 10;
} else if (rand < 0.08) {
} else if (rand < 0.30) {
decorType = 'flower';
maxHp = 1;
}
} else if (terrainType.name === 'dirt' && Math.random() < 0.02) {
} else if (terrainType.name === 'dirt' && Math.random() < 0.05) {
decorType = 'bush';
maxHp = 3;
}
@@ -401,14 +284,12 @@ class TerrainSystem {
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;
const key = `${x},${y}`;
if (this.visibleTiles.has(key)) {
const sprite = this.visibleTiles.get(key);
sprite.setTexture(typeDef.texture);
sprite.setTexture(typeName);
}
}
@@ -446,8 +327,14 @@ class TerrainSystem {
this.offsetY = offsetY;
}
getTile(x, y) {
if (this.tiles[y] && this.tiles[y][x]) {
return this.tiles[y][x];
}
return null;
}
updateCulling(camera) {
// Simple Culling
const view = camera.worldView;
let buffer = 200;
if (this.scene.settings && this.scene.settings.viewDistance === 'LOW') buffer = 50;
@@ -471,25 +358,26 @@ class TerrainSystem {
const startY = Math.max(0, minGridY);
const endY = Math.min(this.height, maxGridY);
const neededKeys = new Set();
const neededTileKeys = new Set();
const neededDecorKeys = new Set();
const neededCropKeys = new Set();
for (let y = startY; y < endY; y++) {
for (let x = startX; x < endX; x++) {
// TILES
const key = `${x},${y}`;
neededKeys.add(key);
const tile = 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();
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);
// TILES
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
@@ -501,23 +389,19 @@ class TerrainSystem {
const screenPos = this.iso.toScreen(x, y);
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);
if (decor.type.includes('house_sprite') || decor.type.includes('market') || decor.type.includes('structure')) {
sprite.setOrigin(0.5, 0.8);
} else {
sprite.setTexture(decor.type);
sprite.setScale(decor.scale || 1);
// Volumetric trees/rocks origin adjustment
// Usually volumetric needed (0.5, 1) or slightly different?
sprite.setOrigin(0.5, 0.9); // Slight tweak
}
// 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);
}
sprite.setTexture(decor.type);
sprite.setScale(decor.scale || 1.0);
sprite.setDepth(this.iso.getDepth(x, y) + 1); // Above tile
sprite.setDepth(this.iso.getDepth(x, y) + 1);
this.visibleDecorations.set(key, sprite);
}
}
@@ -538,16 +422,14 @@ class TerrainSystem {
}
}
// Cleanup tiles
// Cleanup
for (const [key, sprite] of this.visibleTiles) {
if (!neededKeys.has(key)) {
if (!neededTileKeys.has(key)) {
sprite.setVisible(false);
this.tilePool.release(sprite);
this.visibleTiles.delete(key);
}
}
// Cleanup decorations
for (const [key, sprite] of this.visibleDecorations) {
if (!neededDecorKeys.has(key)) {
sprite.setVisible(false);
@@ -555,8 +437,6 @@ class TerrainSystem {
this.visibleDecorations.delete(key);
}
}
// Cleanup crops
for (const [key, sprite] of this.visibleCrops) {
if (!neededCropKeys.has(key)) {
sprite.setVisible(false);

68
src/utils/SpatialGrid.js Normal file
View File

@@ -0,0 +1,68 @@
class SpatialGrid {
constructor(cellSize = 10) {
this.cellSize = cellSize;
this.buckets = new Map(); // Key: "gridX,gridY" -> Set of entities
}
_getKey(x, y) {
const cx = Math.floor(x / this.cellSize);
const cy = Math.floor(y / this.cellSize);
return `${cx},${cy}`;
}
add(entity) {
// Entity must have gridX and gridY
const key = this._getKey(entity.gridX, entity.gridY);
if (!this.buckets.has(key)) {
this.buckets.set(key, new Set());
}
this.buckets.get(key).add(entity);
entity._spatialKey = key;
}
updateEntity(entity) {
const oldKey = entity._spatialKey;
const newKey = this._getKey(entity.gridX, entity.gridY);
if (oldKey !== newKey) {
if (oldKey && this.buckets.has(oldKey)) {
this.buckets.get(oldKey).delete(entity);
if (this.buckets.get(oldKey).size === 0) {
this.buckets.delete(oldKey);
}
}
this.add(entity);
}
}
remove(entity) {
const key = entity._spatialKey;
if (key && this.buckets.has(key)) {
this.buckets.get(key).delete(entity);
if (this.buckets.get(key).size === 0) {
this.buckets.delete(key);
}
}
entity._spatialKey = null;
}
// Najdi entitete v bližini (v sosednjih bucketih)
query(x, y) {
const results = [];
const cx = Math.floor(x / this.cellSize);
const cy = Math.floor(y / this.cellSize);
// Preveri 3x3 sosednjih bucketov (center + 8 sosedov)
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
const key = `${cx + dx},${cy + dy}`;
if (this.buckets.has(key)) {
for (const ent of this.buckets.get(key)) {
results.push(ent);
}
}
}
}
return results;
}
}

File diff suppressed because it is too large Load Diff