// Player Entity // Igralec z WASD kontrolami in isometrično podporo class Player { constructor(scene, gridX = 50, gridY = 50, offsetX = 0, offsetY = 0) { this.scene = scene; this.gridX = gridX; this.gridY = gridY; // Terrain offset this.offsetX = offsetX; this.offsetY = offsetY; this.iso = new IsometricUtils(48, 24); // 🎮 SMOOTH MOVEMENT SYSTEM (Hybrid) this.moveSpeed = 100; // Normal speed px/s this.sprintSpeed = 200; // Sprint speed px/s this.acceleration = 0.15; // How fast to reach target speed // Velocity (current movement) this.velocity = { x: 0, y: 0 }; // Target velocity (where we want to go) this.targetVelocity = { x: 0, y: 0 }; // Sprint system this.sprinting = false; this.energy = 100; this.maxEnergy = 100; this.energyDrain = 10; // per second while sprinting // State this.isMoving = false; this.direction = 'down'; this.lastDir = { x: 0, y: 1 }; // Default south // Stats this.hp = 100; this.maxHp = 100; this.isDead = false; // Kreira sprite this.createSprite(); // Setup kontrole this.setupControls(); // Space za napad this.scene.input.keyboard.on('keydown-SPACE', () => { if (!this.isDead) this.attack(); }); // Začetna pozicija this.updatePosition(); } takeDamage(amount) { if (this.isDead) return; // GOD MODE - Invincibility if (window.godMode) { console.log('⚡ GOD MODE: Damage blocked!'); return; } this.hp -= amount; console.log(`Player HP: ${this.hp}`); // Visual Feedback this.scene.cameras.main.shake(100, 0.01); this.sprite.setTint(0xff0000); this.scene.time.delayedCall(200, () => { if (!this.isDead) this.sprite.clearTint(); }); // Update UI (če obstaja StatsSystem ali UI) if (this.scene.statsSystem) { // Scene specific stats update if linked } if (this.hp <= 0) { this.die(); } } die() { this.isDead = true; this.sprite.setTint(0x555555); this.sprite.setRotation(Math.PI / 2); // Lie down console.log('💀 PLAYER DIED'); // Death Sound if (this.scene.soundManager) { this.scene.soundManager.playDeath(); } // Show Game Over / Reload const txt = this.scene.add.text(this.scene.cameras.main.midPoint.x, this.scene.cameras.main.midPoint.y, 'YOU DIED', { fontSize: '64px', color: '#ff0000', fontStyle: 'bold', stroke: '#000', strokeThickness: 6 }).setOrigin(0.5).setDepth(20000); this.scene.time.delayedCall(3000, () => { window.location.reload(); }); } createSprite() { // KRVAVA ŽETEV: Use new protagonist sprite let texKey = 'player_protagonist'; // Fallback to generated sprite if not loaded if (!this.scene.textures.exists(texKey)) { console.warn('⚠️ player_protagonist sprite not found! Generating fallback...'); TextureGenerator.createPlayerSprite(this.scene, 'player_fallback'); texKey = 'player_fallback'; } // Kreira sprite const screenPos = this.iso.toScreen(this.gridX, this.gridY); this.sprite = this.scene.add.sprite( screenPos.x + this.offsetX, screenPos.y + this.offsetY, texKey, 0 // CRITICAL: Start with frame 0 (first frame only) ); this.sprite.setOrigin(0.5, 1.0); // Bottom center - new sprite has full character! // Scale based on sprite type if (texKey === 'player_protagonist') { this.sprite.setScale(0.5); // 256x256 frames scaled to 128x128 display size } else { this.sprite.setScale(0.5); // Fallback sprite } // --- HAND / HELD ITEM SPRITE --- this.handSprite = this.scene.add.sprite( screenPos.x + this.offsetX + 10, screenPos.y + this.offsetY - 25, 'item_axe' ); this.handSprite.setOrigin(0.5, 0.5); this.handSprite.setScale(0.25); this.handSprite.setVisible(false); this.updateDepth(); } setupControls() { this.keys = this.scene.input.keyboard.addKeys({ // WASD w: Phaser.Input.Keyboard.KeyCodes.W, a: Phaser.Input.Keyboard.KeyCodes.A, s: Phaser.Input.Keyboard.KeyCodes.S, d: Phaser.Input.Keyboard.KeyCodes.D, // Arrow Keys up: Phaser.Input.Keyboard.KeyCodes.UP, down: Phaser.Input.Keyboard.KeyCodes.DOWN, left: Phaser.Input.Keyboard.KeyCodes.LEFT, right: Phaser.Input.Keyboard.KeyCodes.RIGHT, // Actions space: Phaser.Input.Keyboard.KeyCodes.SPACE, shift: Phaser.Input.Keyboard.KeyCodes.SHIFT }); // Gamepad Events this.scene.input.gamepad.on('connected', (pad) => { this.setupGamepadEvents(pad); }); if (this.scene.input.gamepad.total > 0) { this.setupGamepadEvents(this.scene.input.gamepad.getPad(0)); } } setupGamepadEvents(pad) { if (!pad) return; console.log('🎮 Gamepad Connected:', pad.id); pad.on('down', (index) => { if (this.isDead) return; // A Button (0) - Attack if (index === 0) { this.attack(); } // X Button (2) - Interact (Pick/Tame) if (index === 2) { const targetX = this.gridX + this.lastDir.x; const targetY = this.gridY + this.lastDir.y; if (this.scene.interactionSystem) { this.scene.interactionSystem.handleInteraction(targetX, targetY); } } // Y Button (3) - Crafting if (index === 3) { // Zahteva, da UIScene posluša ta event, ali pa direktni klic const ui = this.scene.scene.get('UIScene'); if (ui) ui.toggleCrafting(); } // B Button (1) - Inventory if (index === 1) { const ui = this.scene.scene.get('UIScene'); if (ui) ui.toggleInventory(); } }); } attack() { console.log('⚔️ Player Attack!'); // Attack Sound if (this.scene.soundManager) { this.scene.soundManager.playAttack(); } 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) { // Convert delta to seconds const dt = delta / 1000; // 🎮 HANDLE INPUT (always check for input) this.handleInput(dt); // 🏃 APPLY SMOOTH MOVEMENT // Smoothly interpolate current velocity toward target velocity this.velocity.x = Phaser.Math.Linear( this.velocity.x, this.targetVelocity.x, this.acceleration ); this.velocity.y = Phaser.Math.Linear( this.velocity.y, this.targetVelocity.y, this.acceleration ); // Apply velocity to sprite position this.sprite.x += this.velocity.x * dt; this.sprite.y += this.velocity.y * dt; // Update grid position from pixel position const screenPos = { x: this.sprite.x - this.offsetX, y: this.sprite.y - this.offsetY }; const gridPos = this.iso.toGrid(screenPos.x, screenPos.y); this.gridX = Math.floor(gridPos.x); this.gridY = Math.floor(gridPos.y); // Check if moving const speed = Math.sqrt(this.velocity.x ** 2 + this.velocity.y ** 2); this.isMoving = speed > 5; // 🎨 UPDATE ANIMATION this.updateAnimation(); // 💧 ENERGY REGENERATION if (!this.sprinting && this.energy < this.maxEnergy) { this.energy = Math.min(this.maxEnergy, this.energy + 20 * dt); } // ⚡ ENERGY DRAIN WHILE SPRINTING if (this.sprinting && this.isMoving && this.energy > 0) { this.energy -= this.energyDrain * dt; if (this.energy <= 0) { this.energy = 0; this.sprinting = false; // Can't sprint without energy } } // 🔧 UPDATE HELD ITEM this.updateHeldItem(); // 🌱 SPACE KEY - Farming Action if (this.keys.space && Phaser.Input.Keyboard.JustDown(this.keys.space)) { this.handleFarmingAction(); } } updateHeldItem() { const uiScene = this.scene.scene.get('UIScene'); const invSys = this.scene.inventorySystem; if (uiScene && invSys) { const selectedIdx = uiScene.selectedSlot; const slot = invSys.slots[selectedIdx]; if (slot && (slot.type === 'axe' || slot.type === 'pickaxe' || slot.type === 'hoe' || slot.type === 'sword')) { const texKey = `item_${slot.type}`; if (this.scene.textures.exists(texKey)) { this.handSprite.setTexture(texKey); this.handSprite.setVisible(true); } else { this.handSprite.setVisible(false); } } else { this.handSprite.setVisible(false); } } } handleInput(dt) { // 🎮 COLLECT INPUT FROM ALL SOURCES let inputX = 0; let inputY = 0; // Keyboard (WASD + Arrows) if (this.keys.up.isDown || this.keys.w.isDown) inputY -= 1; if (this.keys.down.isDown || this.keys.s.isDown) inputY += 1; if (this.keys.left.isDown || this.keys.a.isDown) inputX -= 1; if (this.keys.right.isDown || this.keys.d.isDown) inputX += 1; // Virtual Joystick (Mobile) const ui = this.scene.scene.get('UIScene'); if (ui && ui.virtualJoystick) { if (ui.virtualJoystick.up) inputY -= 1; if (ui.virtualJoystick.down) inputY += 1; if (ui.virtualJoystick.left) inputX -= 1; if (ui.virtualJoystick.right) inputX += 1; } // Gamepad (Xbox Controller) if (this.scene.input.gamepad && this.scene.input.gamepad.total > 0) { const pad = this.scene.input.gamepad.getPad(0); if (pad) { const threshold = 0.2; const stickY = pad.leftStick.y; const stickX = pad.leftStick.x; // Analog stick (smooth values) if (Math.abs(stickY) > threshold) inputY += stickY; if (Math.abs(stickX) > threshold) inputX += stickX; // D-Pad (digital) if (pad.up) inputY -= 1; if (pad.down) inputY += 1; if (pad.left) inputX -= 1; if (pad.right) inputX += 1; // Sprint button (B on Xbox, Circle on PS) if (pad.B || pad.buttons[1]?.pressed) { this.sprinting = true; } } } // 🏃 SPRINT DETECTION (Shift key) this.sprinting = this.keys.shift?.isDown || this.sprinting; // Normalize diagonal movement (so diagonal isn't faster) const inputLength = Math.sqrt(inputX ** 2 + inputY ** 2); if (inputLength > 0) { inputX /= inputLength; inputY /= inputLength; } // 🎯 DETERMINE MOVEMENT SPEED let maxSpeed = this.moveSpeed; if (this.sprinting && this.energy > 0) { maxSpeed = this.sprintSpeed; } // 🚀 SET TARGET VELOCITY this.targetVelocity.x = inputX * maxSpeed; this.targetVelocity.y = inputY * maxSpeed; // 🧭 UPDATE DIRECTION & FACING if (inputLength > 0.1) { // Update last direction for attacks/interactions this.lastDir = { x: inputX, y: inputY }; // Determine animation direction (4-way) // Isometric mapping: up/down = X axis, left/right = Y axis let animDir = 'down'; // Prioritize primary direction (stronger input) if (Math.abs(inputX) > Math.abs(inputY)) { // Vertical movement (up/down in isometric) animDir = inputX < 0 ? 'up' : 'down'; } else { // Horizontal movement (left/right in isometric) animDir = inputY < 0 ? 'right' : 'left'; } this.direction = animDir; } } // 🎨 UPDATE ANIMATION (called from update loop) updateAnimation() { if (!this.sprite.anims) return; const speed = Math.sqrt(this.velocity.x ** 2 + this.velocity.y ** 2); try { if (speed < 5) { // 😴 IDLE const idleAnim = `protagonist_idle_${this.direction}`; if (this.scene.anims.exists(idleAnim) && !this.sprite.anims.isPlaying) { this.sprite.play(idleAnim); } } else { // 🚶 WALKING / 🏃 SPRINTING const walkAnim = `protagonist_walk_${this.direction}`; if (this.scene.anims.exists(walkAnim)) { if (this.sprite.anims.currentAnim?.key !== walkAnim) { this.sprite.play(walkAnim, true); } // Faster animation when sprinting const frameRate = this.sprinting ? 12 : 8; if (this.sprite.anims.currentAnim) { this.sprite.anims.currentAnim.frameRate = frameRate; } } } // Hand sprite position update const handOffsets = { 'left': -10, 'right': 10, 'up': 0, 'down': 0 }; if (this.handSprite) { this.handSprite.setX(this.sprite.x + (handOffsets[this.direction] || 0)); } } catch (e) { // Ignore animation errors } } moveToGrid(targetX, targetY) { this.isMoving = true; this.gridX = targetX; this.gridY = targetY; // Footstep Sound if (this.scene.soundManager) { this.scene.soundManager.playFootstep(); } const targetScreen = this.iso.toScreen(targetX, targetY); // Play walk animation - SAFE CHECK (updated for protagonist) const texKey = this.sprite.texture.key; if (texKey === 'player_protagonist') { // KRVAVA ŽETEV: Use directional animations const walkAnim = `protagonist_walk_${this.direction}`; if (this.scene.anims.exists(walkAnim)) { this.sprite.play(walkAnim, true); } } else if (texKey === 'player_dreadlocks') { if (this.scene.anims.exists('player_dreadlocks_walk')) { this.sprite.play('player_dreadlocks_walk', true); } } else if (texKey === 'player_walk') { if (this.scene.anims.exists('player_walk_anim')) { 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(); // Stop animation (updated for protagonist) if (texKey === 'player_protagonist') { // Play idle animation const idleAnim = `protagonist_idle_${this.direction}`; if (this.scene.anims.exists(idleAnim)) { this.sprite.play(idleAnim); } else { this.sprite.stop(); } } else if (texKey === 'player_dreadlocks') { this.sprite.stop(); this.sprite.setFrame(0); } else if (texKey === 'player_walk') { this.sprite.stop(); this.sprite.setFrame(0); } } }); // NOTE: updateDepth() disabled - using sortableObjects Z-sorting in GameScene // this.updateDepth(); } updatePosition() { // 🎨 FLAT 2D POSITIONING (NEW!) const tileSize = 48; // Direct grid to pixel conversion (NO isometric!) const x = Math.round((this.gridX * tileSize) + (tileSize / 2)); const y = Math.round((this.gridY * tileSize) + (tileSize / 2)); this.sprite.setPosition(x, y); const facingRight = !this.sprite.flipX; const handOffset = facingRight ? 10 : -10; this.handSprite.setPosition( Math.round(x + handOffset), Math.round(y - 15) ); this.updateDepth(); } updateDepth() { if (!this.sprite) return; // Optimization: Create dirty check if (this.lastDepthY === undefined || Math.abs(this.sprite.y - this.lastDepthY) > 0.1) { // Uporabi LAYER_OBJECTS da se pravilno sortira z drevesi/kamni const layerBase = this.iso.LAYER_OBJECTS || 200000; const depth = layerBase + this.sprite.y; this.sprite.setDepth(depth); if (this.handSprite) this.handSprite.setDepth(depth + 1); this.lastDepthY = this.sprite.y; } } getPosition() { return { x: this.gridX, y: this.gridY }; } getScreenPosition() { return { x: this.sprite.x, y: this.sprite.y }; } destroy() { if (this.sprite) this.sprite.destroy(); } dieAnimation() { this.sprite.setTint(0xff0000); this.scene.tweens.add({ targets: this.sprite, angle: 90, duration: 500, ease: 'Bounce.easeOut' }); } respawn() { this.sprite.clearTint(); this.sprite.angle = 0; } handleFarmingAction() { if (!this.scene.farmingSystem) return; const uiScene = this.scene.scene.get('UIScene'); const invSys = this.scene.inventorySystem; if (!uiScene || !invSys) return; const selectedIdx = uiScene.selectedSlot; const slot = invSys.slots[selectedIdx]; const itemType = slot ? slot.type : null; // Get tile player is standing on const gridX = this.gridX; const gridY = this.gridY; // HOE - Till soil if (itemType === 'hoe') { const success = this.scene.farmingSystem.tillSoil(gridX, gridY); if (success) { console.log('✅ Tilled soil!'); // Particle effect - soil spray this.createSoilParticles(gridX, gridY); // Tool swing animation this.swingTool(); // TODO: Play dig sound } return; } // SEEDS - Plant if (itemType === 'carrot' || itemType === 'wheat' || itemType === 'item_seeds') { const cropType = itemType === 'item_seeds' ? 'carrot' : itemType; const success = this.scene.farmingSystem.plantSeed(gridX, gridY, cropType); if (success) { invSys.removeItem(itemType, 1); console.log('🌱 Planted seed!'); // Particle effect - seed drop this.createSeedParticles(gridX, gridY); // TODO: Play plant sound } return; } // EMPTY HAND - Harvest if (!itemType) { const success = this.scene.farmingSystem.harvestCrop(gridX, gridY); if (success) { console.log('🌾 Harvested crop!'); // Particle effect - harvest sparkle this.createHarvestParticles(gridX, gridY); // Camera shake this.scene.cameras.main.shake(200, 0.003); // TODO: Play harvest sound } return; } } swingTool() { if (!this.handSprite || !this.handSprite.visible) return; // Save original position const originalAngle = this.handSprite.angle; const originalScale = this.handSprite.scaleX; // Swing animation this.scene.tweens.add({ targets: this.handSprite, angle: originalAngle - 45, scaleX: originalScale * 1.3, scaleY: originalScale * 1.3, duration: 100, yoyo: true, ease: 'Cubic.easeOut', onComplete: () => { this.handSprite.angle = originalAngle; this.handSprite.scaleX = originalScale; this.handSprite.scaleY = originalScale; } }); } createSoilParticles(gridX, gridY) { const screenPos = this.scene.iso.gridToScreen(gridX, gridY); const x = screenPos.x + this.offsetX; const y = screenPos.y + this.offsetY; // Brown soil particles for (let i = 0; i < 10; i++) { const particle = this.scene.add.circle(x, y, 3, 0x8B4513); this.scene.tweens.add({ targets: particle, x: x + (Math.random() - 0.5) * 30, y: y - Math.random() * 20, alpha: 0, duration: 400, onComplete: () => particle.destroy() }); } } createSeedParticles(gridX, gridY) { const screenPos = this.scene.iso.gridToScreen(gridX, gridY); const x = screenPos.x + this.offsetX; const y = screenPos.y + this.offsetY; // Green seed particles for (let i = 0; i < 5; i++) { const particle = this.scene.add.circle(x, y - 20, 2, 0x00ff00); this.scene.tweens.add({ targets: particle, y: y, alpha: 0, duration: 500, ease: 'Cubic.easeIn', onComplete: () => particle.destroy() }); } } createHarvestParticles(gridX, gridY) { const screenPos = this.scene.iso.gridToScreen(gridX, gridY); const x = screenPos.x + this.offsetX; const y = screenPos.y + this.offsetY; // Golden sparkle particles for (let i = 0; i < 15; i++) { const particle = this.scene.add.circle(x, y, 4, 0xFFD700); this.scene.tweens.add({ targets: particle, x: x + (Math.random() - 0.5) * 40, y: y - Math.random() * 40, scaleX: 0, scaleY: 0, alpha: 0, duration: 600, ease: 'Cubic.easeOut', onComplete: () => particle.destroy() }); } } /** * Check if player has required tool equipped/in inventory * @param {string} toolType - Tool type (axe, pickaxe, hoe, etc.) * @returns {boolean} - True if player has the tool */ hasToolEquipped(toolType) { if (!toolType) return true; // No tool required // Check inventory for tool if (this.scene.inventorySystem) { return this.scene.inventorySystem.hasItem(toolType, 1); } return false; } }