Implement Lantern Bearer logic, Phase buttons, and Monster spawning basics
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
import { DungeonGenerator } from '../dungeon/DungeonGenerator.js';
|
||||
import { TurnManager } from './TurnManager.js';
|
||||
import { HERO_DEFINITIONS } from '../data/Heroes.js';
|
||||
import { MONSTER_DEFINITIONS } from '../data/Monsters.js';
|
||||
|
||||
/**
|
||||
* GameEngine for Manual Dungeon Construction with Player Movement
|
||||
@@ -6,6 +9,7 @@ import { DungeonGenerator } from '../dungeon/DungeonGenerator.js';
|
||||
export class GameEngine {
|
||||
constructor() {
|
||||
this.dungeon = new DungeonGenerator();
|
||||
this.turnManager = new TurnManager();
|
||||
this.player = null;
|
||||
this.selectedEntity = null;
|
||||
this.isRunning = false;
|
||||
@@ -22,60 +26,160 @@ export class GameEngine {
|
||||
|
||||
this.dungeon.startDungeon(missionConfig);
|
||||
|
||||
// Create player at center of first tile
|
||||
this.createPlayer(1.5, 2.5); // Center of 2x6 corridor
|
||||
// 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createPlayer(x, y) {
|
||||
this.player = {
|
||||
id: 'p1',
|
||||
name: 'Barbarian',
|
||||
x: Math.floor(x),
|
||||
y: Math.floor(y),
|
||||
texturePath: '/assets/images/dungeon1/standees/barbaro.png'
|
||||
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;
|
||||
}
|
||||
|
||||
const id = `monster_${monsterKey}_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`;
|
||||
|
||||
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(this.player);
|
||||
this.onEntityUpdate(monster);
|
||||
}
|
||||
|
||||
return monster;
|
||||
}
|
||||
|
||||
onCellClick(x, y) {
|
||||
// 1. SELECT / DESELECT PLAYER
|
||||
if (this.player && x === this.player.x && y === this.player.y) {
|
||||
if (this.selectedEntity === this.player) {
|
||||
// 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.deselectPlayer();
|
||||
this.deselectEntity();
|
||||
} else {
|
||||
// Select
|
||||
this.selectedEntity = this.player;
|
||||
// Select new entity
|
||||
if (this.selectedEntity) this.deselectEntity();
|
||||
|
||||
this.selectedEntity = clickedEntity;
|
||||
if (this.onEntitySelect) {
|
||||
this.onEntitySelect(this.player.id, true);
|
||||
this.onEntitySelect(clickedEntity.id, true);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. PLAN MOVEMENT (If player selected)
|
||||
if (this.selectedEntity === this.player) {
|
||||
// 2. PLAN MOVEMENT (If entity selected and we clicked empty space)
|
||||
if (this.selectedEntity) {
|
||||
this.planStep(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
deselectPlayer() {
|
||||
deselectEntity() {
|
||||
if (!this.selectedEntity) return;
|
||||
const id = this.selectedEntity.id;
|
||||
this.selectedEntity = null;
|
||||
this.plannedPath = [];
|
||||
if (this.onEntitySelect) this.onEntitySelect(this.player.id, false);
|
||||
if (this.onEntitySelect) this.onEntitySelect(id, false);
|
||||
if (this.onPathChange) this.onPathChange([]);
|
||||
}
|
||||
|
||||
// Alias for legacy calls if any
|
||||
deselectPlayer() {
|
||||
this.deselectEntity();
|
||||
}
|
||||
|
||||
planStep(x, y) {
|
||||
// Determine start point (either current player pos or last planned step)
|
||||
if (!this.selectedEntity) return;
|
||||
|
||||
// Determine start point
|
||||
const lastStep = this.plannedPath.length > 0
|
||||
? this.plannedPath[this.plannedPath.length - 1]
|
||||
: { x: this.player.x, y: this.player.y };
|
||||
: { x: this.selectedEntity.x, y: this.selectedEntity.y };
|
||||
|
||||
// Check Adjacency
|
||||
const dx = Math.abs(x - lastStep.x);
|
||||
@@ -85,48 +189,62 @@ export class GameEngine {
|
||||
// Check Walkability
|
||||
const isWalkable = this.canMoveTo(x, y);
|
||||
|
||||
// Check if already in path (prevent loops for simplicity or allow backtracking? User said "mark contigua", implying adding)
|
||||
// If clicking the last added step, maybe remove it? (Undo)
|
||||
// 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) {
|
||||
// Clicked last step -> Undo
|
||||
this.plannedPath.pop();
|
||||
if (this.onPathChange) this.onPathChange(this.plannedPath);
|
||||
this.onPathChange && this.onPathChange(this.plannedPath);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isAdjacent && isWalkable) {
|
||||
// Check if not already visited in this path to prevent self-intersection weirdness
|
||||
const alreadyInPath = this.plannedPath.some(p => p.x === x && p.y === y);
|
||||
const isPlayerPos = this.player.x === x && this.player.y === y;
|
||||
const isEntityPos = this.selectedEntity.x === x && this.selectedEntity.y === y;
|
||||
|
||||
if (!alreadyInPath && !isPlayerPos) {
|
||||
// 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 });
|
||||
if (this.onPathChange) {
|
||||
this.onPathChange(this.plannedPath);
|
||||
}
|
||||
this.onPathChange && this.onPathChange(this.plannedPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
executeMovePath() {
|
||||
if (!this.player || !this.plannedPath.length) return;
|
||||
if (!this.selectedEntity || !this.plannedPath.length) return;
|
||||
|
||||
// Clone path for the move event
|
||||
const path = [...this.plannedPath];
|
||||
const entity = this.selectedEntity;
|
||||
|
||||
// Update player logic verification immediately (teleport logic)
|
||||
// The visualization will handle the "botecitos"
|
||||
// Update verify immediately
|
||||
const finalDest = path[path.length - 1];
|
||||
this.player.x = finalDest.x;
|
||||
this.player.y = finalDest.y;
|
||||
entity.x = finalDest.x;
|
||||
entity.y = finalDest.y;
|
||||
|
||||
// Trigger Movement Event (Renderer will animate)
|
||||
// Visual animation
|
||||
if (this.onEntityMove) {
|
||||
this.onEntityMove(this.player, path);
|
||||
this.onEntityMove(entity, path);
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
this.deselectPlayer();
|
||||
// Deduct Moves
|
||||
if (entity.currentMoves !== undefined) {
|
||||
entity.currentMoves -= path.length;
|
||||
if (entity.currentMoves < 0) entity.currentMoves = 0;
|
||||
}
|
||||
|
||||
this.deselectEntity();
|
||||
}
|
||||
|
||||
canMoveTo(x, y) {
|
||||
@@ -141,18 +259,19 @@ export class GameEngine {
|
||||
if (this.onEntityMove) this.onEntityMove(this.player, [{ x, y }]);
|
||||
}
|
||||
|
||||
isPlayerAdjacentToDoor(doorCells) {
|
||||
if (!this.player) return false;
|
||||
// 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;
|
||||
|
||||
// doorCells should be an array of {x, y} objects
|
||||
// If it sends a single object, wrap it
|
||||
const cells = Array.isArray(doorCells) ? doorCells : [doorCells];
|
||||
|
||||
for (const cell of cells) {
|
||||
const dx = Math.abs(this.player.x - cell.x);
|
||||
const dy = Math.abs(this.player.y - cell.y);
|
||||
|
||||
// Adjacent means distance of 1 in one direction and 0 in the other
|
||||
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;
|
||||
}
|
||||
@@ -160,7 +279,55 @@ export class GameEngine {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,30 +5,36 @@ export class TurnManager {
|
||||
this.currentTurn = 0;
|
||||
this.currentPhase = GAME_PHASES.SETUP;
|
||||
this.listeners = {}; // Simple event system
|
||||
|
||||
// Power Phase State
|
||||
this.currentPowerRoll = 0;
|
||||
this.eventsTriggered = [];
|
||||
}
|
||||
|
||||
startGame() {
|
||||
this.currentTurn = 1;
|
||||
this.setPhase(GAME_PHASES.HERO); // Jump straight to Hero phase for now
|
||||
console.log(`--- TURN ${this.currentTurn} START ---`);
|
||||
this.startPowerPhase();
|
||||
}
|
||||
|
||||
nextPhase() {
|
||||
// Simple sequential flow for now
|
||||
// Simple sequential flow
|
||||
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.
|
||||
// Move to Monster Phase
|
||||
this.setPhase(GAME_PHASES.MONSTER);
|
||||
break;
|
||||
case GAME_PHASES.MONSTER:
|
||||
// Move to Exploration Phase
|
||||
this.setPhase(GAME_PHASES.EXPLORATION);
|
||||
break;
|
||||
case GAME_PHASES.EXPLORATION:
|
||||
// End Turn and restart
|
||||
this.endTurn();
|
||||
break;
|
||||
// Exploration is usually triggered as an interrupt, not strictly sequential
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +46,37 @@ export class TurnManager {
|
||||
}
|
||||
}
|
||||
|
||||
startPowerPhase() {
|
||||
this.setPhase(GAME_PHASES.POWER);
|
||||
this.rollPowerDice();
|
||||
}
|
||||
|
||||
rollPowerDice() {
|
||||
const roll = Math.floor(Math.random() * 6) + 1;
|
||||
this.currentPowerRoll = roll;
|
||||
console.log(`Power Roll: ${roll}`);
|
||||
|
||||
let message = "The dungeon is quiet...";
|
||||
let eventTriggered = false;
|
||||
|
||||
if (roll === 1) {
|
||||
message = "UNEXPECTED EVENT! (Roll of 1)";
|
||||
eventTriggered = true;
|
||||
this.triggerRandomEvent();
|
||||
}
|
||||
|
||||
this.emit('POWER_RESULT', { roll, message, eventTriggered });
|
||||
|
||||
// Auto-advance to Hero phase after short delay (game feel)
|
||||
setTimeout(() => {
|
||||
this.nextPhase();
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
triggerRandomEvent() {
|
||||
console.warn("TODO: TRIGGER EVENT CARD DRAW");
|
||||
}
|
||||
|
||||
triggerExploration() {
|
||||
this.setPhase(GAME_PHASES.EXPLORATION);
|
||||
// Logic to return to HERO phase would handle elsewhere
|
||||
@@ -48,7 +85,7 @@ export class TurnManager {
|
||||
endTurn() {
|
||||
console.log(`--- TURN ${this.currentTurn} END ---`);
|
||||
this.currentTurn++;
|
||||
this.setPhase(GAME_PHASES.POWER);
|
||||
this.startPowerPhase();
|
||||
}
|
||||
|
||||
// -- Simple Observer Pattern --
|
||||
|
||||
Reference in New Issue
Block a user