Files
WarhammerQuest/src/engine/game/GameEngine.js

437 lines
14 KiB
JavaScript

import { DungeonGenerator } from '../dungeon/DungeonGenerator.js';
import { TurnManager } from './TurnManager.js';
import { MonsterAI } from './MonsterAI.js';
import { HERO_DEFINITIONS } from '../data/Heroes.js';
import { MONSTER_DEFINITIONS } from '../data/Monsters.js';
import { createEventDeck, EVENT_TYPES } from '../data/Events.js';
/**
* GameEngine for Manual Dungeon Construction with Player Movement
*/
export class GameEngine {
constructor() {
this.dungeon = new DungeonGenerator();
this.turnManager = new TurnManager();
this.ai = new MonsterAI(this); // Init AI
this.player = null;
this.selectedEntity = null;
this.isRunning = false;
this.plannedPath = []; // Array of {x,y}
this.eventDeck = createEventDeck();
// Callbacks
this.onEntityUpdate = null;
this.onEntityMove = null;
this.onEntitySelect = null;
this.onPathChange = null;
}
startMission(missionConfig) {
this.dungeon.startDungeon(missionConfig);
// Create Party (4 Heroes)
this.createParty();
this.isRunning = true;
this.turnManager.startGame();
// Listen for Phase Changes to Reset Moves
this.turnManager.on('phase_changed', (phase) => {
if (phase === 'hero') {
this.resetHeroMoves();
}
});
}
resetHeroMoves() {
if (!this.heroes) return;
this.heroes.forEach(hero => {
hero.currentMoves = hero.stats.move;
hero.hasAttacked = false;
});
console.log("Refilled Hero Moves");
}
createParty() {
this.heroes = [];
this.monsters = []; // Initialize monsters array
// Definition Keys
const heroKeys = ['barbarian', 'dwarf', 'elf', 'wizard'];
// Find valid spawn points dynamically
const startPositions = this.findSpawnPoints(4);
if (startPositions.length < 4) {
console.error("Could not find enough spawn points!");
// Fallback
startPositions.push({ x: 0, y: 0 }, { x: 1, y: 0 }, { x: 0, y: 1 }, { x: 1, y: 1 });
}
heroKeys.forEach((key, index) => {
const definition = HERO_DEFINITIONS[key];
const pos = startPositions[index];
const hero = {
id: `hero_${key}`,
type: 'hero',
key: key,
name: definition.name,
x: pos.x,
y: pos.y,
texturePath: definition.portrait,
stats: { ...definition.stats },
// Game State
currentMoves: definition.stats.move,
hasAttacked: false,
isConscious: true,
hasLantern: key === 'barbarian' // Default leader
};
this.heroes.push(hero);
if (this.onEntityUpdate) {
this.onEntityUpdate(hero);
}
});
// Set First Player as Active
this.activeHeroIndex = 0;
// Legacy support for single player var (getter proxy)
this.player = this.heroes[0];
}
spawnMonster(monsterKey, x, y) {
const definition = MONSTER_DEFINITIONS[monsterKey];
if (!definition) {
console.error(`Monster definition not found: ${monsterKey}`);
return;
}
// Ensure unique ID even in tight loops
if (!this._monsterIdCounter) this._monsterIdCounter = 0;
this._monsterIdCounter++;
const id = `monster_${monsterKey}_${Date.now()}_${this._monsterIdCounter}`;
console.log(`[GameEngine] Creating monster ${id} at ${x},${y}`);
const monster = {
id: id,
type: 'monster',
key: monsterKey,
name: definition.name,
x: x,
y: y,
texturePath: definition.portrait,
stats: { ...definition.stats },
// Game State
currentWounds: definition.stats.wounds,
isDead: false
};
this.monsters.push(monster);
if (this.onEntityUpdate) {
this.onEntityUpdate(monster);
}
return monster;
}
onCellClick(x, y) {
// 1. Check for Hero/Monster Selection
const clickedHero = this.heroes.find(h => h.x === x && h.y === y);
const clickedMonster = this.monsters ? this.monsters.find(m => m.x === x && m.y === y) : null;
const clickedEntity = clickedHero || clickedMonster;
if (clickedEntity) {
if (this.selectedEntity === clickedEntity) {
// Toggle Deselect
this.deselectEntity();
} else {
// Select new entity
if (this.selectedEntity) this.deselectEntity();
this.selectedEntity = clickedEntity;
if (this.onEntitySelect) {
this.onEntitySelect(clickedEntity.id, true);
}
}
return;
}
// 2. PLAN MOVEMENT (If entity selected and we clicked empty space)
if (this.selectedEntity) {
this.planStep(x, y);
}
}
deselectEntity() {
if (!this.selectedEntity) return;
const id = this.selectedEntity.id;
this.selectedEntity = null;
this.plannedPath = [];
if (this.onEntitySelect) this.onEntitySelect(id, false);
if (this.onPathChange) this.onPathChange([]);
}
// Alias for legacy calls if any
deselectPlayer() {
this.deselectEntity();
}
planStep(x, y) {
if (!this.selectedEntity) return;
// Determine start point
const lastStep = this.plannedPath.length > 0
? this.plannedPath[this.plannedPath.length - 1]
: { x: this.selectedEntity.x, y: this.selectedEntity.y };
// Check Adjacency
const dx = Math.abs(x - lastStep.x);
const dy = Math.abs(y - lastStep.y);
const isAdjacent = (dx + dy) === 1;
// Check Walkability
const isWalkable = this.canMoveTo(x, y);
// Check against Max Move Stats
const maxMove = this.selectedEntity.currentMoves || 0;
// Also account for the potential next step
if (this.plannedPath.length >= maxMove && !(this.plannedPath.length > 0 && x === lastStep.x && y === lastStep.y)) {
// Allow undo (next block), but block new steps
if (isAdjacent && isWalkable) {
// Prevent adding more steps
return;
}
}
// Undo Logic
if (this.plannedPath.length > 0 && x === lastStep.x && y === lastStep.y) {
this.plannedPath.pop();
this.onPathChange && this.onPathChange(this.plannedPath);
return;
}
if (isAdjacent && isWalkable) {
const alreadyInPath = this.plannedPath.some(p => p.x === x && p.y === y);
const isEntityPos = this.selectedEntity.x === x && this.selectedEntity.y === y;
// Also check if occupied by OTHER heroes?
const isOccupiedByHero = this.heroes.some(h => h.x === x && h.y === y && h !== this.selectedEntity);
if (!alreadyInPath && !isEntityPos && !isOccupiedByHero) {
this.plannedPath.push({ x, y });
this.onPathChange && this.onPathChange(this.plannedPath);
}
}
}
executeMovePath() {
if (!this.selectedEntity || !this.plannedPath.length) return;
const path = [...this.plannedPath];
const entity = this.selectedEntity;
// Update verify immediately
const finalDest = path[path.length - 1];
entity.x = finalDest.x;
entity.y = finalDest.y;
// Visual animation
if (this.onEntityMove) {
this.onEntityMove(entity, path);
}
// Deduct Moves
if (entity.currentMoves !== undefined) {
entity.currentMoves -= path.length;
if (entity.currentMoves < 0) entity.currentMoves = 0;
}
this.deselectEntity();
}
canMoveTo(x, y) {
// Check if cell is walkable (occupied by a tile)
return this.dungeon.grid.isOccupied(x, y);
}
// Deprecated direct move
movePlayer(x, y) {
this.player.x = x;
this.player.y = y;
if (this.onEntityMove) this.onEntityMove(this.player, [{ x, y }]);
}
// Check if the Leader (Lamp Bearer) is adjacent to the door
isLeaderAdjacentToDoor(doorCells) {
if (!this.heroes || this.heroes.length === 0) return false;
const leader = this.getLeader();
if (!leader) return false;
const cells = Array.isArray(doorCells) ? doorCells : [doorCells];
for (const cell of cells) {
const dx = Math.abs(leader.x - cell.x);
const dy = Math.abs(leader.y - cell.y);
// Orthogonal adjacency check (Manhattan distance === 1)
if ((dx === 1 && dy === 0) || (dx === 0 && dy === 1)) {
return true;
}
}
return false;
}
getLeader() {
// Find hero with lantern, default to barbarian if something breaks, or first hero
return this.heroes.find(h => h.hasLantern) || this.heroes.find(h => h.key === 'barbarian') || this.heroes[0];
}
// Deprecated generic adjacency (kept for safety or other interactions)
isPlayerAdjacentToDoor(doorCells) {
return this.isLeaderAdjacentToDoor(doorCells);
}
update(time) {
// Minimal update loop
}
findSpawnPoints(count) {
const points = [];
const queue = [{ x: 1, y: 1 }]; // Start search near origin but ensure not 0,0 which might be tricky if it's door
// Actually, just scan the grid or BFS from center of first tile?
// First tile is placed at 0,0. Let's scan from 0,0.
const startNode = { x: 0, y: 0 };
const searchQueue = [startNode];
const visited = new Set(['0,0']);
let loops = 0;
while (searchQueue.length > 0 && points.length < count && loops < 200) {
const current = searchQueue.shift();
if (this.dungeon.grid.isOccupied(current.x, current.y)) {
points.push(current);
}
// Neighbors
const neighbors = [
{ x: current.x + 1, y: current.y },
{ x: current.x - 1, y: current.y },
{ x: current.x, y: current.y + 1 },
{ x: current.x, y: current.y - 1 }
];
for (const n of neighbors) {
const key = `${n.x},${n.y}`;
if (!visited.has(key)) {
visited.add(key);
searchQueue.push(n);
}
}
loops++;
}
return points;
}
onRoomRevealed(cells) {
console.log("[GameEngine] Room Revealed!");
// 1. Draw Event Card
if (this.eventDeck.length === 0) {
console.warn("Event deck empty, reshaping...");
this.eventDeck = createEventDeck();
}
const card = this.eventDeck.pop();
console.log(`[GameEngine] Event Drawn: ${card.name}`);
if (card.type === EVENT_TYPES.MONSTER) {
// 2. Determine Count
let count = 0;
if (typeof card.resolve === 'function') {
count = card.resolve(this, { cells });
} else {
count = 1; // Fallback
}
console.log(`[GameEngine] Spawning ${count} ${card.monsterKey}s`);
// 3. Find valid spawn spots
const availableCells = cells.filter(cell => {
const isHero = this.heroes.some(h => h.x === cell.x && h.y === cell.y);
const isMonster = this.monsters.some(m => m.x === cell.x && m.y === cell.y);
return !isHero && !isMonster;
});
console.log(`[GameEngine] Available Spawn Cells: ${availableCells.length}`, availableCells);
// Shuffle
for (let i = availableCells.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[availableCells[i], availableCells[j]] = [availableCells[j], availableCells[i]];
}
// 4. Spawn
let spawnedCount = 0;
for (let i = 0; i < count; i++) {
if (i < availableCells.length) {
const pos = availableCells[i];
console.log(`[GameEngine] Spawning at ${pos.x},${pos.y}`);
this.spawnMonster(card.monsterKey, pos.x, pos.y);
spawnedCount++;
} else {
console.warn("[GameEngine] Not enough space!");
break;
}
}
return {
type: 'MONSTER_SPAWN',
monsterKey: card.monsterKey,
count: spawnedCount,
cardName: card.name
};
}
return null;
}
// =========================================
// MONSTER AI & TURN LOGIC
// =========================================
playMonsterTurn() {
if (this.ai) {
this.ai.executeTurn();
}
}
// AI Helper methods moved to MonsterAI.js
isLeaderAdjacentToDoor(doorCells) {
// ... (Keep this one as it's used by main.js logic for doors)
if (!this.heroes || this.heroes.length === 0) return false;
const leader = this.getLeader();
if (!leader) return false;
const cells = Array.isArray(doorCells) ? doorCells : [doorCells];
for (const cell of cells) {
const dx = Math.abs(leader.x - cell.x);
const dy = Math.abs(leader.y - cell.y);
// Orthogonal adjacency check (Manhattan distance === 1)
if ((dx === 1 && dy === 0) || (dx === 0 && dy === 1)) {
return true;
}
}
return false;
}
}