// NPC Entity // NPC z random walk AI in isometrično podporo class NPC { constructor(scene, gridX, gridY, offsetX = 0, offsetY = 0, type = 'zombie') { this.scene = scene; this.gridX = gridX; this.gridY = gridY; this.type = type; console.log(`🎭 NPC Constructor called - Type: ${type} at (${gridX}, ${gridY})`); // Terrain offset this.offsetX = offsetX; this.offsetY = offsetY; this.iso = new IsometricUtils(48, 24); // Random walk paramters 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; this.attackCooldownTimer = 0; // Elite Zombie Stats if (type === 'elite_zombie') { this.hp = 50; // 2.5x več HP this.maxHp = 50; this.moveSpeed = 150; // 50% hitrejši this.gridMoveTime = 200; // Hitrejši premiki } else { this.hp = 20; this.maxHp = 20; } // Kreira sprite this.createSprite(); // Začetna pozicija this.updatePosition(); // Naključna začetna pavza this.pauseTime = Math.random() * this.maxPauseTime; // Register in SpatialGrid if (this.scene.spatialGrid) { this.scene.spatialGrid.add(this); } } // Methods moved/consolidated below createSprite() { let texKey = `npc_${this.type}`; let isAnimated = false; // Check for animated sprites first if (this.type === 'zombie' && this.scene.textures.exists('zombie_walk')) { texKey = 'zombie_walk'; isAnimated = true; } else if (this.type === 'zombie' && this.scene.textures.exists('zombie_sprite')) { texKey = 'zombie_sprite'; } else if (this.type === 'merchant') { // Uporabi generirano sliko, če obstaja if (this.scene.textures.exists('merchant_new')) { texKey = 'merchant_new'; } else { texKey = 'merchant_texture'; // Fallback na proceduralno } console.log('🏪 Creating MERCHANT NPC with texture:', texKey); } else if (this.type === 'elite_zombie') { // Elite Zombie sprite if (this.scene.textures.exists('elite_zombie')) { texKey = 'elite_zombie'; } else { texKey = 'npc_elite_zombie'; // Fallback na proceduralno } console.log('👹 Creating ELITE ZOMBIE with texture:', texKey); } else if (!this.scene.textures.exists(texKey)) { TextureGenerator.createNPCSprite(this.scene, texKey, this.type); } // 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 ); this.sprite.setOrigin(0.5, 1); if (isAnimated) { this.sprite.setScale(1.5); } else { // Scale po tipu let scale = 0.5; // Default if (this.type === 'merchant') scale = 0.2; else if (this.type === 'elite_zombie') scale = 0.2; // Elite manjši this.sprite.setScale(scale); } // 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(); } moveToGrid(targetX, targetY) { // Determine facing direction before moving const dx = targetX - this.gridX; const dy = targetY - this.gridY; const movingRight = (dx > 0) || (dy > 0); this.sprite.setFlipX(!movingRight); this.isMoving = true; this.gridX = targetX; this.gridY = targetY; const targetScreen = this.iso.toScreen(targetX, targetY); // Animation if (this.sprite.texture.key === 'zombie_walk') { this.sprite.play('zombie_walk_anim', true); } // Tween za smooth gibanje this.scene.tweens.add({ targets: this.sprite, x: targetScreen.x + this.offsetX, y: targetScreen.y + this.offsetY, duration: this.gridMoveTime, ease: 'Linear', onComplete: () => { this.isMoving = false; this.updatePosition(); // Stop Animation if (this.sprite.texture.key === 'zombie_walk') { this.sprite.stop(); this.sprite.setFrame(0); } } }); // Posodobi depth this.updateDepth(); } update(delta) { // 1. Viewport Culling - če sprite ni viden na kameri, preskoči risanje const camera = this.scene.cameras.main; if (camera && this.sprite) { const worldView = camera.worldView; const isVisible = Phaser.Geom.Rectangle.Overlaps(worldView, this.sprite.getBounds()); if (!isVisible && 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; // Preskoči AI če ni viden } else if (isVisible && !this.sprite.visible) { this.sprite.setVisible(true); if (this.eyesGroup) this.eyesGroup.setVisible(true); } } // 2. 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 (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 } } } // 3. Optimization: Update depth ONLY if moving if (this.isMoving) { this.updateDepth(); this.updatePosition(); if (this.scene.spatialGrid) this.scene.spatialGrid.updateEntity(this); 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') { // Defensive behavior - attack nearby enemy zombies this.defendPlayer(); this.followPlayer(); return; } this.pauseTime += delta; if (this.pauseTime >= this.maxPauseTime) { this.performRandomWalk(); this.pauseTime = 0; } } defendPlayer() { if (!this.scene.npcs || this.attackCooldownTimer > 0) return; // Find nearest enemy zombie let nearestEnemy = null; let minDist = 6; // Defense radius for (const npc of this.scene.npcs) { if (npc === this || npc.state === 'TAMED' || npc.state === 'FOLLOW') continue; if (npc.type !== 'zombie') continue; const dist = Phaser.Math.Distance.Between(this.gridX, this.gridY, npc.gridX, npc.gridY); if (dist < minDist) { minDist = dist; nearestEnemy = npc; } } if (nearestEnemy) { // Attack if close enough if (minDist <= 1.5) { this.attackEnemy(nearestEnemy); } else { // Move towards enemy this.moveTowards(nearestEnemy.gridX, nearestEnemy.gridY); } } } attackEnemy(target) { if (this.attackCooldownTimer > 0) return; this.attackCooldownTimer = 1500; console.log('🧟❤️ Tamed Zombie DEFENDS!'); // Attack Animation if (this.sprite) { this.scene.tweens.add({ targets: this.sprite, y: this.sprite.y - 10, yoyo: true, duration: 100, repeat: 1 }); } // Deal Damage if (target && target.takeDamage) { target.takeDamage(15); // Tamed zombies hit harder! } } 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); if (this.attackCooldownTimer > 0) { this.attackCooldownTimer -= delta; } 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); } } } moveTowards(targetX, targetY) { // Optimization: if very close, use direct movement (collision checks handled by isValidMove) const dist = Phaser.Math.Distance.Between(this.gridX, this.gridY, targetX, targetY); // Use pathfinding for longer distances if (dist > 2 && this.scene.pathfinding) { // Check if we need to recalc path (target changed or no path) const targetKey = `${targetX},${targetY}`; if (!this.currentPath || this.pathTargetKey !== targetKey || this.currentPath.length === 0) { // Recalc path (Async Worker) if (!this.isWaitingForPath) { this.isWaitingForPath = true; this.scene.pathfinding.findPath(this.gridX, this.gridY, targetX, targetY, (path) => { this.isWaitingForPath = false; this.currentPath = path; this.pathTargetKey = targetKey; // Process path once received if (this.currentPath && this.currentPath.length > 0) { // Remove start node if it's current pos if (this.currentPath[0].x === this.gridX && this.currentPath[0].y === this.gridY) { this.currentPath.shift(); } } }); } else { return; // Wait for path } } // Follow Path if (this.currentPath && this.currentPath.length > 0) { const step = this.currentPath[0]; // Move there this.moveToGrid(step.x, step.y); // Remove step this.currentPath.shift(); return; } } // Fallback: Direct Movement (Dumb AI) 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; // Attack Animation (Jump) if (this.sprite) { this.scene.tweens.add({ targets: this.sprite, y: this.sprite.y - 10, yoyo: true, duration: 100, repeat: 1 }); } // Apply Damage to Player if (this.scene.player && this.scene.player.takeDamage) { console.log('🧟 Zombie ATTACKS Player!'); this.scene.player.takeDamage(10); } else if (this.scene.statsSystem) { this.scene.statsSystem.takeDamage(10); } } performRandomWalk() { 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); 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; } // EYES if (this.eyesGroup) { this.eyesGroup.setPosition(this.sprite.x, this.sprite.y); } this.updateDepth(); } updateDepth() { if (!this.sprite) return; if (this.lastDepthY === undefined || Math.abs(this.sprite.y - this.lastDepthY) > 0.1) { const layerBase = this.iso.LAYER_OBJECTS || 200000; const depth = layerBase + this.sprite.y; this.sprite.setDepth(depth); 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); } } } takeDamage(amount) { this.hp -= amount; // Hit Sound if (this.scene.soundManager) { this.scene.soundManager.playHit(); } // Blood Splash Effect if (this.scene.particleEffects) { this.scene.particleEffects.bloodSplash(this.sprite.x, this.sprite.y - 20); } // 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); } // WHITE FLASH Effect if (this.sprite) { this.sprite.setTint(0xFFFFFF); // White flash this.scene.time.delayedCall(50, () => { if (this.sprite) { this.sprite.setTint(0xFF0000); // Red flash this.scene.time.delayedCall(50, () => { if (this.sprite) { this.sprite.clearTint(); if (this.state === 'TAMED' || this.state === 'FOLLOW' || this.state === 'STAY') { this.sprite.setTint(0xAAFFAA); } } }); } }); } // KNOCKBACK Effect if (this.sprite && this.scene.player) { const knockbackDist = 10; const angle = Phaser.Math.Angle.Between( this.scene.player.sprite.x, this.scene.player.sprite.y, this.sprite.x, this.sprite.y ); const knockX = Math.cos(angle) * knockbackDist; const knockY = Math.sin(angle) * knockbackDist; this.scene.tweens.add({ targets: this.sprite, x: this.sprite.x + knockX, y: this.sprite.y + knockY, duration: 100, yoyo: true, ease: 'Quad.easeOut' }); } // FLOATING DAMAGE NUMBERS if (this.sprite) { const dmgTxt = this.scene.add.text( this.sprite.x, this.sprite.y - 40, `-${amount}`, { fontSize: '16px', fontFamily: 'Courier New', color: '#FF0000', stroke: '#000', strokeThickness: 3, fontStyle: 'bold' } ); dmgTxt.setOrigin(0.5); dmgTxt.setDepth(999999); this.scene.tweens.add({ targets: dmgTxt, y: dmgTxt.y - 30, alpha: 0, duration: 800, ease: 'Cubic.easeOut', onComplete: () => dmgTxt.destroy() }); } console.log(`Zombie HP: ${this.hp}`); if (this.hp <= 0) { this.die(); } } die() { console.log('🧟💀 Zombie DEAD'); // Death Sound if (this.scene.soundManager) { this.scene.soundManager.playDeath(); } // Spawn loot - Elite zombies drop better items! if (this.scene.lootSystem) { const lootItem = (this.type === 'elite_zombie') ? 'item_scrap' : 'item_bone'; this.scene.lootSystem.spawnLoot(this.gridX, this.gridY, lootItem); // Elite bonus: 50% chance for extra chip if (this.type === 'elite_zombie' && Math.random() < 0.5) { this.scene.lootSystem.spawnLoot(this.gridX, this.gridY, 'item_chip'); } } else if (this.scene.interactionSystem && this.scene.interactionSystem.spawnLoot) { // Fallback const lootItem = (this.type === 'elite_zombie') ? 'item_scrap' : 'item_bone'; this.scene.interactionSystem.spawnLoot(this.gridX, this.gridY, lootItem); } // Quest Tracking (Kill) if (this.scene.questSystem) { this.scene.questSystem.trackAction(this.type); } this.destroy(); const idx = this.scene.npcs.indexOf(this); if (idx > -1) this.scene.npcs.splice(idx, 1); } tame() { if (this.state === 'TAMED' || this.type !== 'zombie') return; this.state = 'TAMED'; console.log('🧟❤️ Zombie TAMED!'); this.isTamed = true; // Mark as tamed // Register to ZombieWorkerSystem (assign FARM work by default) if (this.scene.zombieWorkerSystem) { this.scene.zombieWorkerSystem.assignWork(this, 'FARM', 5); } // Visual Feedback const headX = this.sprite.x; const headY = this.sprite.y - 50; const heart = this.scene.add.text(headX, headY, '❤️', { fontSize: '24px' }); heart.setOrigin(0.5); heart.setDepth(this.sprite.depth + 200); this.scene.tweens.add({ targets: heart, y: headY - 40, alpha: 0, duration: 1500, onComplete: () => heart.destroy() }); // 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); } this.addTamedEyes(); } interact() { console.log('🗣️ Inteacting with NPC:', this.type); // ZOMBIE LOGIC if (this.type === 'zombie') { if (this.state !== 'TAMED') { // Try tame (For now instant) this.tame(); } else { // Command: Toggle FOLLOW / STAY if (this.state === 'FOLLOW') { this.state = 'STAY'; this.showEmote('🛑'); // Stop if (this.workerStats) this.workerStats.task = 'IDLE'; } else { this.state = 'FOLLOW'; this.showEmote('👣'); // Footprints if (this.workerStats) this.workerStats.task = 'FOLLOW'; } } return; } // NPC QUEST LOGIC if (this.scene.questSystem) { const availableQuest = this.scene.questSystem.getAvailableQuest(this.type); if (availableQuest) { const ui = this.scene.scene.get('UIScene'); if (ui && ui.showQuestDialog) { ui.showQuestDialog(availableQuest, () => { this.scene.questSystem.startQuest(availableQuest.id); }); return; } } } // Default behavior (Emote) this.showEmote('👋'); if (this.sprite) { this.scene.tweens.add({ targets: this.sprite, y: this.sprite.y - 10, yoyo: true, duration: 100 }); } } addTamedEyes() { if (this.eyesGroup) return; this.eyesGroup = this.scene.add.container(this.sprite.x, this.sprite.y); this.eyesGroup.setDepth(this.sprite.depth + 1); const eyeL = this.scene.add.rectangle(-4, -54, 4, 4, 0x00FFFF); // Cyan eyes const eyeR = this.scene.add.rectangle(4, -54, 4, 4, 0x00FFFF); this.eyesGroup.add([eyeL, eyeR]); } destroy() { if (this.scene.spatialGrid) { this.scene.spatialGrid.remove(this); } if (this.sprite) this.sprite.destroy(); if (this.healthBar) this.healthBar.destroy(); if (this.healthBarBg) this.healthBarBg.destroy(); if (this.eyesGroup) this.eyesGroup.destroy(); } }