Implement Initiative Turn System and Fog of War

This commit is contained in:
2026-01-09 14:12:40 +01:00
parent e45207807d
commit b08a922c00
4 changed files with 258 additions and 3 deletions

View File

@@ -52,12 +52,18 @@ export class GameEngine {
if (phase === 'hero' || phase === 'exploration') {
this.resetHeroMoves();
}
if (phase === 'hero') {
this.initializeTurnOrder();
}
});
// End of Turn Logic (Buffs, cooldowns, etc)
this.turnManager.on('turn_ended', (turn) => {
this.handleEndTurn();
});
// Initial Light Update
setTimeout(() => this.updateLighting(), 500);
}
resetHeroMoves() {
@@ -161,6 +167,70 @@ export class GameEngine {
this.player = this.heroes[0];
}
initializeTurnOrder() {
console.log("[GameEngine] Initializing Turn Order...");
// 1. Identify Leader
const leader = this.getLeader();
// 2. Sort Rest by Initiative (Descending)
// Note: Sort is stable or we rely on index? Array.sort is stable in modern JS.
const others = this.heroes.filter(h => h !== leader);
others.sort((a, b) => b.stats.init - a.stats.init);
// 3. Construct Order
this.heroTurnOrder = [leader, ...others];
this.currentTurnIndex = 0;
console.log("Turn Order:", this.heroTurnOrder.map(h => `${h.name} (${h.stats.init})`));
// 4. Activate First
this.activateHero(this.heroTurnOrder[0]);
}
activateHero(hero) {
this.selectedEntity = hero;
// Update selection UI
if (this.onEntitySelect) {
// Deselect all keys first?
this.heroes.forEach(h => this.onEntitySelect(h.id, false));
this.onEntitySelect(hero.id, true);
}
// Notify UI about active turn
if (this.onShowMessage) {
this.onShowMessage(`Turno de ${hero.name}`, "Mueve y Ataca.");
}
// Mark as active in renderer (Green Ring vs Yellow Selection)
if (window.RENDERER) {
this.heroes.forEach(h => window.RENDERER.setEntityActive(h.id, false));
window.RENDERER.setEntityActive(hero.id, true);
}
}
nextHeroTurn() {
this.currentTurnIndex++;
if (this.currentTurnIndex < this.heroTurnOrder.length) {
this.activateHero(this.heroTurnOrder[this.currentTurnIndex]);
} else {
console.log("All heroes acted. Ending Phase sequence if auto?");
this.deselectEntity();
if (window.RENDERER) {
this.heroes.forEach(h => window.RENDERER.setEntityActive(h.id, false));
}
if (this.onShowMessage) {
this.onShowMessage("Fase de Aventureros Terminada", "Pasando a Monstruos...");
}
// Auto Advance Phase? Or Manual?
// Usually manual "End Turn" button triggers nextHeroTurn.
// When last hero ends, we trigger nextPhase.
setTimeout(() => {
this.turnManager.nextPhase();
}, 1000);
}
}
spawnMonster(monsterKey, x, y, options = {}) {
const definition = MONSTER_DEFINITIONS[monsterKey];
if (!definition) {
@@ -309,8 +379,20 @@ export class GameEngine {
const clickedEntity = clickedHero || clickedMonster;
if (clickedEntity) {
// STRICT TURN ORDER CHECK
if (this.turnManager.currentPhase === 'hero' && clickedHero) {
const currentActiveHero = this.heroTurnOrder ? this.heroTurnOrder[this.currentTurnIndex] : null;
if (currentActiveHero && clickedHero.id !== currentActiveHero.id) {
if (this.onShowMessage) this.onShowMessage("No es su turno", `Es el turno de ${currentActiveHero.name}.`);
return;
}
}
if (this.selectedEntity === clickedEntity) {
// Toggle Deselect
// Prevent deselecting active hero effectively? Or allow it but re-select him if trying to select others?
// For now, allow deselect freely.
this.deselectEntity();
} else if (this.selectedMonster === clickedMonster && clickedMonster) {
// Clicking on already selected monster - deselect it
@@ -369,9 +451,61 @@ export class GameEngine {
performHeroAttack(targetMonsterId) {
const hero = this.selectedEntity;
const monster = this.monsters.find(m => m.id === targetMonsterId);
// Attack ends turn logic could be here? Assuming user clicks "End Turn" manually for now.
// Or if standard rules: 1 attack per turn.
// For now, just attack.
return this.combatSystem.handleMeleeAttack(hero, monster);
}
updateLighting() {
if (!window.RENDERER) return;
const leader = this.getLeader();
if (!leader) return;
// 1. Get Leader Tile ID
const leaderTileId = this.dungeon.grid.occupiedCells.get(`${leader.x},${leader.y}`);
if (!leaderTileId) return;
const visibleTileIds = new Set([leaderTileId]);
// 2. Find Neighbor Tiles (Connected Board Sections)
// Iterate grid occupied cells to find cells belonging to leaderTileId
// Then check their neighbors for DIFFERENT tile IDs that are connected.
// Optimization: We could cache this or iterate efficiently
// For now, scan occupiedCells (Map)
for (const [key, tid] of this.dungeon.grid.occupiedCells.entries()) {
if (tid === leaderTileId) {
const [cx, cy] = key.split(',').map(Number);
// Check 4 neighbors
const neighbors = [
{ x: cx + 1, y: cy }, { x: cx - 1, y: cy },
{ x: cx, y: cy + 1 }, { x: cx, y: cy - 1 }
];
for (const n of neighbors) {
const nKey = `${n.x},${n.y}`;
const nTileId = this.dungeon.grid.occupiedCells.get(nKey);
if (nTileId && nTileId !== leaderTileId) {
// Found a neighbor tile!
// Check connectivity logic (Walls/Doors)
if (this.dungeon.grid.canMoveBetween(cx, cy, n.x, n.y)) {
visibleTileIds.add(nTileId);
}
}
}
}
}
window.RENDERER.updateFogOfWar(Array.from(visibleTileIds));
}
performRangedAttack(targetMonsterId) {
const hero = this.selectedEntity;
const monster = this.monsters.find(m => m.id === targetMonsterId);
@@ -517,6 +651,11 @@ export class GameEngine {
entity.y = step.y;
stepsTaken++;
// Update Light if Lantern Bearer
if (entity.hasLantern) {
this.updateLighting();
}
// 2. Check for New Tile Entry
const tileId = this.dungeon.grid.occupiedCells.get(`${step.x},${step.y}`);