Implement advanced pathfinding and combat visual effects

- Add monster turn visual feedback (green ring on attacker, red ring on victim)
- Implement proper attack sequence with timing and animations
- Add room boundary and height level pathfinding system
- Monsters now respect room walls and can only pass through doors
- Add height level support (1-8) with stairs (9) for level transitions
- Fix attack validation to prevent attacks through walls
- Speed up hero movement animation (300ms per tile)
- Fix exploration phase message to not show on initial tile placement
- Disable hero movement during exploration phase (doors only)
This commit is contained in:
2026-01-06 16:18:46 +01:00
parent dd7356f1bd
commit 3efbf8d5fb
5 changed files with 279 additions and 31 deletions

View File

@@ -25,6 +25,8 @@ export class GameEngine {
this.onEntityUpdate = null;
this.onEntityMove = null;
this.onEntitySelect = null;
this.onEntityActive = null; // New: When entity starts/ends turn
this.onEntityHit = null; // New: When entity takes damage
this.onPathChange = null;
}
@@ -225,9 +227,10 @@ export class GameEngine {
if (!this.selectedEntity) return;
// Valid Phase Check
// Allow movement in Hero Phase AND Exploration Phase (for positioning ease)
// Allow movement ONLY in Hero Phase.
// Exploration Phase is for opening doors only (no movement).
const phase = this.turnManager.currentPhase;
if (phase !== 'hero' && phase !== 'exploration' && this.selectedEntity.type === 'hero') {
if (phase !== 'hero' && this.selectedEntity.type === 'hero') {
return;
}

View File

@@ -34,12 +34,18 @@ export class MonsterAI {
processMonster(monster) {
return new Promise(resolve => {
// NO green ring here - only during attack
// Calculate delay based on potential move distance to ensure animation finishes
const moveTime = (monster.stats.move * 300) + 500; // +buffer for attack logic
// SLOWER: 600ms per tile + Extra buffer for potential attack sequence
const moveTime = (monster.stats.move * 600) + 3000; // 3s buffer for attack sequence
setTimeout(() => {
this.actMonster(monster);
setTimeout(resolve, moveTime);
setTimeout(() => {
resolve();
}, moveTime);
}, 100);
});
}
@@ -88,11 +94,15 @@ export class MonsterAI {
console.log(`[MonsterAI] ${monster.id} moved to ${monster.x},${monster.y}`);
// 7. Check if NOW adjacent after move -> ATTACK
const postMoveHero = this.getAdjacentHero(monster);
if (postMoveHero) {
console.log(`[MonsterAI] ${monster.id} engaged ${postMoveHero.name} after move. Attacking!`);
this.performAttack(monster, postMoveHero);
}
// Wait for movement animation to complete before checking
const movementDuration = actualPath.length * 600;
setTimeout(() => {
const postMoveHero = this.getAdjacentHero(monster);
if (postMoveHero) {
console.log(`[MonsterAI] ${monster.id} engaged ${postMoveHero.name} after move. Attacking!`);
this.performAttack(monster, postMoveHero);
}
}, movementDuration);
}
getClosestHero(monster) {
@@ -119,15 +129,28 @@ export class MonsterAI {
});
}
isOccupied(x, y) {
// Check Grid
if (!this.game.dungeon.grid.isOccupied(x, y)) return true; // Wall/Void
isOccupied(x, y, fromX, fromY) {
// Check if target cell exists in grid
if (!this.game.dungeon.grid.isOccupied(x, y)) {
return true; // Wall/Void
}
// Check Heroes
if (this.game.heroes.some(h => h.x === x && h.y === y)) return true;
if (this.game.heroes.some(h => h.x === x && h.y === y)) {
return true;
}
// Check Monsters
if (this.game.monsters.some(m => m.x === x && m.y === y)) return true;
if (this.game.monsters.some(m => m.x === x && m.y === y)) {
return true;
}
// NEW: Check if movement is valid (room boundaries, height levels, stairs)
if (fromX !== undefined && fromY !== undefined) {
if (!this.game.dungeon.grid.canMoveBetween(fromX, fromY, x, y)) {
return true; // Movement blocked by room boundary or height restriction
}
}
return false;
}
@@ -168,13 +191,9 @@ export class MonsterAI {
];
for (const n of neighbors) {
// Determine blocking
// Note: We normally block if occupied.
// But for Fallback to work in a crowd, we might want to know if we can at least get closer.
// However, we literally cannot walk on occupied tiles.
// So the fallback is simply: "To the tile closest to the hero that ISN'T occupied."
if (this.isOccupied(n.x, n.y)) continue;
// Check if movement from current to neighbor is valid
// This now includes room boundary, height, and stair checks
if (this.isOccupied(n.x, n.y, current.x, current.y)) continue;
const key = `${n.x},${n.y}`;
if (!visited.has(key)) {
@@ -194,13 +213,41 @@ export class MonsterAI {
}
performAttack(monster, hero) {
const result = CombatMechanics.resolveMeleeAttack(monster, hero);
console.log(`[COMBAT] ${result.message}`);
// SEQUENCE:
// 1. Show green ring on monster
// 2. Monster attack animation (we'll simulate with delay)
// 3. Show red ring + shake on hero
// 4. Remove both rings
// 5. Show combat result
// Notify UI/GameEngine about damage (if we had a hook)
if (this.game.onCombatResult) {
this.game.onCombatResult(result);
const result = CombatMechanics.resolveMeleeAttack(monster, hero);
// Step 1: Green ring on attacker
if (this.game.onEntityActive) {
this.game.onEntityActive(monster.id, true);
}
// Step 2: Attack animation delay (500ms)
setTimeout(() => {
// Step 3: Trigger hit visual on defender (if hit succeeded)
if (result.hitSuccess && this.game.onEntityHit) {
this.game.onEntityHit(hero.id);
}
// Step 4: Remove green ring after red ring appears (1200ms for red ring duration)
setTimeout(() => {
if (this.game.onEntityActive) {
this.game.onEntityActive(monster.id, false);
}
// Step 5: Show combat result after both rings are gone
setTimeout(() => {
if (this.game.onCombatResult) {
this.game.onCombatResult(result);
}
}, 200); // Small delay after rings disappear
}, 1200); // Wait for red ring to disappear
}, 500); // Attack animation delay
}
getAdjacentHero(entity) {
@@ -215,8 +262,17 @@ export class MonsterAI {
const dx = Math.abs(entity.x - hero.x);
const dy = Math.abs(entity.y - hero.y);
// Orthogonal adjacency only (Manhattan dist 1)
return (dx + dy) === 1;
// Must be orthogonally adjacent (Manhattan dist 1)
if ((dx + dy) !== 1) return false;
// NEW: Check if movement between monster and hero is valid
// This prevents attacking through walls/room boundaries
if (!this.game.dungeon.grid.canMoveBetween(entity.x, entity.y, hero.x, hero.y)) {
return false; // Wall or room boundary blocks attack
}
return true;
});
}
}