diff --git a/optimizations.md b/optimizations.md index 6b17c2c..30a24f7 100644 --- a/optimizations.md +++ b/optimizations.md @@ -8,7 +8,7 @@ Cilj: Izboljšati delovanje igre na počasnejših računalnikih (low-end devices 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:** @@ -64,7 +64,8 @@ Dodati meni "Settings", kjer lahko uporabnik izbere: ## Akcijski Plan (Koraki) -1. **Korak 1 (Takoj):** Implementiraj "Chunk System" za `updateCulling`. Ne iteriraj čez 10.000 tileov vsak frame. Iteriraj samo čez vidno območje intersekcije (minX do maxX, minY do maxY). To je enostaven matematičen fix v `TerrainSystem`. -2. **Korak 2:** Prepiši `WeatherSystem` za uporabo preprostih delcev ali zmanjšaj število kapelj na low-end. -3. **Korak 3:** Implementiraj "Throttling" za NPC AI. -4. **Korak 4:** Prehod na `Blitter` za teren (dolgoročno). +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. diff --git a/src/entities/NPC.js b/src/entities/NPC.js index e7bc25a..69e7c52 100644 --- a/src/entities/NPC.js +++ b/src/entities/NPC.js @@ -14,14 +14,17 @@ class NPC { this.iso = new IsometricUtils(48, 24); // Random walk paramters - this.moveSpeed = 100; // px/s (počasnejše od igralca) - this.gridMoveTime = 300; // ms za premik (počasneje) + this.moveSpeed = 100; // px/s + this.gridMoveTime = 300; // ms // Stanje this.state = 'WANDER'; // WANDER, TAMED, FOLLOW this.isMoving = false; this.pauseTime = 0; - this.maxPauseTime = 2000; // Pavza med premiki (2s) + this.maxPauseTime = 2000; + this.attackCooldownTimer = 0; + this.hp = 20; + this.maxHp = 20; // Kreira sprite this.createSprite(); @@ -57,15 +60,47 @@ class NPC { // Change color slightly to indicate friend this.sprite.setTint(0xAAFFAA); + + // Hide Health Bar if tamed + if (this.healthBarBg) { + this.healthBarBg.setVisible(false); + this.healthBar.setVisible(false); + } + + // Change Eyes to Friendly (Cyan) + this.addTamedEyes(); + } + + addTamedEyes() { + if (this.eyesGroup) return; + + // Container for eyes + // Coordinates relative to sprite center bottom (0.5, 1) + // Head is roughly at y - height. + // Assuming sprite height ~50-60px visual. + + this.eyesGroup = this.scene.add.container(this.sprite.x, this.sprite.y); + + // Eyes + const eyeL = this.scene.add.rectangle(-5, -55, 3, 3, 0x00FFFF); + const eyeR = this.scene.add.rectangle(5, -55, 3, 3, 0x00FFFF); + + this.eyesGroup.add([eyeL, eyeR]); + this.eyesGroup.setDepth(this.sprite.depth + 2); } toggleState() { if (this.state === 'WANDER') { this.tame(); } else { - // Maybe command to stay/follow? + // Command to stay/follow this.state = this.state === 'FOLLOW' ? 'STAY' : 'FOLLOW'; console.log(`Command: ${this.state}`); + + // Visual feedback for command + const txt = this.scene.add.text(this.sprite.x, this.sprite.y - 60, this.state, { fontSize: '12px', color: '#FFF' }); + txt.setOrigin(0.5); + this.scene.tweens.add({ targets: txt, y: txt.y - 20, alpha: 0, duration: 1000, onComplete: () => txt.destroy() }); } } @@ -100,12 +135,21 @@ class NPC { this.sprite.setScale(0.3); } + // HEALTH BAR + this.healthBarBg = this.scene.add.graphics(); + this.healthBarBg.fillStyle(0x000000, 0.5); + this.healthBarBg.fillRect(-16, -70, 32, 4); + this.healthBarBg.setVisible(false); + + this.healthBar = this.scene.add.graphics(); + this.healthBar.fillStyle(0x00ff00, 1); + this.healthBar.fillRect(-16, -70, 32, 4); + this.healthBar.setVisible(false); + // Depth sorting this.updateDepth(); } - // ... loop update ... - moveToGrid(targetX, targetY) { // Determine facing direction before moving const dx = targetX - this.gridX; @@ -133,6 +177,7 @@ class NPC { ease: 'Linear', onComplete: () => { this.isMoving = false; + this.updatePosition(); // Stop Animation if (this.sprite.texture.key === 'zombie_walk') { @@ -147,106 +192,259 @@ class NPC { } update(delta) { - this.updateDepth(); // Continuous depth sorting + // 1. Distance Culling + if (this.scene.player) { + const playerPos = this.scene.player.getPosition(); + const dist = Phaser.Math.Distance.Between(this.gridX, this.gridY, playerPos.x, playerPos.y); + const cullDist = (this.state === 'CHASE' || this.state === 'FOLLOW') ? 50 : 30; - if (this.isMoving) { - return; // Že se premika + if (dist > cullDist) { + if (this.sprite.visible) { + this.sprite.setVisible(false); + if (this.healthBar) { + this.healthBar.setVisible(false); + this.healthBarBg.setVisible(false); + } + if (this.eyesGroup) this.eyesGroup.setVisible(false); + } + return; + } else { + if (!this.sprite.visible) { + this.sprite.setVisible(true); + if (this.eyesGroup) this.eyesGroup.setVisible(true); + // Bars stay hidden until damaged usually + } + } } - // Tamed logic + // 2. Optimization: Update depth ONLY if moving + if (this.isMoving) { + this.updateDepth(); + this.updatePosition(); + return; + } + + // 3. AI Logic + if (this.type === 'zombie' && this.state !== 'TAMED' && this.state !== 'FOLLOW') { + this.handleAggressiveAI(delta); + } else { + this.handlePassiveAI(delta); + } + + this.updatePosition(); + } + + handlePassiveAI(delta) { if (this.state === 'TAMED' || this.state === 'FOLLOW') { this.followPlayer(); return; } - // Random walk - pavza med premiki this.pauseTime += delta; - if (this.pauseTime >= this.maxPauseTime) { this.performRandomWalk(); this.pauseTime = 0; } } - followPlayer() { + handleAggressiveAI(delta) { if (!this.scene.player) return; const playerPos = this.scene.player.getPosition(); const dist = Phaser.Math.Distance.Between(this.gridX, this.gridY, playerPos.x, playerPos.y); - // Če je precej dlje, se premakni k njemu - if (dist > 2) { - const dx = Math.sign(playerPos.x - this.gridX); - const dy = Math.sign(playerPos.y - this.gridY); + if (this.attackCooldownTimer > 0) { + this.attackCooldownTimer -= delta; + } - // Move 1 tile towards player - let targetX = this.gridX + dx; - let targetY = this.gridY + dy; - - // Avoid occupying SAME tile as player - if (targetX === playerPos.x && targetY === playerPos.y) { - if (Math.random() < 0.5) targetX = this.gridX; - else targetY = this.gridY; + if (this.state === 'WANDER') { + if (dist < 8) { + this.state = 'CHASE'; + this.showEmote('!'); + } else { + this.pauseTime += delta; + if (this.pauseTime >= this.maxPauseTime) { + this.performRandomWalk(); + this.pauseTime = 0; + } } + } + else if (this.state === 'CHASE') { + if (dist > 15) { + this.state = 'WANDER'; + return; + } + if (dist <= 1.5) { + this.tryAttack(); + } else { + this.moveTowards(playerPos.x, playerPos.y); + } + } + } - this.moveToGrid(targetX, targetY); + moveTowards(targetX, targetY) { + const dx = Math.sign(targetX - this.gridX); + const dy = Math.sign(targetY - this.gridY); + let nextX = this.gridX + dx; + let nextY = this.gridY + dy; + + if (this.isValidMove(nextX, nextY)) { + this.moveToGrid(nextX, nextY); + } else if (this.isValidMove(this.gridX + dx, this.gridY)) { + this.moveToGrid(this.gridX + dx, this.gridY); + } else if (this.isValidMove(this.gridX, this.gridY + dy)) { + this.moveToGrid(this.gridX, this.gridY + dy); + } + } + + isValidMove(x, y) { + const terrainSystem = this.scene.terrainSystem; + if (!terrainSystem) return true; + if (!this.iso.isInBounds(x, y, terrainSystem.width, terrainSystem.height)) return false; + if (terrainSystem.tiles[y][x].type.name === 'water') return false; + + const key = `${x},${y}`; + if (terrainSystem.decorationsMap.has(key)) { + const decor = terrainSystem.decorationsMap.get(key); + const solidTypes = ['tree', 'stone', 'bush', 'wall', 'ruin', 'fence', 'house', 'gravestone']; + if (solidTypes.includes(decor.type)) return false; + } + return true; + } + + tryAttack() { + if (this.attackCooldownTimer > 0) return; + this.attackCooldownTimer = 1500; + + if (this.sprite) { + this.scene.tweens.add({ + targets: this.sprite, + y: this.sprite.y - 10, + yoyo: true, duration: 100, repeat: 1 + }); + } + + if (this.scene.statsSystem) { + this.scene.statsSystem.takeDamage(10); + this.scene.cameras.main.shake(100, 0.005); } } performRandomWalk() { - // Naključna smer (NSEW + možnost obstati) - const directions = [ - { x: -1, y: 0 }, // North-West - { x: 1, y: 0 }, // South-East - { x: 0, y: -1 }, // South-West - { x: 0, y: 1 }, // North-East - { x: 0, y: 0 } // Stay (30% možnost) - ]; - - const dir = Phaser.Math.RND.pick(directions); - const targetX = this.gridX + dir.x; - const targetY = this.gridY + dir.y; - - // Preveri kolizijo z robovi - const terrainSystem = this.scene.terrainSystem; - if (terrainSystem && this.iso.isInBounds(targetX, targetY, terrainSystem.width, terrainSystem.height)) { - // Preveri da ni ista pozicija kot igralec - if (this.scene.player) { - const playerPos = this.scene.player.getPosition(); - if (targetX === playerPos.x && targetY === playerPos.y) { - return; // Ne premakni se na igralca - } - } - - if (dir.x !== 0 || dir.y !== 0) { - this.moveToGrid(targetX, targetY); - } + const dirs = [{ x: 0, y: -1 }, { x: 0, y: 1 }, { x: -1, y: 0 }, { x: 1, y: 0 }]; + const dir = dirs[Math.floor(Math.random() * dirs.length)]; + const nextX = this.gridX + dir.x; + const nextY = this.gridY + dir.y; + if (this.isValidMove(nextX, nextY)) { + this.moveToGrid(nextX, nextY); } } + followPlayer() { + if (!this.scene.player) return; + const playerPos = this.scene.player.getPosition(); + const dist = Phaser.Math.Distance.Between(this.gridX, this.gridY, playerPos.x, playerPos.y); + if (dist < 2) return; + this.moveTowards(playerPos.x, playerPos.y); + } + + showEmote(text) { + const emote = this.scene.add.text(this.sprite.x, this.sprite.y - 40, text, { fontSize: '20px', fontStyle: 'bold', color: '#FF0000' }); + emote.setOrigin(0.5); + emote.setDepth(this.sprite.depth + 100); + this.scene.tweens.add({ targets: emote, y: emote.y - 20, alpha: 0, duration: 1000, onComplete: () => emote.destroy() }); + } updatePosition() { + if (!this.sprite) return; + const screenPos = this.iso.toScreen(this.gridX, this.gridY); - this.sprite.setPosition( - screenPos.x + this.offsetX, - screenPos.y + this.offsetY - ); + + if (!this.isMoving) { + this.sprite.setPosition( + screenPos.x + this.offsetX, + screenPos.y + this.offsetY + ); + } + + // BARS + if (this.healthBar && this.healthBarBg) { + this.healthBarBg.x = this.sprite.x; + 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() { - // Pixel perfect sorting - if (this.sprite) this.sprite.setDepth(this.sprite.y); + if (this.sprite) { + this.sprite.setDepth(this.sprite.y); + } } - getPosition() { - return { x: this.gridX, y: this.gridY }; + takeDamage(amount) { + this.hp -= amount; + + // Show Health Bar + if (this.healthBar) { + this.healthBar.setVisible(true); + this.healthBarBg.setVisible(true); + + // Update width + const percent = Math.max(0, this.hp / this.maxHp); + this.healthBar.clear(); + this.healthBar.fillStyle(percent < 0.3 ? 0xff0000 : 0x00ff00, 1); + this.healthBar.fillRect(-16, -70, 32 * percent, 4); + } + + if (this.sprite) { + this.sprite.setTint(0xff0000); + this.scene.time.delayedCall(100, () => { + if (this.sprite) { + this.sprite.clearTint(); + // Re-apply tamed tint if tamed + if (this.state === 'TAMED' || this.state === 'FOLLOW' || this.state === 'STAY') { + this.sprite.setTint(0xAAFFAA); + } + } + }); + } + + console.log(`Zombie HP: ${this.hp}`); + + if (this.hp <= 0) { + this.die(); + } + } + + die() { + console.log('🧟💀 Zombie DEAD'); + // Spawn loot - BONE + if (this.scene.interactionSystem && this.scene.interactionSystem.spawnLoot) { + this.scene.interactionSystem.spawnLoot(this.gridX, this.gridY, 'item_bone'); + } + this.destroy(); + + const idx = this.scene.npcs.indexOf(this); + if (idx > -1) this.scene.npcs.splice(idx, 1); } destroy() { - if (this.sprite) { - this.sprite.destroy(); - } + if (this.sprite) this.sprite.destroy(); + if (this.healthBar) this.healthBar.destroy(); + if (this.healthBarBg) this.healthBarBg.destroy(); + if (this.eyesGroup) this.eyesGroup.destroy(); } } diff --git a/src/entities/Player.js b/src/entities/Player.js index 7243d2e..1c50205 100644 --- a/src/entities/Player.js +++ b/src/entities/Player.js @@ -6,19 +6,20 @@ class Player { this.gridX = gridX; this.gridY = gridY; - // Terrain offset (za sinhronizacijo s terrain containerjem) + // Terrain offset this.offsetX = offsetX; this.offsetY = offsetY; this.iso = new IsometricUtils(48, 24); - // Hitrostgibanja + // Hitrost gibanja this.moveSpeed = 150; // px/s this.gridMoveTime = 200; // ms za premik na eno kocko // Stanje this.isMoving = false; this.direction = 'down'; + this.lastDir = { x: 0, y: 1 }; // Default south // Kreira sprite this.createSprite(); @@ -26,6 +27,11 @@ class Player { // Setup kontrole this.setupControls(); + // Space za napad + this.scene.input.keyboard.on('keydown-SPACE', () => { + this.attack(); + }); + // Začetna pozicija this.updatePosition(); } @@ -53,7 +59,7 @@ class Player { // Scale logic if (isAnimated) { - this.sprite.setScale(1.5); // 64px frame -> looks good around 96px total height relative to 48px tile + this.sprite.setScale(1.5); } else { this.sprite.setScale(0.3); } @@ -61,58 +67,17 @@ class Player { // --- HAND / HELD ITEM SPRITE --- this.handSprite = this.scene.add.sprite( screenPos.x + this.offsetX + 10, - screenPos.y + this.offsetY - 25, // Adjusted for new height + screenPos.y + this.offsetY - 25, 'item_axe' ); this.handSprite.setOrigin(0.5, 0.5); this.handSprite.setScale(0.25); this.handSprite.setVisible(false); - // Depth sorting - this.updateDepth(); - } - - // ... setupControls ... - - // ... update ... - - moveToGrid(targetX, targetY) { - this.isMoving = true; - this.gridX = targetX; - this.gridY = targetY; - - const targetScreen = this.iso.toScreen(targetX, targetY); - - // Play Animation - if (this.sprite.texture.key === 'player_walk') { - this.sprite.play('player_walk_anim', true); - } - - // Tween za smooth gibanje - this.scene.tweens.add({ - targets: [this.sprite, this.handSprite], // Move both - x: '+=' + (targetScreen.x + this.offsetX - this.sprite.x), - y: '+=' + (targetScreen.y + this.offsetY - this.sprite.y), - duration: this.gridMoveTime, - ease: 'Linear', - onComplete: () => { - this.isMoving = false; - this.updatePosition(); - - // Stop Animation - if (this.sprite.texture.key === 'player_walk') { - this.sprite.stop(); - this.sprite.setFrame(0); // Idle frame - } - } - }); - - // Posodobi depth this.updateDepth(); } setupControls() { - // WASD kontrole this.keys = this.scene.input.keyboard.addKeys({ up: Phaser.Input.Keyboard.KeyCodes.W, down: Phaser.Input.Keyboard.KeyCodes.S, @@ -121,14 +86,43 @@ class Player { }); } + attack() { + console.log('⚔️ Player Attack!'); + if (this.scene.interactionSystem) { + const targetX = this.gridX + this.lastDir.x; + const targetY = this.gridY + this.lastDir.y; + this.scene.interactionSystem.handleInteraction(targetX, targetY, true); // true = attackMode + } + + // Animation + this.scene.tweens.add({ + targets: this.handSprite, + angle: 45, // Swing + yoyo: true, + duration: 100 + }); + + // Player lunge + const lungeX = this.sprite.x + (this.lastDir.x * 10); + const lungeY = this.sprite.y + (this.lastDir.y * 5); + this.scene.tweens.add({ + targets: this.sprite, + x: lungeX, + y: lungeY, + yoyo: true, + duration: 50 + }); + } + update(delta) { - this.updateDepth(); + if (this.isMoving) { + this.updateDepth(); + } if (!this.isMoving) { this.handleInput(); } - // Sync Held Item with Inventory this.updateHeldItem(); } @@ -158,47 +152,110 @@ class Player { let targetX = this.gridX; let targetY = this.gridY; let moved = false; - let facingRight = !this.sprite.flipX; // Keep current + let facingRight = !this.sprite.flipX; - // WASD za isometric movement - if (this.keys.up.isDown) { // North-West - targetX--; + // WASD + let dx = 0; + let dy = 0; + + if (this.keys.up.isDown) { + dx = -1; dy = 0; moved = true; - facingRight = false; // Left-ish - } else if (this.keys.down.isDown) { // South-East - targetX++; + facingRight = false; + } else if (this.keys.down.isDown) { + dx = 1; dy = 0; moved = true; - facingRight = true; // Right-ish + facingRight = true; } - if (this.keys.left.isDown) { // South-West - targetY++; // SWAPPED: Was -- + if (this.keys.left.isDown) { + dx = 0; dy = 1; moved = true; - facingRight = false; // Left-ish - } else if (this.keys.right.isDown) { // North-East - targetY--; // SWAPPED: Was ++ + facingRight = false; + } else if (this.keys.right.isDown) { + dx = 0; dy = -1; moved = true; - facingRight = true; // Right-ish + facingRight = true; } - // Apply Facing - this.sprite.setFlipX(!facingRight); + // Update target + targetX = this.gridX + dx; + targetY = this.gridY + dy; - // Update Hand Position based on facing - const handOffset = facingRight ? 10 : -10; - this.handSprite.setX(this.sprite.x + handOffset); - this.handSprite.setFlipX(!facingRight); + // Update Facing Direction and Last Dir + if (moved) { + // Keep diagonal input clean or prioritize one axis? + // Just use the calculated dx/dy. + // Note: If both UP and LEFT pressed, logic above overwrites dx/dy. + // Let's refine to allow diagonal accumulation if needed, but existing logic prioritized axis. + // Current logic: RIGHT/LEFT overwrites UP/DOWN. This is fine for now. - // Preveri kolizijo z robovi mape + this.lastDir = { x: dx, y: dy }; + this.sprite.setFlipX(!facingRight); + + // Hand offset + const handOffset = facingRight ? 10 : -10; + this.handSprite.setX(this.sprite.x + handOffset); + this.handSprite.setFlipX(!facingRight); + } + + // Collision Check const terrainSystem = this.scene.terrainSystem; if (moved && terrainSystem) { if (this.iso.isInBounds(targetX, targetY, terrainSystem.width, terrainSystem.height)) { - this.moveToGrid(targetX, targetY); + + const tile = terrainSystem.tiles[targetY][targetX]; + let isPassable = true; + + if (tile.type.name === 'water') isPassable = false; + + const key = `${targetX},${targetY}`; + if (terrainSystem.decorationsMap.has(key)) { + const decor = terrainSystem.decorationsMap.get(key); + const solidTypes = ['tree', 'stone', 'bush', 'wall', 'ruin', 'fence', 'house', 'gravestone']; + if (solidTypes.includes(decor.type)) { + console.log('⛔ Blocked by:', decor.type); + isPassable = false; + } + } + + if (isPassable) { + this.moveToGrid(targetX, targetY); + } } } } + moveToGrid(targetX, targetY) { + this.isMoving = true; + this.gridX = targetX; + this.gridY = targetY; + const targetScreen = this.iso.toScreen(targetX, targetY); + + if (this.sprite.texture.key === 'player_walk') { + this.sprite.play('player_walk_anim', true); + } + + this.scene.tweens.add({ + targets: [this.sprite, this.handSprite], + x: '+=' + (targetScreen.x + this.offsetX - this.sprite.x), + y: '+=' + (targetScreen.y + this.offsetY - this.sprite.y), + duration: this.gridMoveTime, + ease: 'Linear', + onComplete: () => { + this.isMoving = false; + this.updatePosition(); + + if (this.sprite.texture.key === 'player_walk') { + this.sprite.stop(); + this.sprite.setFrame(0); + } + } + }); + + this.updateDepth(); + } updatePosition() { const screenPos = this.iso.toScreen(this.gridX, this.gridY); @@ -207,7 +264,6 @@ class Player { screenPos.y + this.offsetY ); - // Update hand const facingRight = !this.sprite.flipX; const handOffset = facingRight ? 10 : -10; this.handSprite.setPosition( @@ -219,7 +275,6 @@ class Player { } updateDepth() { - // Pixel-perfect depth sorting based on screen Y if (this.sprite) { this.sprite.setDepth(this.sprite.y); if (this.handSprite) this.handSprite.setDepth(this.sprite.y + 1); @@ -235,9 +290,7 @@ class Player { } destroy() { - if (this.sprite) { - this.sprite.destroy(); - } + if (this.sprite) this.sprite.destroy(); } dieAnimation() { diff --git a/src/scenes/GameScene.js b/src/scenes/GameScene.js index 795f4dc..a385af6 100644 --- a/src/scenes/GameScene.js +++ b/src/scenes/GameScene.js @@ -6,6 +6,13 @@ class GameScene extends Phaser.Scene { this.terrainContainer = null; this.player = null; this.npcs = []; // Array za NPCje + + // Settings + this.settings = { + viewDistance: 'HIGH', // LOW, MEDIUM, HIGH + particles: 'HIGH', // NONE, LOW, HIGH + shadows: true + }; } create() { @@ -108,6 +115,9 @@ class GameScene extends Phaser.Scene { console.log('🌄 Initializing Parallax System...'); this.parallaxSystem = new ParallaxSystem(this); + // Generate Item Sprites for UI + TextureGenerator.createItemSprites(this); + // Launch UI Scene console.log('🖥️ Launching UI Scene...'); this.scene.launch('UIScene'); @@ -115,10 +125,10 @@ class GameScene extends Phaser.Scene { // Initialize Save System this.saveSystem = new SaveSystem(this); - // Auto-load if available (optional, for now manual) - // this.saveSystem.loadGame(); + // Auto-load if available + this.saveSystem.loadGame(); - console.log('✅ GameScene ready - FAZA 17!'); + console.log('✅ GameScene ready - FAZA 18 (Crafting & AI)!'); } setupCamera() { @@ -277,4 +287,12 @@ class GameScene extends Phaser.Scene { this.clouds.push({ sprite: cloud, speed: Phaser.Math.FloatBetween(10, 30) }); } } + + saveGame() { + if (this.saveSystem) this.saveSystem.saveGame(); + } + + loadGame() { + if (this.saveSystem) this.saveSystem.loadGame(); + } } diff --git a/src/scenes/UIScene.js b/src/scenes/UIScene.js index 04afa22..f5c4d18 100644 --- a/src/scenes/UIScene.js +++ b/src/scenes/UIScene.js @@ -20,9 +20,121 @@ class UIScene extends Phaser.Scene { this.createGoldDisplay(); this.createClock(); this.createDebugInfo(); + this.createSettingsButton(); // Resize event this.scale.on('resize', this.resize, this); + + // Crafting Menu (C) + this.input.keyboard.on('keydown-C', () => { + this.toggleCraftingMenu(); + }); + + // Save (F5) + this.input.keyboard.on('keydown-F5', () => { + if (this.gameScene) this.gameScene.saveGame(); + }); + + // Factory Reset (F8) - Fix za "zginla drevesa" + this.input.keyboard.on('keydown-F8', () => { + console.log('🔥 FACTORY RESET - Clearing Save & Reloading...'); + localStorage.removeItem('novafarma_savefile'); + window.location.reload(); + }); + } + + // ... (rest of class) ... + + toggleCraftingMenu() { + if (!this.craftingContainer) this.createCraftingMenu(); + this.craftingContainer.setVisible(!this.craftingContainer.visible); + } + + createCraftingMenu() { + const w = 300; + const h = 250; + const x = this.scale.width / 2; + const y = this.scale.height / 2; + + this.craftingContainer = this.add.container(x, y); + this.craftingContainer.setDepth(2000); // Top of everything + + // Bg + const bg = this.add.graphics(); + bg.fillStyle(0x222222, 0.95); + bg.fillRect(-w / 2, -h / 2, w, h); + bg.lineStyle(2, 0x888888, 1); + bg.strokeRect(-w / 2, -h / 2, w, h); + this.craftingContainer.add(bg); + + // Title + const title = this.add.text(0, -h / 2 + 20, 'CRAFTING', { fontSize: '24px', fontStyle: 'bold', color: '#ffffff' }).setOrigin(0.5); + this.craftingContainer.add(title); + + // Recipes + const recipes = [ + { name: 'Axe', code: '1', req: '5 Wood', type: 'axe', cost: { wood: 5 } }, + { name: 'Pickaxe', code: '2', req: '5 Wood, 2 Stone', type: 'pickaxe', cost: { wood: 5, stone: 2 } }, + { name: 'Hoe', code: '3', req: '3 Wood, 2 Stone', type: 'hoe', cost: { wood: 3, stone: 2 } }, + { name: 'Sword', code: '4', req: '10 Wood, 5 Stone', type: 'sword', cost: { wood: 10, stone: 5 } } + ]; + + recipes.forEach((r, i) => { + const rowY = -h / 2 + 70 + (i * 40); + + // Text + const txt = this.add.text(-w / 2 + 20, rowY, `[${r.code}] ${r.name} (${r.req})`, { + fontSize: '16px', color: '#eeeeee' + }); + this.craftingContainer.add(txt); + + // Button Logic (Keyboard 1-4 works too via GameScene? No, let's localize input) + }); + + // Instruction + const instr = this.add.text(0, h / 2 - 20, 'Press number keys to craft', { fontSize: '12px', color: '#aaaaaa' }).setOrigin(0.5); + this.craftingContainer.add(instr); + + // Input listener for crafting + this.input.keyboard.on('keydown', (e) => { + if (!this.craftingContainer.visible) return; + + const key = e.key; + const recipe = recipes.find(r => r.code === key); + if (recipe) { + this.tryCraft(recipe); + } + }); + + this.craftingContainer.setVisible(false); + } + + tryCraft(recipe) { + if (!this.gameScene || !this.gameScene.inventorySystem) return; + + const inv = this.gameScene.inventorySystem; + + // Check cost + if (recipe.cost.wood && !inv.hasItem('wood', recipe.cost.wood)) { + console.log('Craft fail: Wood'); + return; // Add UI feedback "Need Wood" + } + if (recipe.cost.stone && !inv.hasItem('stone', recipe.cost.stone)) { + console.log('Craft fail: Stone'); + return; + } + + // Consume + if (recipe.cost.wood) inv.removeItem('wood', recipe.cost.wood); + if (recipe.cost.stone) inv.removeItem('stone', recipe.cost.stone); + + // Add Item + inv.addItem(recipe.type, 1); + console.log(`Crafted ${recipe.name}!`); + + // Flash effect + this.cameras.main.flash(200, 0, 255, 0); // Green flash + this.craftingContainer.setVisible(false); } resize(gameSize) { @@ -35,7 +147,9 @@ class UIScene extends Phaser.Scene { this.createClock(); this.createGoldDisplay(); this.createInventoryBar(); + this.createInventoryBar(); this.createDebugInfo(); + this.createSettingsButton(); // Refresh data if (this.gameScene) { @@ -539,4 +653,113 @@ class UIScene extends Phaser.Scene { this.tradeItemsContainer.add(btnBg); this.tradeItemsContainer.add(btnLabel); } + + // --- SETTINGS MENU --- + + createSettingsButton() { + if (this.settingsBtn) this.settingsBtn.destroy(); + this.settingsBtn = this.add.text(10, 10, '⚙️ SETTINGS', { + fontSize: '16px', + fill: '#ffffff', + backgroundColor: '#000000', + padding: { x: 5, y: 5 } + }); + this.settingsBtn.setInteractive({ useHandCursor: true }); + this.settingsBtn.on('pointerdown', () => this.toggleSettingsMenu()); + } + + toggleSettingsMenu() { + if (!this.settingsContainer) { + this.createSettingsMenu(); + } + this.settingsContainer.setVisible(!this.settingsContainer.visible); + if (this.settingsContainer.visible) { + this.settingsContainer.setDepth(20000); // Always on top + } + } + + createSettingsMenu() { + this.settingsContainer = this.add.container(this.width / 2, this.height / 2); + + // BG + const bg = this.add.graphics(); + bg.fillStyle(0x000000, 0.9); + bg.fillRect(-150, -120, 300, 240); + bg.lineStyle(2, 0x888888, 1); + bg.strokeRect(-150, -120, 300, 240); + this.settingsContainer.add(bg); + + // Title + const title = this.add.text(0, -90, 'SETTINGS', { fontSize: '24px', fontStyle: 'bold', fill: '#ffffff' }).setOrigin(0.5); + this.settingsContainer.add(title); + + // Close + const closeBtn = this.add.text(130, -110, 'X', { fontSize: '20px', fill: '#ff0000' }).setOrigin(0.5); + closeBtn.setInteractive({ useHandCursor: true }); + closeBtn.on('pointerdown', () => this.toggleSettingsMenu()); + this.settingsContainer.add(closeBtn); + + // Options + let y = -40; + + // 1. View Distance (Simple Toggle for now: Low/High) + this.createSettingToggle(0, y, 'VIEW DISTANCE', + () => this.gameScene.settings.viewDistance, // getter + (val) => { // setter + this.gameScene.settings.viewDistance = val; + // Apply immediately? Or next frame. + // TerrainSystem reads it in updateCulling? We need to ensure it does. + if (this.gameScene.terrainSystem) this.gameScene.terrainSystem.lastCullX = -9999; // Force update + }, + ['LOW', 'HIGH'] + ); + + y += 50; + // 2. Weather Particles + this.createSettingToggle(0, y, 'PARTICLES', + () => this.gameScene.settings.particles, + (val) => { + this.gameScene.settings.particles = val; + // Restart rain if active + if (this.gameScene.weatherSystem && (this.gameScene.weatherSystem.currentWeather === 'rain' || this.gameScene.weatherSystem.currentWeather === 'storm')) { + this.gameScene.weatherSystem.startRain(this.gameScene.weatherSystem.currentWeather === 'storm'); + } + }, + ['NONE', 'LOW', 'HIGH'] + ); + + y += 50; + // 3. Shadows + this.createSettingToggle(0, y, 'SHADOWS', + () => this.gameScene.settings.shadows ? 'ON' : 'OFF', + (val) => { + this.gameScene.settings.shadows = (val === 'ON'); + // Trigger redraw? Complex. For now just saves state. + // Maybe reload scene? + }, + ['ON', 'OFF'] + ); + } + + createSettingToggle(x, y, label, getter, setter, options) { + const labelText = this.add.text(x - 80, y, label, { fontSize: '16px', fill: '#aaaaaa' }).setOrigin(1, 0.5); + this.settingsContainer.add(labelText); + + const currentVal = getter(); + const valueText = this.add.text(x + 50, y, currentVal, { fontSize: '16px', fill: '#ffffff', fontStyle: 'bold' }).setOrigin(0.5, 0.5); + this.settingsContainer.add(valueText); + + // Click to cycle + const hitArea = this.add.rectangle(x + 50, y, 100, 30, 0xffffff, 0.1); + hitArea.setInteractive({ useHandCursor: true }); + hitArea.on('pointerdown', () => { + const cur = getter(); + let idx = options.indexOf(cur); + idx = (idx + 1) % options.length; + const next = options[idx]; + setter(next); + valueText.setText(next); + }); + this.settingsContainer.add(hitArea); + } } diff --git a/src/systems/FarmingSystem.js b/src/systems/FarmingSystem.js index 7a256d3..51710bb 100644 --- a/src/systems/FarmingSystem.js +++ b/src/systems/FarmingSystem.js @@ -18,6 +18,7 @@ class FarmingSystem { // Let's say if it has crop and it is ripe, harvest it regardless of tool. if (tile.hasCrop) { const crop = terrain.cropsMap.get(`${gridX},${gridY}`); + console.log('🌾 Check harvest:', crop); if (crop && crop.stage === 4) { this.harvest(gridX, gridY); return true; @@ -26,10 +27,11 @@ class FarmingSystem { // 2. TILLING (Requires Hoe) if (toolType === 'hoe') { - if (tile.type === 'grass' || tile.type === 'dirt') { + const typeName = tile.type.name || tile.type; + if (typeName.includes('grass') || typeName === 'dirt') { if (!tile.hasDecoration && !tile.hasCrop) { console.log('🚜 Tilling soil...'); - terrain.setTileType(gridX, gridY, 'farmland'); + terrain.setTileType(gridX, gridY, 'farmland'); // This sets it to string 'farmland' usually? or object? Assuming method handles it. // Play sound return true; } @@ -38,10 +40,16 @@ class FarmingSystem { // 3. PLANTING (Requires Seeds) if (toolType === 'seeds') { - if (tile.type === 'farmland' && !tile.hasCrop && !tile.hasDecoration) { + const typeName = tile.type.name || tile.type; + if (typeName === 'farmland' && !tile.hasCrop && !tile.hasDecoration) { console.log('🌱 Planting seeds...'); this.plant(gridX, gridY); - return true; // Consume seed logic handled by caller? + + // Remove 1 seed from inventory + if (this.scene.inventorySystem) { + this.scene.inventorySystem.removeItem('seeds', 1); + } + return true; } } diff --git a/src/systems/InteractionSystem.js b/src/systems/InteractionSystem.js index 499a4e9..f428946 100644 --- a/src/systems/InteractionSystem.js +++ b/src/systems/InteractionSystem.js @@ -3,315 +3,228 @@ class InteractionSystem { this.scene = scene; this.iso = new IsometricUtils(48, 24); - // Input listener setup (only once) + // Input listener setup this.scene.input.on('pointerdown', (pointer) => { if (pointer.button === 0) { // Left Click - this.handleLeftClick(pointer); + const worldX = pointer.worldX - this.scene.terrainOffsetX; + const worldY = pointer.worldY - this.scene.terrainOffsetY; + const gridPos = this.iso.toGrid(worldX, worldY); + this.handleInteraction(gridPos.x, gridPos.y, false); } }); + // Key E listener (Easy Interaction/Tame) + this.scene.input.keyboard.on('keydown-E', () => { + this.handleInteractKey(); + }); + // Loot Array this.drops = []; } - handleLeftClick(pointer) { - if (!this.scene.player) return; - - // 1. Account for camera and offset - const worldX = pointer.worldX - this.scene.terrainOffsetX; - const worldY = pointer.worldY - this.scene.terrainOffsetY; - - // 2. Convert to Grid - const gridPos = this.iso.toGrid(worldX, worldY); - - // 3. Check distance + handleInteractKey() { + if (!this.scene.player || !this.scene.npcs) return; const playerPos = this.scene.player.getPosition(); - const dist = Phaser.Math.Distance.Between(playerPos.x, playerPos.y, gridPos.x, gridPos.y); - // Allow interaction within radius of 2.5 tiles - if (dist > 2.5) { - console.log('Too far:', dist.toFixed(1)); - return; - } + // Find nearest NPC + let nearest = null; + let minDist = 2.5; // Interaction range - console.log(`☝️ Clicked tile: ${gridPos.x},${gridPos.y}`); - - // DETERMINE TOOL / ACTION - let activeTool = null; - const uiScene = this.scene.scene.get('UIScene'); - const invSys = this.scene.inventorySystem; - - if (uiScene && invSys) { - const selectedIdx = uiScene.selectedSlot; - const slotData = invSys.slots[selectedIdx]; - if (slotData) activeTool = slotData.type; - } - - // 0. Build Mode Override - if (this.scene.buildingSystem && this.scene.buildingSystem.isBuildMode) { - this.scene.buildingSystem.tryBuild(gridPos.x, gridPos.y); - return; // Consume click - } - - // 3.5 Check for NPC Click - if (this.scene.npcs) { - for (const npc of this.scene.npcs) { - if (Math.abs(npc.gridX - gridPos.x) < 2.5 && Math.abs(npc.gridY - gridPos.y) < 2.5) { - console.log(`🗣️ Interact with NPC: ${npc.type}`); - - if (npc.type === 'zombie') { - // Taming Logic - npc.toggleState(); - return; // Done - } - - if (npc.type === 'merchant') { - // Open Trade Menu - if (uiScene && invSys) { - uiScene.showTradeMenu(invSys); - } - return; // Stop processing - } - return; // Stop processing other clicks (farming/terrain) if clicked NPC - } + for (const npc of this.scene.npcs) { + const d = Phaser.Math.Distance.Between(playerPos.x, playerPos.y, npc.gridX, npc.gridY); + if (d < minDist) { + minDist = d; + nearest = npc; } } - // 4. Try Farming Action (Tilling, Planting, Harvesting) - if (this.scene.farmingSystem) { - const didFarm = this.scene.farmingSystem.interact(gridPos.x, gridPos.y, activeTool); - if (didFarm) { - // Animation? - return; - } - } - - // 5. Try damage decoration - const id = `${gridPos.x},${gridPos.y}`; - if (this.scene.terrainSystem.decorationsMap.has(id)) { - const decor = this.scene.terrainSystem.decorationsMap.get(id); - - // Calculate Damage based on Tool - let damage = 1; // Default hand damage - - // Tool Logic - if (decor.type === 'tree' && activeTool === 'axe') { - damage = 3; // Axe destroys tree fast - } else if ((decor.type === 'bush' || decor.type === 'stone') && activeTool === 'pickaxe') { - damage = 3; // Pickaxe destroys stone fast - } - - // Apply damage - const result = this.scene.terrainSystem.damageDecoration(gridPos.x, gridPos.y, damage); - - if (result === 'destroyed') { - // Play proper sound - if (decor.type === 'tree') { - if (this.scene.soundManager) this.scene.soundManager.playChop(); - } else { - // Play stone break sound (using chop for now or generic hit) - if (this.scene.soundManager) this.scene.soundManager.playChop(); - } - - // AUTO-LOOT directly to Inventory - let lootType = 'wood'; // Default - let lootCount = 1; - - if (decor.type === 'tree') { - lootType = 'wood'; - lootCount = 3 + Math.floor(Math.random() * 3); // 3-5 wood - } else if (decor.type === 'bush' || decor.type === 'stone') { - lootType = 'stone'; - lootCount = 2 + Math.floor(Math.random() * 3); // 2-4 stone - } else if (decor.type === 'flower') { - lootType = 'seeds'; // Flowers drop seeds? Or flower item? - lootCount = 1; - } - - console.log(`🎁 Auto-looted: ${lootCount}x ${lootType}`); - - if (invSys) { - invSys.addItem(lootType, lootCount); - - // Show floating text feedback " +3 Wood " - const screenPos = this.iso.toScreen(gridPos.x, gridPos.y); - const txt = this.scene.add.text( - screenPos.x + this.scene.terrainOffsetX, - screenPos.y + this.scene.terrainOffsetY - 30, - `+${lootCount} ${lootType.toUpperCase()}`, - { fontSize: '14px', fill: '#ffff00', stroke: '#000', strokeThickness: 2 } - ); - txt.setOrigin(0.5); - this.scene.tweens.add({ - targets: txt, - y: txt.y - 40, - alpha: 0, - duration: 1000, - onComplete: () => txt.destroy() - }); - } - - } else if (result === 'hit') { - // Play hit sound - if (this.scene.soundManager) this.scene.soundManager.playChop(); + if (nearest) { + console.log('E Interacted with:', nearest.type); + if (nearest.type === 'zombie') { + // Always Tame on E key (Combat is Space/Click) + nearest.tame(); + } else { + nearest.toggleState(); // Merchant/NPC talk } } } - handleDecorationClick(gridX, gridY) { + handleInteraction(gridX, gridY, isAttack = false) { if (!this.scene.player) return; - // Check distance + // 3. Check distance const playerPos = this.scene.player.getPosition(); const dist = Phaser.Math.Distance.Between(playerPos.x, playerPos.y, gridX, gridY); - if (dist > 3.0) { // Slightly increased radius for easier clicking - console.log('Too far:', dist.toFixed(1)); - return; - } + // Allow interaction within radius of 2.5 tiles (1.5 for attack) + const maxDist = isAttack ? 1.5 : 2.5; + if (dist > maxDist) return; - // Get Active Tool - let activeTool = null; + // DETERMINE TOOL + let activeTool = isAttack ? 'sword' : null; const uiScene = this.scene.scene.get('UIScene'); const invSys = this.scene.inventorySystem; - if (uiScene && invSys) { + if (uiScene && invSys && !isAttack) { const selectedIdx = uiScene.selectedSlot; const slotData = invSys.slots[selectedIdx]; if (slotData) activeTool = slotData.type; } - // REUSE LOGIC + if (isAttack && invSys) { + const selectedIdx = uiScene.selectedSlot; + const slotData = invSys.slots[selectedIdx]; + if (slotData) activeTool = slotData.type; + } + + // 0. Build Mode Override (Only click) + if (!isAttack && this.scene.buildingSystem && this.scene.buildingSystem.isBuildMode) { + this.scene.buildingSystem.tryBuild(gridX, gridY); + return; + } + + // 3.5 Check for NPC Interaction + if (this.scene.npcs) { + for (const npc of this.scene.npcs) { + // 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 (npc.type === 'merchant' && !isAttack) { + if (uiScene && invSys) uiScene.showTradeMenu(invSys); + return; + } + + if (npc.type === 'zombie') { + // Logic: Attack vs Tame + const isWeapon = activeTool === 'sword' || activeTool === 'axe' || activeTool === 'pickaxe'; + + if (isAttack || isWeapon) { + // COMBAT + let damage = 1; + if (activeTool === 'sword') damage = 5; + if (activeTool === 'axe') damage = 3; + if (activeTool === 'pickaxe') damage = 2; + + if (npc.takeDamage) { + npc.takeDamage(damage); + } + return; + } + else { + // TAME ATTEMPT + console.log('🤝 Attempting to TAME zombie at', npc.gridX, npc.gridY); + npc.tame(); + return; + } + } + + if (!isAttack) npc.toggleState(); + return; + } + } + } + + // 4. Try Farming Action + if (this.scene.farmingSystem && !isAttack) { + const didFarm = this.scene.farmingSystem.interact(gridX, gridY, activeTool); + if (didFarm) return; + } + + // 5. Try damage decoration const id = `${gridX},${gridY}`; if (this.scene.terrainSystem.decorationsMap.has(id)) { const decor = this.scene.terrainSystem.decorationsMap.get(id); - // Calculate Damage based on Tool - let damage = 1; // Default hand damage + let damage = 1; - // Tool Logic - if (decor.type === 'tree' && activeTool === 'axe') { - damage = 3; // Axe destroys tree fast - } else if ((decor.type === 'bush' || decor.type === 'stone') && activeTool === 'pickaxe') { - damage = 3; // Pickaxe destroys stone fast + 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; // Apply damage - const result = this.scene.terrainSystem.damageDecoration(gridX, gridY, damage); + decor.hp -= damage; + this.showFloatingText(`${-damage}`, gridX, gridY, '#ffaaaa'); - if (result === 'destroyed') { - // Play proper sound - if (decor.type === 'tree') { - if (this.scene.soundManager) this.scene.soundManager.playChop(); - } else { - if (this.scene.soundManager) this.scene.soundManager.playChop(); + if (decor.hp <= 0) { + const type = this.scene.terrainSystem.removeDecoration(gridX, gridY); + // Loot logic + 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); } - // AUTO-LOOT directly to Inventory - let lootType = 'wood'; // Default - let lootCount = 1; - - if (decor.type === 'tree') { - lootType = 'wood'; - lootCount = 3 + Math.floor(Math.random() * 3); // 3-5 wood - } else if (decor.type === 'bush' || decor.type === 'stone') { - lootType = 'stone'; - lootCount = 2 + Math.floor(Math.random() * 3); // 2-4 stone - } else if (decor.type === 'flower') { - lootType = 'seeds'; - lootCount = 1; - } - - console.log(`🎁 Auto-looted: ${lootCount}x ${lootType}`); - - if (invSys) { - invSys.addItem(lootType, lootCount); - - // Show floating text feedback - const screenPos = this.iso.toScreen(gridX, gridY); - const txt = this.scene.add.text( - screenPos.x + this.scene.terrainOffsetX, - screenPos.y + this.scene.terrainOffsetY - 30, - `+${lootCount} ${lootType.toUpperCase()}`, - { fontSize: '14px', fill: '#ffff00', stroke: '#000', strokeThickness: 2 } - ); - txt.setOrigin(0.5); + } else { + // Shake visual + const sprite = this.scene.terrainSystem.visibleDecorations.get(id); + if (sprite) { this.scene.tweens.add({ - targets: txt, - y: txt.y - 40, - alpha: 0, - duration: 1000, - onComplete: () => txt.destroy() + targets: sprite, + x: sprite.x + 2, yoyo: true, duration: 50, repeat: 2 }); + sprite.setTint(0xffaaaa); + this.scene.time.delayedCall(200, () => sprite.clearTint()); } - - } else if (result === 'hit') { - if (this.scene.soundManager) this.scene.soundManager.playChop(); } } } + showFloatingText(text, gridX, gridY, color) { + const screenPos = this.iso.toScreen(gridX, gridY); + const txt = this.scene.add.text( + screenPos.x + this.scene.terrainOffsetX, + screenPos.y + this.scene.terrainOffsetY - 30, + text, + { fontSize: '14px', fill: color, stroke: '#000', strokeThickness: 2 } + ).setOrigin(0.5); + 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}`); - // Convert to Screen const screenPos = this.iso.toScreen(gridX, gridY); const x = screenPos.x + this.scene.terrainOffsetX; const y = screenPos.y + this.scene.terrainOffsetY; - // Create simplistic item drop sprite let symbol = '?'; if (type === 'wood') symbol = '🪵'; + if (type === 'stone') symbol = '🪨'; if (type === 'seeds') symbol = '🌱'; if (type === 'wheat') symbol = '🌾'; - if (type === 'hoe') 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); // above tiles + drop.setDepth(this.iso.getDepth(gridX, gridY) + 500); - // Bounce animation this.scene.tweens.add({ - targets: drop, - y: y - 40, - duration: 500, - yoyo: true, - ease: 'Sine.easeOut', - repeat: -1 + targets: drop, y: y - 40, duration: 500, yoyo: true, ease: 'Sine.easeOut', repeat: -1 }); - this.drops.push({ - gridX, - gridY, - sprite: drop, - type: type - }); + this.drops.push({ gridX, gridY, sprite: drop, type: type }); } update() { - // Check for player pickup if (!this.scene.player) return; - const playerPos = this.scene.player.getPosition(); - - // Filter drops to pick up for (let i = this.drops.length - 1; i >= 0; i--) { const drop = this.drops[i]; - - // Check if player is ON the drop tile if (Math.abs(drop.gridX - playerPos.x) < 0.8 && Math.abs(drop.gridY - playerPos.y) < 0.8) { - // Pick up! - console.log('🎒 Picked up:', drop.type); - - // Play pickup sound - if (this.scene.soundManager) this.scene.soundManager.playPickup(); - - // Add to inventory - if (this.scene.inventorySystem) { - this.scene.inventorySystem.addItem(drop.type, 1); - } - - // Destroy visual + if (this.scene.inventorySystem) this.scene.inventorySystem.addItem(drop.type, 1); drop.sprite.destroy(); this.drops.splice(i, 1); } diff --git a/src/systems/TerrainSystem.js b/src/systems/TerrainSystem.js index 6c07fd1..c52c765 100644 --- a/src/systems/TerrainSystem.js +++ b/src/systems/TerrainSystem.js @@ -13,7 +13,6 @@ class TerrainSystem { 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 @@ -22,6 +21,13 @@ class TerrainSystem { this.offsetX = 0; this.offsetY = 0; + // Culling optimization + 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( () => { @@ -251,6 +257,8 @@ 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'); @@ -496,7 +504,7 @@ class TerrainSystem { // Force Visual Update immediately? // updateCulling will catch it on next frame, but to be safe: - // Or leave it to update loop. + this.lastCullX = -9999; // Force update return true; } @@ -524,7 +532,9 @@ class TerrainSystem { const key = `${x},${y}`; this.cropsMap.set(key, cropData); this.tiles[y][x].hasCrop = true; - // updateCulling loop will pick it up on next frame + 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) { @@ -558,14 +568,32 @@ class TerrainSystem { // 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) + const view = camera.worldView; - const buffer = 200; + // Optimization: Adjust buffer based on View Distance setting + let buffer = 200; + if (this.scene.settings && this.scene.settings.viewDistance === 'LOW') { + buffer = 50; + } const left = view.x - buffer - this.offsetX; const top = view.y - buffer - this.offsetY; const right = view.x + view.width + buffer - this.offsetX; const bottom = view.y + view.height + buffer - this.offsetY; - // Calculate visible bounding box (rough) const p1 = this.iso.toGrid(left, top); const p2 = this.iso.toGrid(right, top); const p3 = this.iso.toGrid(left, bottom); @@ -576,6 +604,12 @@ 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); @@ -587,118 +621,112 @@ class TerrainSystem { for (let y = startY; y < endY; y++) { for (let x = startX; x < endX; x++) { - const key = `${x},${y}`; - neededKeys.add(key); + if (x >= 0 && x < this.width && y >= 0 && y < this.height) { + 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]; + // Tile Logic + if (!this.visibleTiles.has(key)) { + const tilePos = this.iso.toScreen(x, y); + const tileData = this.tiles[y][x]; - const sprite = this.tilePool.get(); - sprite.setTexture(tileData.texture); + // Get from Pool + const sprite = this.tilePool.get(); + sprite.setTexture(tileData.texture); - // Elevation effect: MOČAN vertikalni offset za hribe - const elevationOffset = tileData.elevation * -25; // Povečano iz -10 na -25 - sprite.setPosition( - tilePos.x + this.offsetX, - tilePos.y + this.offsetY + elevationOffset - ); + // Elevation effect + const elevationOffset = tileData.elevation * -25; + sprite.setPosition( + tilePos.x + this.offsetX, + tilePos.y + this.offsetY + elevationOffset + ); - // DRAMATIČNO senčenje glede na višino - if (tileData.type === 'grass') { - let brightness = 1.0; - - if (tileData.elevation > 0.5) { - // Visoko = svetlo (1.0 - 1.5) - brightness = 1.0 + (tileData.elevation - 0.5) * 1.0; + // 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)) + )); } else { - // Nizko = temno (0.7 - 1.0) - brightness = 0.7 + tileData.elevation * 0.6; + sprite.clearTint(); } - 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)) - )); + // FIXED DEPTH FOR DEBUG + sprite.setDepth(-1000); + sprite.setVisible(true); + + this.visibleTiles.set(key, sprite); } - sprite.setDepth(this.iso.getDepth(x, y) - 2000); // Tiles always in background + // 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; - this.visibleTiles.set(key, sprite); - } + 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); - // Elevation effect matching tile logic - const tileData = this.tiles[y][x]; - const elevationOffset = tileData.elevation * -25; - - // 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 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); + this.visibleCrops.set(key, sprite); + } } } - } - // Decoration Logic - if (this.tiles[y][x].hasDecoration) { - neededDecorKeys.add(key); + // 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); + 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); + 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 - ); + // 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); + 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); + // Apply scale if present + if (decor.scale) sprite.setScale(decor.scale); + else sprite.setScale(1); - sprite.flipX = (x + y) % 2 === 0; + sprite.flipX = (x + y) % 2 === 0; - // INTERACTIVITY FIX: Allow clicking sprites directly - sprite.setInteractive({ pixelPerfect: true, useHandCursor: true }); + // Sprites are just visual now, interaction handled by InteractionSystem via grid + // sprite.setInteractive(...) removed to fix conflicts - // Clear old listeners - sprite.off('pointerdown'); - // Add click listener - sprite.on('pointerdown', (pointer) => { - if (this.scene.interactionSystem) { - // Manually trigger interaction logic - this.scene.interactionSystem.handleDecorationClick(x, y); - } - }); - - this.visibleDecorations.set(key, sprite); + this.visibleDecorations.set(key, sprite); + } } } } diff --git a/src/systems/WeatherSystem.js b/src/systems/WeatherSystem.js index 283e32d..0af742c 100644 --- a/src/systems/WeatherSystem.js +++ b/src/systems/WeatherSystem.js @@ -11,7 +11,8 @@ class WeatherSystem { this.currentWeather = 'clear'; this.weatherDuration = 0; this.maxWeatherDuration = 10000; // Random duration logic handles this - this.rainParticles = []; // {x, y, speed, length} + this.rainEmitter = null; // Replaced manual Array with Emitter + // --- State --- this.currentPhase = 'day'; @@ -90,20 +91,7 @@ class WeatherSystem { } updateWeatherPhysics(delta) { - if (this.currentWeather === 'rain' || this.currentWeather === 'storm') { - const width = this.scene.scale.width; - const height = this.scene.scale.height; - - for (const drop of this.rainParticles) { - drop.y += (drop.speed * delta) / 1000; - - // Wrap around - if (drop.y > height) { - drop.y = -10; - drop.x = Math.random() * width; - } - } - } + // Optimisation: Physics now handled by Phaser Particles } render() { @@ -168,14 +156,7 @@ class WeatherSystem { overlay.fillStyle(0x000033, isDark); overlay.fillRect(0, 0, width, height); - // Draw drops - overlay.lineStyle(1, 0x88aaff, 0.5); - for (const drop of this.rainParticles) { - overlay.beginPath(); - overlay.moveTo(drop.x, drop.y); - overlay.lineTo(drop.x - 2, drop.y + drop.length); - overlay.strokePath(); - } + // Rain drops are now particles (separate GameObject) } } @@ -204,22 +185,61 @@ class WeatherSystem { } startRain(heavy) { - const width = this.scene.scale.width; - const height = this.scene.scale.height; - const dropCount = heavy ? 150 : 100; + const uiScene = this.scene.scene.get('UIScene'); + if (!uiScene) return; - for (let i = 0; i < dropCount; i++) { - this.rainParticles.push({ - x: Math.random() * width, - y: Math.random() * height, - speed: heavy ? Phaser.Math.Between(400, 600) : Phaser.Math.Between(200, 400), - length: heavy ? Phaser.Math.Between(10, 15) : Phaser.Math.Between(5, 10) - }); + // Check Settings + const quality = this.scene.settings ? this.scene.settings.particles : 'HIGH'; + if (quality === 'NONE') return; + + // Ensure texture exists + if (!this.scene.textures.exists('rain_drop')) { + const graphics = this.scene.make.graphics({ x: 0, y: 0, add: false }); + graphics.fillStyle(0x88aaff, 1); + graphics.fillRect(0, 0, 2, 8); + graphics.generateTexture('rain_drop', 2, 8); + graphics.destroy(); } + + // Clean up old + if (this.rainEmitter) { + this.rainEmitter.destroy(); + } + + const width = this.scene.scale.width; + + let pQuantity = heavy ? 5 : 2; + let pFreq = heavy ? 10 : 30; + + if (quality === 'LOW') { + pQuantity = heavy ? 2 : 1; + pFreq = heavy ? 50 : 100; + } + + // Use modern particles or fallback + // Helper to support both if needed, but standard 3.60+ is: + this.rainEmitter = uiScene.add.particles(0, 0, 'rain_drop', { + x: { min: 0, max: width }, + y: -20, + quantity: pQuantity, + frequency: pFreq, // Emit every X ms + lifespan: 1500, + speedY: { min: heavy ? 600 : 400, max: heavy ? 900 : 600 }, + speedX: { min: -50, max: 0 }, // Slight wind + scaleY: { min: 1.0, max: 2.0 }, + alpha: { start: 0.6, end: 0 }, + emitting: true + }); + + // Depth just above overlay (-1000) + this.rainEmitter.setDepth(-990); } clearWeather() { - this.rainParticles = []; + if (this.rainEmitter) { + this.rainEmitter.destroy(); + this.rainEmitter = null; + } } // --- Getters for Other Systems --- diff --git a/src/utils/TextureGenerator.js b/src/utils/TextureGenerator.js index 07dc8f3..29df766 100644 --- a/src/utils/TextureGenerator.js +++ b/src/utils/TextureGenerator.js @@ -577,27 +577,6 @@ class TextureGenerator { // Left Wall (Broken) ctx.fillStyle = '#555555'; // Dark Grey ctx.beginPath(); - ctx.moveTo(32, 60); - ctx.lineTo(10, 50); - ctx.lineTo(10, 40); // Lower than house - ctx.lineTo(20, 45); // Jagged - ctx.lineTo(25, 38); - ctx.lineTo(32, 45); - ctx.fill(); - - // Right Wall (Broken) - ctx.fillStyle = '#777777'; // Light Grey - ctx.beginPath(); - ctx.moveTo(32, 60); - ctx.lineTo(54, 50); - ctx.lineTo(54, 35); - ctx.lineTo(45, 30); - ctx.lineTo(40, 35); - ctx.lineTo(32, 25); // Exposed interior? - ctx.lineTo(32, 60); - ctx.fill(); - - // Debris piles ctx.fillStyle = '#333333'; ctx.beginPath(); // Pile 1 ctx.arc(20, 55, 5, 0, Math.PI * 2); @@ -831,4 +810,117 @@ class TextureGenerator { canvas.refresh(); } } + + // Generiraj vse ikone za items + static createItemSprites(scene) { + // 1. AXE + this.createToolSprites(scene); + + // 2. PICKAXE + if (!scene.textures.exists('item_pickaxe')) { + const size = 32; + const canvas = scene.textures.createCanvas('item_pickaxe', size, size); + const ctx = canvas.getContext(); + ctx.clearRect(0, 0, size, size); + + // Handle + ctx.fillStyle = '#8B4513'; + ctx.fillRect(14, 12, 4, 18); + + // Head (Pick) + ctx.fillStyle = '#C0C0C0'; // Silver + ctx.beginPath(); + ctx.moveTo(16, 12); + ctx.quadraticCurveTo(28, 8, 30, 16); // Right curve + ctx.lineTo(26, 18); // Sharp point right + ctx.lineTo(16, 14); // Center + ctx.lineTo(6, 18); // Sharp point left + ctx.lineTo(2, 16); // Left curve tip + ctx.quadraticCurveTo(4, 8, 16, 12); + ctx.fill(); + + canvas.refresh(); + } + + // 3. HOE + if (!scene.textures.exists('item_hoe')) { + const size = 32; + const canvas = scene.textures.createCanvas('item_hoe', size, size); + const ctx = canvas.getContext(); + ctx.clearRect(0, 0, size, size); + + // Handle + ctx.fillStyle = '#8B4513'; + ctx.fillRect(14, 4, 4, 26); + + // Head + ctx.fillStyle = '#C0C0C0'; + ctx.fillRect(6, 4, 12, 4); // Top bar + ctx.fillRect(6, 4, 4, 8); // Down blade + + canvas.refresh(); + } + + // 4. STONE + if (!scene.textures.exists('item_stone')) { + const size = 32; + const canvas = scene.textures.createCanvas('item_stone', size, size); + const ctx = canvas.getContext(); + ctx.clearRect(0, 0, size, size); + + ctx.fillStyle = '#808080'; // Grey + ctx.beginPath(); + ctx.arc(16, 16, 10, 0, Math.PI * 2); + ctx.fill(); + + // Shading + ctx.fillStyle = '#A9A9A9'; + ctx.beginPath(); + ctx.arc(12, 12, 4, 0, Math.PI * 2); + ctx.fill(); + + // Crack + ctx.strokeStyle = '#555555'; + ctx.beginPath(); + ctx.moveTo(16, 16); + ctx.lineTo(20, 20); + ctx.stroke(); + + canvas.refresh(); + } + + // 5. WOOD + if (!scene.textures.exists('item_wood')) { + const size = 32; + const canvas = scene.textures.createCanvas('item_wood', size, size); + const ctx = canvas.getContext(); + ctx.clearRect(0, 0, size, size); + + // Log + ctx.fillStyle = '#8B4513'; + ctx.fillRect(8, 12, 16, 8); + ctx.fillStyle = '#A0522D'; // Lighter finish + ctx.fillRect(24, 12, 4, 8); // End cap + ctx.fillStyle = '#D2691E'; // Bark details + ctx.fillRect(10, 14, 8, 2); + + canvas.refresh(); + } + + // 6. SEEDS + if (!scene.textures.exists('item_seeds')) { + const size = 32; + const canvas = scene.textures.createCanvas('item_seeds', size, size); + const ctx = canvas.getContext(); + ctx.clearRect(0, 0, size, size); + + ctx.fillStyle = '#DEB887'; // Burlywood + // 3 seeds + ctx.beginPath(); ctx.arc(12, 16, 3, 0, Math.PI * 2); ctx.fill(); + ctx.beginPath(); ctx.arc(20, 14, 3, 0, Math.PI * 2); ctx.fill(); + ctx.beginPath(); ctx.arc(16, 20, 3, 0, Math.PI * 2); ctx.fill(); + + canvas.refresh(); + } + } }