udomacenje zombija in uboj\

This commit is contained in:
2025-12-07 12:47:47 +01:00
parent 8e401a9d6f
commit 2404d44ef7
10 changed files with 1086 additions and 532 deletions

View File

@@ -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();
}
}

View File

@@ -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() {