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
This commit is contained in:
20
src/engine/game/Entity.js
Normal file
20
src/engine/game/Entity.js
Normal file
@@ -0,0 +1,20 @@
|
||||
|
||||
export class Entity {
|
||||
constructor(id, name, type, x, y, texturePath) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.type = type; // 'hero', 'monster'
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.texturePath = texturePath;
|
||||
this.stats = {
|
||||
move: 4,
|
||||
wounds: 10
|
||||
};
|
||||
}
|
||||
|
||||
setPosition(x, y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
15
src/engine/game/GameConstants.js
Normal file
15
src/engine/game/GameConstants.js
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
export const GAME_PHASES = {
|
||||
SETUP: 'setup', // Game hasn't started or is generating initial room
|
||||
POWER: 'power', // Start of turn: Power roll + Events
|
||||
HERO: 'hero', // Heroes move and perform actions
|
||||
EXPLORATION: 'exploration', // Revealing new rooms (triggered by heroes at edge)
|
||||
MONSTER: 'monster', // Monsters move and attack
|
||||
END_TURN: 'end_turn' // Cleanup
|
||||
};
|
||||
|
||||
// Events that can be triggered
|
||||
export const GAME_EVENTS = {
|
||||
PHASE_CHANGED: 'phase_changed',
|
||||
TURN_STARTED: 'turn_started'
|
||||
};
|
||||
196
src/engine/game/GameEngine.js
Normal file
196
src/engine/game/GameEngine.js
Normal file
@@ -0,0 +1,196 @@
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
65
src/engine/game/TurnManager.js
Normal file
65
src/engine/game/TurnManager.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import { GAME_PHASES, GAME_EVENTS } from './GameConstants.js';
|
||||
|
||||
export class TurnManager {
|
||||
constructor() {
|
||||
this.currentTurn = 0;
|
||||
this.currentPhase = GAME_PHASES.SETUP;
|
||||
this.listeners = {}; // Simple event system
|
||||
}
|
||||
|
||||
startGame() {
|
||||
this.currentTurn = 1;
|
||||
this.setPhase(GAME_PHASES.HERO); // Jump straight to Hero phase for now
|
||||
console.log(`--- TURN ${this.currentTurn} START ---`);
|
||||
}
|
||||
|
||||
nextPhase() {
|
||||
// Simple sequential flow for now
|
||||
switch (this.currentPhase) {
|
||||
case GAME_PHASES.POWER:
|
||||
this.setPhase(GAME_PHASES.HERO);
|
||||
break;
|
||||
case GAME_PHASES.HERO:
|
||||
// Usually goes to Exploration if at edge, or Monster if not.
|
||||
// For this dev stage, let's allow manual triggering of Exploration
|
||||
// via UI, so we stay in HERO until confirmed done.
|
||||
this.setPhase(GAME_PHASES.MONSTER);
|
||||
break;
|
||||
case GAME_PHASES.MONSTER:
|
||||
this.endTurn();
|
||||
break;
|
||||
// Exploration is usually triggered as an interrupt, not strictly sequential
|
||||
}
|
||||
}
|
||||
|
||||
setPhase(phase) {
|
||||
if (this.currentPhase !== phase) {
|
||||
console.log(`Phase Switch: ${this.currentPhase} -> ${phase}`);
|
||||
this.currentPhase = phase;
|
||||
this.emit(GAME_EVENTS.PHASE_CHANGED, phase);
|
||||
}
|
||||
}
|
||||
|
||||
triggerExploration() {
|
||||
this.setPhase(GAME_PHASES.EXPLORATION);
|
||||
// Logic to return to HERO phase would handle elsewhere
|
||||
}
|
||||
|
||||
endTurn() {
|
||||
console.log(`--- TURN ${this.currentTurn} END ---`);
|
||||
this.currentTurn++;
|
||||
this.setPhase(GAME_PHASES.POWER);
|
||||
}
|
||||
|
||||
// -- Simple Observer Pattern --
|
||||
on(event, callback) {
|
||||
if (!this.listeners[event]) this.listeners[event] = [];
|
||||
this.listeners[event].push(callback);
|
||||
}
|
||||
|
||||
emit(event, data) {
|
||||
if (this.listeners[event]) {
|
||||
this.listeners[event].forEach(cb => cb(data));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user