777 lines
26 KiB
JavaScript
777 lines
26 KiB
JavaScript
// 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);
|
|
}
|
|
}
|
|
|
|
tame() {
|
|
if (this.state === 'TAMED' || this.type !== 'zombie') return;
|
|
|
|
this.state = 'TAMED';
|
|
console.log('🧟❤️ Zombie TAMED!');
|
|
|
|
// Visual Feedback
|
|
const headX = this.sprite.x;
|
|
const headY = this.sprite.y - 40;
|
|
|
|
const heart = this.scene.add.text(headX, headY, '❤️', { fontSize: '20px' });
|
|
heart.setOrigin(0.5);
|
|
heart.setDepth(this.sprite.depth + 100);
|
|
|
|
this.scene.tweens.add({
|
|
targets: heart,
|
|
y: headY - 30,
|
|
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);
|
|
}
|
|
|
|
// 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 {
|
|
// 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() });
|
|
}
|
|
}
|
|
|
|
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!');
|
|
|
|
// Visual Feedback
|
|
const headX = this.sprite.x;
|
|
const headY = this.sprite.y - 40;
|
|
|
|
const heart = this.scene.add.text(headX, headY, '❤️', { fontSize: '20px' });
|
|
heart.setOrigin(0.5);
|
|
heart.setDepth(this.sprite.depth + 100);
|
|
|
|
this.scene.tweens.add({
|
|
targets: heart,
|
|
y: headY - 30,
|
|
alpha: 0,
|
|
duration: 1500,
|
|
onComplete: () => heart.destroy()
|
|
});
|
|
|
|
// Change color slightly to indicate friend
|
|
this.sprite.setTint(0xAAFFAA);
|
|
|
|
// Hide Health Bar if tamed (optional)
|
|
if (this.healthBarBg) {
|
|
this.healthBarBg.setVisible(false);
|
|
this.healthBar.setVisible(false);
|
|
}
|
|
|
|
this.addTamedEyes();
|
|
}
|
|
|
|
interact() {
|
|
console.log('🗣️ Inteacting with NPC:', this.type);
|
|
|
|
// Quest Check
|
|
if (this.scene.questSystem) {
|
|
const availableQuest = this.scene.questSystem.getAvailableQuest(this.type);
|
|
if (availableQuest) {
|
|
console.log('Quest Available from NPC!');
|
|
// Open Dialog UI
|
|
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('👋');
|
|
// Small jump or animation?
|
|
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();
|
|
}
|
|
}
|