Files
WarhammerQuest/src/engine/game/GameEngine.js
marti 9234a2e3a0 feat: Implement door interaction system and UI improvements
- Add interactive door system with click detection on door meshes
- Create custom DoorModal component replacing browser confirm()
- Implement door opening with texture change to door1_open.png
- Add additive door rendering to preserve opened doors
- Remove exploration button and requestExploration method
- Implement camera orbit controls with smooth animations
- Add active view indicator (yellow highlight) on camera buttons
- Add vertical zoom slider with label
- Fix camera to maintain isometric perspective while rotating
- Integrate all systems into main game loop
2026-01-01 17:16:58 +01:00

197 lines
6.5 KiB
JavaScript

import { DungeonGenerator } from '../dungeon/DungeonGenerator.js';
import { TurnManager } from './TurnManager.js';
import { GAME_PHASES } from './GameConstants.js';
import { Entity } from './Entity.js';
export class GameEngine {
constructor() {
this.dungeon = new DungeonGenerator();
this.turnManager = new TurnManager();
this.missionConfig = null;
this.player = null;
this.selectedPath = [];
this.selectedEntityId = null;
// Simple event callbacks (external systems can assign these)
this.onPathChange = null; // (path) => {}
this.onEntityUpdate = null; // (entity) => {}
this.onEntityMove = null; // (entity, path) => {}
this.onEntitySelect = null; // (entityId, isSelected) => {}
this.setupEventHooks();
}
setupEventHooks() {
this.turnManager.on('phase_changed', (phase) => this.handlePhaseChange(phase));
}
startMission(missionConfig) {
this.missionConfig = missionConfig;
console.log(`[GameEngine] Starting mission: ${missionConfig.name}`);
// 1. Initialize Dungeon (Places the first room)
this.dungeon.startDungeon(missionConfig);
// 2. Initialize Player
// Find a valid starting spot (first occupied cell)
const startingSpot = this.dungeon.grid.occupiedCells.keys().next().value;
const [startX, startY] = startingSpot ? startingSpot.split(',').map(Number) : [0, 0];
console.log(`[GameEngine] Spawning player at valid spot: ${startX}, ${startY}`);
this.player = new Entity('p1', 'Barbaro', 'hero', startX, startY, '/assets/images/dungeon1/standees/barbaro.png');
if (this.onEntityUpdate) this.onEntityUpdate(this.player);
// 3. Start the Turn Sequence
this.turnManager.startGame();
}
update(deltaTime) {
// Continuous logic
}
handlePhaseChange(phase) {
console.log(`[GameEngine] Phase Changed to: ${phase}`);
}
// --- Interaction ---
onCellClick(x, y) {
if (this.turnManager.currentPhase !== GAME_PHASES.HERO) return;
console.log(`Cell Click: ${x},${y}`);
// 1. Selector Check (Click on Player?)
if (this.player.x === x && this.player.y === y) {
if (this.selectedEntityId === this.player.id) {
// Deselect
this.selectedEntityId = null;
console.log("Player Deselected");
if (this.onEntitySelect) this.onEntitySelect(this.player.id, false);
// Clear path too
this.selectedPath = [];
this._notifyPath();
} else {
// Select
this.selectedEntityId = this.player.id;
console.log("Player Selected");
if (this.onEntitySelect) this.onEntitySelect(this.player.id, true);
}
return;
}
// If nothing selected, ignore floor clicks
if (!this.selectedEntityId) return;
// 2. Check if valid Floor (Occupied Cell)
if (!this.dungeon.grid.occupiedCells.has(`${x},${y}`)) {
console.log("Invalid cell: Void");
return;
}
// 3. Logic: Path Building (Only if Selected)
// A. If clicking on last selected -> Deselect (Remove last step)
if (this.selectedPath.length > 0) {
const last = this.selectedPath[this.selectedPath.length - 1];
if (last.x === x && last.y === y) {
this.selectedPath.pop();
this._notifyPath();
return;
}
}
// B. Determine Previous Point (Player or Last Path Node)
let prevX = this.player.x;
let prevY = this.player.y;
if (this.selectedPath.length > 0) {
const last = this.selectedPath[this.selectedPath.length - 1];
prevX = last.x;
prevY = last.y;
}
// Note: Manhattan distance 1 = Adjacency (No diagonals)
const dist = Math.abs(x - prevX) + Math.abs(y - prevY);
if (dist === 1) {
// Check Path Length Limit (Speed)
if (this.selectedPath.length < this.player.stats.move) {
this.selectedPath.push({ x, y });
this._notifyPath();
} else {
console.log("Max movement reached");
}
} else {
// Restart path if clicking adjacent to player
const distFromPlayer = Math.abs(x - this.player.x) + Math.abs(y - this.player.y);
if (distFromPlayer === 1) {
this.selectedPath = [{ x, y }];
this._notifyPath();
}
}
}
onCellRightClick(x, y) {
if (this.turnManager.currentPhase !== GAME_PHASES.HERO) return;
if (!this.selectedEntityId) return; // Must satisfy selection rule
// Must be clicking the last tile of the path to confirm
if (this.selectedPath.length === 0) return;
const last = this.selectedPath[this.selectedPath.length - 1];
if (last.x === x && last.y === y) {
console.log("Confirming Move...");
this.confirmMove();
}
}
confirmMove() {
if (this.selectedPath.length === 0) return;
const target = this.selectedPath[this.selectedPath.length - 1];
const pathCopy = [...this.selectedPath];
// 1. Trigger Animation Sequence
if (this.onEntityMove) this.onEntityMove(this.player, pathCopy);
// 2. Update Logical Position
this.player.setPosition(target.x, target.y);
// 3. Cleanup
this.selectedPath = [];
this._notifyPath();
// Note: Exploration is now manual via door interaction
}
_notifyPath() {
if (this.onPathChange) this.onPathChange(this.selectedPath);
}
exploreExit(exitCell) {
console.log('[GameEngine] Exploring exit:', exitCell);
// Find this exit in pendingExits
const exit = this.dungeon.pendingExits.find(ex => ex.x === exitCell.x && ex.y === exitCell.y);
if (exit) {
// Prioritize this exit
const idx = this.dungeon.pendingExits.indexOf(exit);
if (idx > -1) {
this.dungeon.pendingExits.splice(idx, 1);
this.dungeon.pendingExits.unshift(exit);
}
// Trigger exploration
this.turnManager.triggerExploration();
this.dungeon.step();
this.turnManager.setPhase(GAME_PHASES.HERO);
} else {
console.warn('[GameEngine] Exit not found in pendingExits');
}
}
}