kockasta mapa
This commit is contained in:
@@ -67,6 +67,7 @@
|
|||||||
<script src="src/utils/IsometricUtils.js"></script>
|
<script src="src/utils/IsometricUtils.js"></script>
|
||||||
<script src="src/utils/TextureGenerator.js"></script>
|
<script src="src/utils/TextureGenerator.js"></script>
|
||||||
<script src="src/utils/ObjectPool.js"></script>
|
<script src="src/utils/ObjectPool.js"></script>
|
||||||
|
<script src="src/utils/SpatialGrid.js"></script>
|
||||||
|
|
||||||
<!-- Systems -->
|
<!-- Systems -->
|
||||||
<script src="src/systems/TerrainSystem.js"></script>
|
<script src="src/systems/TerrainSystem.js"></script>
|
||||||
@@ -74,6 +75,7 @@
|
|||||||
<!-- TimeSystem merged into WeatherSystem -->
|
<!-- TimeSystem merged into WeatherSystem -->
|
||||||
<script src="src/systems/StatsSystem.js"></script>
|
<script src="src/systems/StatsSystem.js"></script>
|
||||||
<script src="src/systems/InventorySystem.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/InteractionSystem.js"></script>
|
||||||
<script src="src/systems/FarmingSystem.js"></script>
|
<script src="src/systems/FarmingSystem.js"></script>
|
||||||
<script src="src/systems/BuildingSystem.js"></script>
|
<script src="src/systems/BuildingSystem.js"></script>
|
||||||
|
|||||||
@@ -21,22 +21,54 @@ Stvari, ki so bile uspešno implementirane in izboljšale delovanje.
|
|||||||
## 🟡 2. Odprte Tehnične Naloge (To-Do)
|
## 🟡 2. Odprte Tehnične Naloge (To-Do)
|
||||||
Stvari, ki bi jih bilo dobro urediti za boljšo stabilnost.
|
Stvari, ki bi jih bilo dobro urediti za boljšo stabilnost.
|
||||||
|
|
||||||
- [ ] **Global Error Handling**
|
- [x] **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`).
|
- 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.
|
||||||
- [ ] **Centraliziran Loot Manager**
|
- [x] **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.
|
- Implementiran `LootSystem.js`. Skrbi za `spawnLoot`, animacijo dropov, pobiranje (razdalja do igralca) in čiščenje InteractionSystem-a.
|
||||||
- [ ] **Z-Sorting (Depth) Optimizacija**
|
- [x] **Z-Sorting (Depth) Optimizacija**
|
||||||
- Globina se še vedno nastavlja pogosto. Lahko bi optimizirali tako, da se statični objekti sortirajo samo enkrat.
|
- 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)
|
## 🔴 3. Performančne Nadgradnje (High-End)
|
||||||
Če bo igra postala počasna pri velikem svetu (256x256).
|
Če bo igra postala počasna pri velikem svetu (256x256).
|
||||||
|
|
||||||
- [ ] **Phaser Blitter / Tilemap**
|
- [x] Phaser Blitter / Tilemap (Zamenjava 1000 spritov za teren z enim objektom)
|
||||||
- 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.
|
- [x] **Spatial Hashing za Kolizijo**
|
||||||
- [ ] **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.
|
||||||
- Namesto preverjanja razdalje do vsakega NPC-ja uporabiti prostorsko mrežo (Spatial Grid) za hitrejše iskanje sosedov.
|
|
||||||
- [ ] **Web Workers za AI**
|
- [x] Phaser Blitter / Tilemap (Zamenjava 1000 spritov za teren z enim objektom)
|
||||||
- Prestavi pathfinding (iskanje poti) na ločen thread (Worker), da ne blokira glavne igre.
|
- [ ] 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).*
|
*Status: Koda je trenutno stabilna in očiščena (7.12.2025).*
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ class NPC {
|
|||||||
|
|
||||||
// Naključna začetna pavza
|
// Naključna začetna pavza
|
||||||
this.pauseTime = Math.random() * this.maxPauseTime;
|
this.pauseTime = Math.random() * this.maxPauseTime;
|
||||||
|
|
||||||
|
// Register in SpatialGrid
|
||||||
|
if (this.scene.spatialGrid) {
|
||||||
|
this.scene.spatialGrid.add(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tame() {
|
tame() {
|
||||||
@@ -221,6 +226,7 @@ class NPC {
|
|||||||
if (this.isMoving) {
|
if (this.isMoving) {
|
||||||
this.updateDepth();
|
this.updateDepth();
|
||||||
this.updatePosition();
|
this.updatePosition();
|
||||||
|
if (this.scene.spatialGrid) this.scene.spatialGrid.updateEntity(this);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,23 +384,31 @@ class NPC {
|
|||||||
this.healthBarBg.y = this.sprite.y;
|
this.healthBarBg.y = this.sprite.y;
|
||||||
this.healthBar.x = this.sprite.x;
|
this.healthBar.x = this.sprite.x;
|
||||||
this.healthBar.y = this.sprite.y;
|
this.healthBar.y = this.sprite.y;
|
||||||
|
|
||||||
this.healthBarBg.setDepth(this.sprite.depth + 100);
|
|
||||||
this.healthBar.setDepth(this.sprite.depth + 101);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// EYES
|
// EYES
|
||||||
if (this.eyesGroup) {
|
if (this.eyesGroup) {
|
||||||
this.eyesGroup.setPosition(this.sprite.x, this.sprite.y);
|
this.eyesGroup.setPosition(this.sprite.x, this.sprite.y);
|
||||||
this.eyesGroup.setDepth(this.sprite.depth + 2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateDepth();
|
this.updateDepth();
|
||||||
}
|
}
|
||||||
|
|
||||||
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.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() {
|
die() {
|
||||||
console.log('🧟💀 Zombie DEAD');
|
console.log('🧟💀 Zombie DEAD');
|
||||||
// Spawn loot - BONE
|
// 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.scene.interactionSystem.spawnLoot(this.gridX, this.gridY, 'item_bone');
|
||||||
}
|
}
|
||||||
this.destroy();
|
this.destroy();
|
||||||
@@ -492,6 +509,9 @@ class NPC {
|
|||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
if (this.scene.spatialGrid) {
|
||||||
|
this.scene.spatialGrid.remove(this);
|
||||||
|
}
|
||||||
if (this.sprite) this.sprite.destroy();
|
if (this.sprite) this.sprite.destroy();
|
||||||
if (this.healthBar) this.healthBar.destroy();
|
if (this.healthBar) this.healthBar.destroy();
|
||||||
if (this.healthBarBg) this.healthBarBg.destroy();
|
if (this.healthBarBg) this.healthBarBg.destroy();
|
||||||
|
|||||||
@@ -319,9 +319,13 @@ class Player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateDepth() {
|
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);
|
this.sprite.setDepth(this.sprite.y);
|
||||||
if (this.handSprite) this.handSprite.setDepth(this.sprite.y + 1);
|
if (this.handSprite) this.handSprite.setDepth(this.sprite.y + 1);
|
||||||
|
this.lastDepthY = this.sprite.y;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ class GameScene extends Phaser.Scene {
|
|||||||
|
|
||||||
create() {
|
create() {
|
||||||
console.log('🎮 GameScene: Initialized!');
|
console.log('🎮 GameScene: Initialized!');
|
||||||
|
|
||||||
|
// Generate procedural textures
|
||||||
|
new TextureGenerator(this).generateAll();
|
||||||
window.gameState.currentScene = 'GameScene';
|
window.gameState.currentScene = 'GameScene';
|
||||||
|
|
||||||
const width = this.cameras.main.width;
|
const width = this.cameras.main.width;
|
||||||
@@ -28,6 +31,9 @@ class GameScene extends Phaser.Scene {
|
|||||||
// Initialize Isometric Utils
|
// Initialize Isometric Utils
|
||||||
this.iso = new IsometricUtils();
|
this.iso = new IsometricUtils();
|
||||||
|
|
||||||
|
// Initialize Spatial Grid
|
||||||
|
this.spatialGrid = new SpatialGrid(10);
|
||||||
|
|
||||||
// Inicializiraj terrain sistem - 100x100 mapa
|
// Inicializiraj terrain sistem - 100x100 mapa
|
||||||
console.log('🌍 Initializing terrain...');
|
console.log('🌍 Initializing terrain...');
|
||||||
try {
|
try {
|
||||||
@@ -101,6 +107,7 @@ class GameScene extends Phaser.Scene {
|
|||||||
|
|
||||||
this.statsSystem = new StatsSystem(this);
|
this.statsSystem = new StatsSystem(this);
|
||||||
this.inventorySystem = new InventorySystem(this);
|
this.inventorySystem = new InventorySystem(this);
|
||||||
|
this.lootSystem = new LootSystem(this);
|
||||||
this.interactionSystem = new InteractionSystem(this);
|
this.interactionSystem = new InteractionSystem(this);
|
||||||
this.farmingSystem = new FarmingSystem(this);
|
this.farmingSystem = new FarmingSystem(this);
|
||||||
this.buildingSystem = new BuildingSystem(this);
|
this.buildingSystem = new BuildingSystem(this);
|
||||||
@@ -205,6 +212,7 @@ class GameScene extends Phaser.Scene {
|
|||||||
// Update Systems
|
// Update Systems
|
||||||
// TimeSystem update removed (handled by WeatherSystem)
|
// TimeSystem update removed (handled by WeatherSystem)
|
||||||
if (this.statsSystem) this.statsSystem.update(delta);
|
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.interactionSystem) this.interactionSystem.update(delta);
|
||||||
if (this.farmingSystem) this.farmingSystem.update(delta);
|
if (this.farmingSystem) this.farmingSystem.update(delta);
|
||||||
// DayNight update removed (handled by WeatherSystem)
|
// DayNight update removed (handled by WeatherSystem)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class UIScene extends Phaser.Scene {
|
|||||||
this.createInventoryBar();
|
this.createInventoryBar();
|
||||||
this.createGoldDisplay();
|
this.createGoldDisplay();
|
||||||
this.createClock();
|
this.createClock();
|
||||||
this.createDebugInfo();
|
// this.createDebugInfo();
|
||||||
this.createSettingsButton();
|
this.createSettingsButton();
|
||||||
|
|
||||||
// Resize event
|
// Resize event
|
||||||
@@ -430,21 +430,25 @@ class UIScene extends Phaser.Scene {
|
|||||||
|
|
||||||
createDebugInfo() {
|
createDebugInfo() {
|
||||||
if (this.debugText) this.debugText.destroy();
|
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
|
const x = this.scale.width - 170;
|
||||||
// Original was 10, 100 (top leftish). User said "manjkajo na dnu".
|
const y = 120; // Below Gold and Clock area
|
||||||
// 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.
|
|
||||||
|
|
||||||
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',
|
fontSize: '12px',
|
||||||
fontFamily: 'monospace',
|
fontFamily: 'monospace',
|
||||||
fill: '#00ff00', // Green as requested before? Or White? Sticking to Green from file.
|
fill: '#ffffff',
|
||||||
stroke: '#000000',
|
stroke: '#000000',
|
||||||
strokeThickness: 3
|
strokeThickness: 2
|
||||||
});
|
});
|
||||||
|
this.debugText.setDepth(3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
@@ -457,7 +461,6 @@ class UIScene extends Phaser.Scene {
|
|||||||
this.setBarValue(this.healthBar, (hp / maxHp) * 100);
|
this.setBarValue(this.healthBar, (hp / maxHp) * 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync Hunger/Thirst (if stats system exists)
|
|
||||||
if (this.gameScene.statsSystem && this.hungerBar && this.thirstBar) {
|
if (this.gameScene.statsSystem && this.hungerBar && this.thirstBar) {
|
||||||
const stats = this.gameScene.statsSystem;
|
const stats = this.gameScene.statsSystem;
|
||||||
this.setBarValue(this.hungerBar, stats.hunger);
|
this.setBarValue(this.hungerBar, stats.hunger);
|
||||||
|
|||||||
@@ -17,9 +17,6 @@ class InteractionSystem {
|
|||||||
this.scene.input.keyboard.on('keydown-E', () => {
|
this.scene.input.keyboard.on('keydown-E', () => {
|
||||||
this.handleInteractKey();
|
this.handleInteractKey();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Loot Array
|
|
||||||
this.drops = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleInteractKey() {
|
handleInteractKey() {
|
||||||
@@ -30,7 +27,9 @@ class InteractionSystem {
|
|||||||
let nearest = null;
|
let nearest = null;
|
||||||
let minDist = 2.5; // Interaction range
|
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);
|
const d = Phaser.Math.Distance.Between(playerPos.x, playerPos.y, npc.gridX, npc.gridY);
|
||||||
if (d < minDist) {
|
if (d < minDist) {
|
||||||
minDist = d;
|
minDist = d;
|
||||||
@@ -84,8 +83,10 @@ class InteractionSystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3.5 Check for NPC Interaction
|
// 3.5 Check for NPC Interaction
|
||||||
if (this.scene.npcs) {
|
const candidates = this.scene.spatialGrid ? this.scene.spatialGrid.query(gridX, gridY) : this.scene.npcs;
|
||||||
for (const npc of this.scene.npcs) {
|
|
||||||
|
if (candidates) {
|
||||||
|
for (const npc of candidates) {
|
||||||
// Increased radius to 1.8 to catch moving NPCs easier
|
// 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) {
|
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') {
|
if (decor.type === 'tree') {
|
||||||
damage = (activeTool === 'axe') ? 3 : 1;
|
damage = (activeTool === 'axe') ? 3 : 1;
|
||||||
if (!isAttack && activeTool !== 'axe') return;
|
|
||||||
}
|
}
|
||||||
else if (decor.type === 'stone') {
|
else if (decor.type === 'stone') {
|
||||||
damage = (activeTool === 'pickaxe') ? 3 : 1;
|
damage = (activeTool === 'pickaxe') ? 3 : 1;
|
||||||
if (!isAttack && activeTool !== 'pickaxe') return;
|
|
||||||
}
|
}
|
||||||
else if (decor.type === 'bush') damage = 2;
|
else if (decor.type === 'bush') damage = 2;
|
||||||
|
|
||||||
@@ -153,17 +152,18 @@ class InteractionSystem {
|
|||||||
|
|
||||||
if (decor.hp <= 0) {
|
if (decor.hp <= 0) {
|
||||||
const type = this.scene.terrainSystem.removeDecoration(gridX, gridY);
|
const type = this.scene.terrainSystem.removeDecoration(gridX, gridY);
|
||||||
// Loot logic
|
// Loot logic via LootSystem
|
||||||
let loot = 'wood';
|
let loot = 'wood';
|
||||||
if (type === 'stone') loot = 'stone';
|
if (type === 'stone') loot = 'stone';
|
||||||
if (type === 'bush') loot = 'seeds'; // Maybe berries?
|
if (type === 'bush') loot = 'seeds'; // Maybe berries?
|
||||||
if (type === 'tree') {
|
|
||||||
this.spawnLoot(gridX, gridY, 'wood');
|
if (this.scene.lootSystem) {
|
||||||
this.spawnLoot(gridX, gridY, 'wood');
|
if (type === 'tree') {
|
||||||
this.spawnLoot(gridX, gridY, 'wood');
|
this.scene.lootSystem.spawnLoot(gridX, gridY, 'wood', 3);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.spawnLoot(gridX, gridY, loot);
|
this.scene.lootSystem.spawnLoot(gridX, gridY, loot, 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
@@ -192,42 +192,7 @@ class InteractionSystem {
|
|||||||
this.scene.tweens.add({ targets: txt, y: txt.y - 40, alpha: 0, duration: 1000, onComplete: () => txt.destroy() });
|
this.scene.tweens.add({ targets: txt, y: txt.y - 40, alpha: 0, duration: 1000, onComplete: () => txt.destroy() });
|
||||||
}
|
}
|
||||||
|
|
||||||
spawnLoot(gridX, gridY, type) {
|
update(delta) {
|
||||||
console.log(`🎁 Spawning ${type} at ${gridX},${gridY}`);
|
// No logic needed here anymore (loot pickup handled by LootSystem)
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
118
src/systems/LootSystem.js
Normal file
118
src/systems/LootSystem.js
Normal 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()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,276 +10,150 @@ class TerrainSystem {
|
|||||||
this.noise = new PerlinNoise(Date.now());
|
this.noise = new PerlinNoise(Date.now());
|
||||||
|
|
||||||
this.tiles = [];
|
this.tiles = [];
|
||||||
this.decorations = []; // Array za save/load compat
|
this.decorations = [];
|
||||||
this.decorationsMap = new Map(); // Fast lookup key->decor
|
this.decorationsMap = new Map();
|
||||||
this.cropsMap = new Map(); // Store dynamic crops separately
|
this.cropsMap = new Map();
|
||||||
// Render state monitoring
|
|
||||||
this.visibleTiles = new Map(); // Key: "x,y", Value: Sprite
|
this.visibleTiles = new Map();
|
||||||
this.visibleDecorations = new Map(); // Key: "x,y", Value: Sprite
|
this.visibleDecorations = new Map();
|
||||||
this.visibleCrops = new Map(); // Key: "x,y", Value: Sprite
|
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.offsetX = 0;
|
||||||
this.offsetY = 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() {
|
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)) {
|
types.forEach((type) => {
|
||||||
const key = `tile_${type.name}`;
|
if (this.scene.textures.exists(type.name)) return;
|
||||||
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
|
|
||||||
|
|
||||||
const graphics = this.scene.make.graphics({ x: 0, y: 0, add: false });
|
const graphics = this.scene.make.graphics({ x: 0, y: 0, add: false });
|
||||||
|
|
||||||
// Helper for colors
|
const x = 0;
|
||||||
const baseColor = Phaser.Display.Color.IntegerToColor(type.color);
|
const top = 0;
|
||||||
let darkColor, darkerColor;
|
const midX = 24;
|
||||||
|
const midY = 12;
|
||||||
|
const bottomY = 24;
|
||||||
|
|
||||||
// Y-LAYER STACKING: Different side colors based on layer
|
graphics.fillStyle(type.color);
|
||||||
if (type.name === 'grass_full') {
|
|
||||||
// A: Full Grass Block - všechno zeleno
|
// Diamond Only
|
||||||
darkColor = 0x5cb85c; // Green (lighter)
|
graphics.beginPath();
|
||||||
darkerColor = 0x4a9d3f; // Green (darker)
|
graphics.moveTo(midX, top);
|
||||||
} else if (type.name === 'grass_top') {
|
graphics.lineTo(x + 48, midY);
|
||||||
// B: Grass Top - zgoraj zeleno, stranice zemlja
|
graphics.lineTo(midX, bottomY);
|
||||||
darkColor = 0x8b6f47; // Dirt color (brown)
|
graphics.lineTo(x, midY);
|
||||||
darkerColor = 0x5d4a2e; // Darker Dirt
|
graphics.closePath();
|
||||||
} else if (type.name === 'dirt') {
|
graphics.fill();
|
||||||
// C: Full Dirt - vse rjavo
|
|
||||||
darkColor = 0x8b6f47; // Dirt
|
// Grid Stroke (Black/Dark)
|
||||||
darkerColor = 0x654321; // Darker Dirt
|
graphics.lineStyle(1, 0x000000, 0.3);
|
||||||
} else {
|
graphics.strokePath();
|
||||||
// Standard block: Darken base color significantly
|
|
||||||
darkColor = Phaser.Display.Color.IntegerToColor(type.color).darken(30).color;
|
// Simple details
|
||||||
darkerColor = Phaser.Display.Color.IntegerToColor(type.color).darken(50).color;
|
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.generateTexture(type.name, tileWidth, tileHeight);
|
||||||
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.destroy();
|
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() {
|
generate() {
|
||||||
console.log(`🌍 Generating terrain data: ${this.width}x${this.height}...`);
|
|
||||||
|
|
||||||
// Zagotovi teksture
|
|
||||||
this.createTileTextures();
|
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++) {
|
for (let y = 0; y < this.height; y++) {
|
||||||
this.tiles[y] = [];
|
this.tiles[y] = [];
|
||||||
for (let x = 0; x < this.width; x++) {
|
for (let x = 0; x < this.width; x++) {
|
||||||
const noiseValue = this.noise.getNormalized(x, y, 0.05, 4);
|
const nx = x * 0.1;
|
||||||
let elevation = this.noise.getNormalized(x, y, 0.03, 3);
|
const ny = y * 0.1;
|
||||||
let terrainType = this.getTerrainTypeByElevation(noiseValue, elevation);
|
const elevation = this.noise.noise(nx, ny);
|
||||||
|
|
||||||
const pathNoise = this.noise.getNormalized(x, y, 0.08, 2);
|
let terrainType = this.terrainTypes.WATER;
|
||||||
if (pathNoise > 0.48 && pathNoise < 0.52 && noiseValue > 0.35) {
|
if (elevation > this.terrainTypes.SAND.height) terrainType = this.terrainTypes.SAND;
|
||||||
terrainType = this.terrainTypes.PATH;
|
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;
|
||||||
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);
|
|
||||||
|
|
||||||
this.tiles[y][x] = {
|
this.tiles[y][x] = {
|
||||||
gridX: x,
|
|
||||||
gridY: y,
|
|
||||||
type: terrainType.name,
|
type: terrainType.name,
|
||||||
texture: terrainType.texture,
|
texture: terrainType.name,
|
||||||
height: noiseValue,
|
|
||||||
elevation: elevation,
|
|
||||||
yLayer: terrainType.yLayer,
|
|
||||||
hasDecoration: false,
|
hasDecoration: false,
|
||||||
hasCrop: false
|
hasCrop: false
|
||||||
};
|
};
|
||||||
@@ -291,24 +165,33 @@ class TerrainSystem {
|
|||||||
|
|
||||||
if (terrainType.name.includes('grass')) {
|
if (terrainType.name.includes('grass')) {
|
||||||
const rand = Math.random();
|
const rand = Math.random();
|
||||||
if (elevation > 0.6 && rand < 0.05) {
|
if (elevation > 0.6 && rand < 0.1) {
|
||||||
decorType = 'bush';
|
decorType = 'bush';
|
||||||
maxHp = 5;
|
maxHp = 5;
|
||||||
} else if (rand < 0.025) {
|
}
|
||||||
|
// Trees - Volumetric
|
||||||
|
else if (rand < 0.15) {
|
||||||
decorType = 'tree';
|
decorType = 'tree';
|
||||||
maxHp = 5;
|
maxHp = 5;
|
||||||
const sizeRand = Math.random();
|
const sizeRand = Math.random();
|
||||||
if (sizeRand < 0.2) scale = 0.25;
|
if (sizeRand < 0.2) scale = 0.8;
|
||||||
else if (sizeRand < 0.8) scale = 0.6 + Math.random() * 0.2;
|
else if (sizeRand < 0.8) scale = 1.0 + Math.random() * 0.3;
|
||||||
else scale = 1.0;
|
else scale = 1.3;
|
||||||
} else if (rand < 0.03) {
|
}
|
||||||
|
// 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';
|
decorType = 'gravestone';
|
||||||
maxHp = 10;
|
maxHp = 10;
|
||||||
} else if (rand < 0.08) {
|
} else if (rand < 0.30) {
|
||||||
decorType = 'flower';
|
decorType = 'flower';
|
||||||
maxHp = 1;
|
maxHp = 1;
|
||||||
}
|
}
|
||||||
} else if (terrainType.name === 'dirt' && Math.random() < 0.02) {
|
} else if (terrainType.name === 'dirt' && Math.random() < 0.05) {
|
||||||
decorType = 'bush';
|
decorType = 'bush';
|
||||||
maxHp = 3;
|
maxHp = 3;
|
||||||
}
|
}
|
||||||
@@ -401,14 +284,12 @@ class TerrainSystem {
|
|||||||
|
|
||||||
setTileType(x, y, typeName) {
|
setTileType(x, y, typeName) {
|
||||||
if (!this.tiles[y] || !this.tiles[y][x]) return;
|
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].type = typeName;
|
||||||
this.tiles[y][x].texture = typeDef.texture;
|
|
||||||
const key = `${x},${y}`;
|
const key = `${x},${y}`;
|
||||||
if (this.visibleTiles.has(key)) {
|
if (this.visibleTiles.has(key)) {
|
||||||
const sprite = this.visibleTiles.get(key);
|
const sprite = this.visibleTiles.get(key);
|
||||||
sprite.setTexture(typeDef.texture);
|
sprite.setTexture(typeName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,8 +327,14 @@ class TerrainSystem {
|
|||||||
this.offsetY = offsetY;
|
this.offsetY = offsetY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTile(x, y) {
|
||||||
|
if (this.tiles[y] && this.tiles[y][x]) {
|
||||||
|
return this.tiles[y][x];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
updateCulling(camera) {
|
updateCulling(camera) {
|
||||||
// Simple Culling
|
|
||||||
const view = camera.worldView;
|
const view = camera.worldView;
|
||||||
let buffer = 200;
|
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;
|
||||||
@@ -471,25 +358,26 @@ class TerrainSystem {
|
|||||||
const startY = Math.max(0, minGridY);
|
const startY = Math.max(0, minGridY);
|
||||||
const endY = Math.min(this.height, maxGridY);
|
const endY = Math.min(this.height, maxGridY);
|
||||||
|
|
||||||
const neededKeys = new Set();
|
const neededTileKeys = new Set();
|
||||||
const neededDecorKeys = new Set();
|
const neededDecorKeys = new Set();
|
||||||
const neededCropKeys = new Set();
|
const neededCropKeys = new Set();
|
||||||
|
|
||||||
for (let y = startY; y < endY; y++) {
|
for (let y = startY; y < endY; y++) {
|
||||||
for (let x = startX; x < endX; x++) {
|
for (let x = startX; x < endX; x++) {
|
||||||
// TILES
|
|
||||||
const key = `${x},${y}`;
|
const key = `${x},${y}`;
|
||||||
neededKeys.add(key);
|
const tile = this.tiles[y][x];
|
||||||
|
|
||||||
if (!this.visibleTiles.has(key)) {
|
// TILES
|
||||||
const tile = this.tiles[y][x];
|
if (tile) {
|
||||||
const screenPos = this.iso.toScreen(x, y);
|
neededTileKeys.add(key);
|
||||||
const sprite = this.tilePool.get();
|
if (!this.visibleTiles.has(key)) {
|
||||||
|
const sprite = this.tilePool.get();
|
||||||
sprite.setPosition(screenPos.x + this.offsetX, screenPos.y + this.offsetY);
|
sprite.setTexture(tile.type);
|
||||||
sprite.setTexture(tile.texture);
|
const screenPos = this.iso.toScreen(x, y);
|
||||||
sprite.setDepth(this.iso.getDepth(x, y));
|
sprite.setPosition(screenPos.x + this.offsetX, screenPos.y + this.offsetY);
|
||||||
this.visibleTiles.set(key, sprite);
|
sprite.setDepth(this.iso.getDepth(x, y));
|
||||||
|
this.visibleTiles.set(key, sprite);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DECORATIONS
|
// DECORATIONS
|
||||||
@@ -501,23 +389,19 @@ class TerrainSystem {
|
|||||||
const screenPos = this.iso.toScreen(x, y);
|
const screenPos = this.iso.toScreen(x, y);
|
||||||
|
|
||||||
sprite.setPosition(screenPos.x + this.offsetX, screenPos.y + this.offsetY);
|
sprite.setPosition(screenPos.x + this.offsetX, screenPos.y + this.offsetY);
|
||||||
// Fix for house sprite
|
|
||||||
if (decor.type.includes('house_sprite') || decor.type.includes('market_sprite')) {
|
if (decor.type.includes('house_sprite') || decor.type.includes('market') || decor.type.includes('structure')) {
|
||||||
sprite.setTexture(decor.type);
|
sprite.setOrigin(0.5, 0.8);
|
||||||
sprite.setScale(decor.scale || 1.0);
|
|
||||||
} else {
|
} else {
|
||||||
sprite.setTexture(decor.type);
|
// Volumetric trees/rocks origin adjustment
|
||||||
sprite.setScale(decor.scale || 1);
|
// Usually volumetric needed (0.5, 1) or slightly different?
|
||||||
|
sprite.setOrigin(0.5, 0.9); // Slight tweak
|
||||||
}
|
}
|
||||||
|
|
||||||
// Origin adjusted for buildings
|
sprite.setTexture(decor.type);
|
||||||
if (decor.type.includes('house') || decor.type.includes('market')) {
|
sprite.setScale(decor.scale || 1.0);
|
||||||
sprite.setOrigin(0.5, 0.8); // Adjusted origin for buildings
|
|
||||||
} else {
|
|
||||||
sprite.setOrigin(0.5, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
sprite.setDepth(this.iso.getDepth(x, y) + 1); // Above tile
|
sprite.setDepth(this.iso.getDepth(x, y) + 1);
|
||||||
this.visibleDecorations.set(key, sprite);
|
this.visibleDecorations.set(key, sprite);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -538,16 +422,14 @@ class TerrainSystem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup tiles
|
// Cleanup
|
||||||
for (const [key, sprite] of this.visibleTiles) {
|
for (const [key, sprite] of this.visibleTiles) {
|
||||||
if (!neededKeys.has(key)) {
|
if (!neededTileKeys.has(key)) {
|
||||||
sprite.setVisible(false);
|
sprite.setVisible(false);
|
||||||
this.tilePool.release(sprite);
|
this.tilePool.release(sprite);
|
||||||
this.visibleTiles.delete(key);
|
this.visibleTiles.delete(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup decorations
|
|
||||||
for (const [key, sprite] of this.visibleDecorations) {
|
for (const [key, sprite] of this.visibleDecorations) {
|
||||||
if (!neededDecorKeys.has(key)) {
|
if (!neededDecorKeys.has(key)) {
|
||||||
sprite.setVisible(false);
|
sprite.setVisible(false);
|
||||||
@@ -555,8 +437,6 @@ class TerrainSystem {
|
|||||||
this.visibleDecorations.delete(key);
|
this.visibleDecorations.delete(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup crops
|
|
||||||
for (const [key, sprite] of this.visibleCrops) {
|
for (const [key, sprite] of this.visibleCrops) {
|
||||||
if (!neededCropKeys.has(key)) {
|
if (!neededCropKeys.has(key)) {
|
||||||
sprite.setVisible(false);
|
sprite.setVisible(false);
|
||||||
|
|||||||
68
src/utils/SpatialGrid.js
Normal file
68
src/utils/SpatialGrid.js
Normal 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
Reference in New Issue
Block a user