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; } }