- 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
197 lines
6.5 KiB
JavaScript
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');
|
|
}
|
|
}
|
|
}
|