diff --git a/public/assets/images/dungeon1/standees/heroes/barbaro.png b/public/assets/images/dungeon1/standees/heroes/barbarian.png similarity index 100% rename from public/assets/images/dungeon1/standees/heroes/barbaro.png rename to public/assets/images/dungeon1/standees/heroes/barbarian.png diff --git a/src/engine/data/Heroes.js b/src/engine/data/Heroes.js new file mode 100644 index 0000000..7550dd3 --- /dev/null +++ b/src/engine/data/Heroes.js @@ -0,0 +1,67 @@ +export const HERO_DEFINITIONS = { + barbarian: { + id: 'barbarian', + name: 'Bárbaro', + portrait: '/assets/images/dungeon1/standees/heroes/barbarian.png?v=1', + stats: { + move: 4, + ws: 4, // Weapon Skill + bs: 5, // Ballistic Skill (3+ to hit, often lower is better in WHQ, let's use standard table numbers for now) + str: 4, + toughness: 4, + wounds: 12, + attacks: 1, + init: 3, + luck: 2 // Rerolls?? + } + }, + dwarf: { + id: 'dwarf', + name: 'Enano', + portrait: '/assets/images/dungeon1/standees/heroes/dwarf.png', + stats: { + move: 3, + ws: 5, + bs: 5, + str: 3, + toughness: 5, + wounds: 13, + attacks: 1, + init: 2, + luck: 0 + } + }, + elf: { + id: 'elf', + name: 'Elfa', + portrait: '/assets/images/dungeon1/standees/heroes/elfa.png', + stats: { + move: 5, + ws: 4, + bs: 2, // Amazing shot + str: 3, + toughness: 3, + wounds: 10, + attacks: 1, + init: 6, + luck: 1 + } + }, + wizard: { + id: 'wizard', + name: 'Hechicero', + portrait: '/assets/images/dungeon1/standees/heroes/warlock.png', + stats: { + move: 4, + ws: 3, + bs: 6, + str: 3, + toughness: 3, + wounds: 9, + attacks: 1, + init: 4, + luck: 1, + power: 0 // Special mechanic + } + } +}; diff --git a/src/engine/data/Monsters.js b/src/engine/data/Monsters.js new file mode 100644 index 0000000..d8a8b80 --- /dev/null +++ b/src/engine/data/Monsters.js @@ -0,0 +1,32 @@ +export const MONSTER_DEFINITIONS = { + orc: { + id: 'orc', + name: 'Orco', + portrait: '/assets/images/dungeon1/standees/enemies/orc.png', + stats: { + move: 4, + ws: 3, + bs: 5, + str: 3, + toughness: 4, + wounds: 4, + attacks: 1, + gold: 15 + } + }, + chaos_warrior: { + id: 'chaos_warrior', + name: 'Guerrero del Caos', + portrait: '/assets/images/dungeon1/standees/enemies/chaosWarrior.png', + stats: { + move: 4, + ws: 5, + bs: 0, + str: 5, + toughness: 5, + wounds: 8, + attacks: 2, + gold: 150 + } + } +}; diff --git a/src/engine/dungeon/TileDefinitions.js b/src/engine/dungeon/TileDefinitions.js index dfd08d0..efee3c9 100644 --- a/src/engine/dungeon/TileDefinitions.js +++ b/src/engine/dungeon/TileDefinitions.js @@ -71,9 +71,9 @@ export const TILES = { layout: [[1, 1], [1, 1], [1, 1], [1, 1], [1, 1], [1, 1]], exits: [ { x: 0, y: 0, direction: DIRECTIONS.SOUTH }, { x: 1, y: 0, direction: DIRECTIONS.SOUTH }, - { x: 0, y: 5, direction: DIRECTIONS.NORTH }, { x: 1, y: 5, direction: DIRECTIONS.NORTH }, - { x: 0, y: 3, direction: DIRECTIONS.WEST }, { x: 0, y: 4, direction: DIRECTIONS.WEST }, - { x: 1, y: 3, direction: DIRECTIONS.EAST }, { x: 1, y: 4, direction: DIRECTIONS.EAST } + { x: 0, y: 5, direction: DIRECTIONS.NORTH }, { x: 1, y: 5, direction: DIRECTIONS.NORTH } + //{ x: 0, y: 3, direction: DIRECTIONS.WEST }, { x: 0, y: 4, direction: DIRECTIONS.WEST }, + //{ x: 1, y: 3, direction: DIRECTIONS.EAST }, { x: 1, y: 4, direction: DIRECTIONS.EAST } ] }, [DIRECTIONS.SOUTH]: { @@ -81,9 +81,9 @@ export const TILES = { layout: [[1, 1], [1, 1], [1, 1], [1, 1], [1, 1], [1, 1]], exits: [ { x: 0, y: 0, direction: DIRECTIONS.SOUTH }, { x: 1, y: 0, direction: DIRECTIONS.SOUTH }, - { x: 0, y: 5, direction: DIRECTIONS.NORTH }, { x: 1, y: 5, direction: DIRECTIONS.NORTH }, - { x: 0, y: 3, direction: DIRECTIONS.WEST }, { x: 0, y: 4, direction: DIRECTIONS.WEST }, - { x: 1, y: 3, direction: DIRECTIONS.EAST }, { x: 1, y: 4, direction: DIRECTIONS.EAST } + { x: 0, y: 5, direction: DIRECTIONS.NORTH }, { x: 1, y: 5, direction: DIRECTIONS.NORTH } + //{ x: 0, y: 3, direction: DIRECTIONS.WEST }, { x: 0, y: 4, direction: DIRECTIONS.WEST }, + //{ x: 1, y: 3, direction: DIRECTIONS.EAST }, { x: 1, y: 4, direction: DIRECTIONS.EAST } ] }, [DIRECTIONS.EAST]: { @@ -91,9 +91,9 @@ export const TILES = { layout: [[1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1]], exits: [ { x: 0, y: 0, direction: DIRECTIONS.WEST }, { x: 0, y: 1, direction: DIRECTIONS.WEST }, - { x: 5, y: 0, direction: DIRECTIONS.EAST }, { x: 5, y: 1, direction: DIRECTIONS.EAST }, - { x: 3, y: 0, direction: DIRECTIONS.SOUTH }, { x: 4, y: 0, direction: DIRECTIONS.SOUTH }, - { x: 3, y: 1, direction: DIRECTIONS.NORTH }, { x: 4, y: 1, direction: DIRECTIONS.NORTH } + { x: 5, y: 0, direction: DIRECTIONS.EAST }, { x: 5, y: 1, direction: DIRECTIONS.EAST } + //{ x: 3, y: 0, direction: DIRECTIONS.SOUTH }, { x: 4, y: 0, direction: DIRECTIONS.SOUTH }, + //{ x: 3, y: 1, direction: DIRECTIONS.NORTH }, { x: 4, y: 1, direction: DIRECTIONS.NORTH } ] }, [DIRECTIONS.WEST]: { @@ -101,9 +101,9 @@ export const TILES = { layout: [[1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1]], exits: [ { x: 0, y: 0, direction: DIRECTIONS.WEST }, { x: 0, y: 1, direction: DIRECTIONS.WEST }, - { x: 5, y: 0, direction: DIRECTIONS.EAST }, { x: 5, y: 1, direction: DIRECTIONS.EAST }, - { x: 3, y: 0, direction: DIRECTIONS.SOUTH }, { x: 4, y: 0, direction: DIRECTIONS.SOUTH }, - { x: 3, y: 1, direction: DIRECTIONS.NORTH }, { x: 4, y: 1, direction: DIRECTIONS.NORTH } + { x: 5, y: 0, direction: DIRECTIONS.EAST }, { x: 5, y: 1, direction: DIRECTIONS.EAST } + //{ x: 3, y: 0, direction: DIRECTIONS.SOUTH }, { x: 4, y: 0, direction: DIRECTIONS.SOUTH }, + //{ x: 3, y: 1, direction: DIRECTIONS.NORTH }, { x: 4, y: 1, direction: DIRECTIONS.NORTH } ] } } diff --git a/src/engine/game/GameEngine.js b/src/engine/game/GameEngine.js index 1bf3f22..49969ba 100644 --- a/src/engine/game/GameEngine.js +++ b/src/engine/game/GameEngine.js @@ -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; + } } diff --git a/src/engine/game/TurnManager.js b/src/engine/game/TurnManager.js index d8fdcc6..7c1fb9a 100644 --- a/src/engine/game/TurnManager.js +++ b/src/engine/game/TurnManager.js @@ -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 -- diff --git a/src/main.js b/src/main.js index 3b30c76..c1172fc 100644 --- a/src/main.js +++ b/src/main.js @@ -44,10 +44,10 @@ game.onEntityUpdate = (entity) => { renderer.addEntity(entity); renderer.updateEntityPosition(entity); - // Center camera on player spawn - if (entity.id === 'p1' && !entity._centered) { + // Center camera on FIRST hero spawn + if (game.heroes && game.heroes[0] && entity.id === game.heroes[0].id && !window._cameraCentered) { cameraManager.centerOn(entity.x, entity.y); - entity._centered = true; + window._cameraCentered = true; } }; @@ -55,9 +55,7 @@ game.onEntityMove = (entity, path) => { renderer.moveEntityAlongPath(entity, path); }; -game.onEntitySelect = (entityId, isSelected) => { - renderer.toggleEntitySelection(entityId, isSelected); -}; +// game.onEntitySelect is now handled by UIManager to wrap the renderer call renderer.onHeroFinishedMove = (x, y) => { cameraManager.centerOn(x, y); @@ -107,10 +105,24 @@ const handleClick = (x, y, doorMesh) => { } if (!doorMesh.userData.isOpen) { - const doorExit = doorMesh.userData.cells[0]; - if (game.isPlayerAdjacentToDoor(doorMesh.userData.cells)) { + // 1. Check Selection and Leadership (STRICT) + const selectedHero = game.selectedEntity; + if (!selectedHero) { + ui.showModal('Ningún Héroe seleccionado', 'Selecciona al Líder (Portador de la Lámpara) para abrir la puerta.'); + return; + } + + if (!selectedHero.hasLantern) { + ui.showModal('Acción no permitida', `${selectedHero.name} no lleva la Lámpara. Solo el Líder puede explorar.`); + return; + } + + // 2. Check Adjacency + // Since we know selectedHero IS the leader, we can just check if *this* hero is adjacent. + // game.isLeaderAdjacentToDoor checks the 'getLeader()' position, which aligns with selectedHero here. + if (game.isLeaderAdjacentToDoor(doorMesh.userData.cells)) { // Open door visually renderer.openDoor(doorMesh); @@ -119,11 +131,16 @@ const handleClick = (x, y, doorMesh) => { const exitData = doorMesh.userData.exitData; if (exitData) { generator.selectDoor(exitData); + + // Allow UI to update phase if not already + // if (game.turnManager.currentPhase !== 'exploration') { + // game.turnManager.setPhase('exploration'); + // } } else { console.error('[Main] Door missing exitData'); } } else { - // Optional: Message if too far? + ui.showModal('Demasiado lejos', 'El Líder debe estar adyacente a la puerta para abrirla.'); } return; } @@ -144,6 +161,20 @@ renderer.setupInteraction( } ); +// Debug: Spawn Monster +window.addEventListener('keydown', (e) => { + if (e.key === 'm' || e.key === 'M') { + const x = game.player.x + 2; + const y = game.player.y; + if (game.dungeon.grid.isOccupied(x, y)) { + console.log("Spawning Orc..."); + game.spawnMonster('orc', x, y); + } else { + console.log("Cannot spawn here"); + } + } +}); + // 7. Start game.startMission(mission); diff --git a/src/view/GameRenderer.js b/src/view/GameRenderer.js index 611d407..b85bb41 100644 --- a/src/view/GameRenderer.js +++ b/src/view/GameRenderer.js @@ -185,6 +185,14 @@ export class GameRenderer { mesh.position.set(entity.x, h / 2, -entity.y); + // Clear old children if re-adding (to prevent multiple rings) + for (let i = mesh.children.length - 1; i >= 0; i--) { + const child = mesh.children[i]; + if (child.name === "SelectionRing") { + mesh.remove(child); + } + } + // Selection Circle const ringGeom = new THREE.RingGeometry(0.3, 0.4, 32); const ringMat = new THREE.MeshBasicMaterial({ color: 0xffff00, side: THREE.DoubleSide, transparent: true, opacity: 0.35 }); @@ -471,7 +479,7 @@ export class GameRenderer { }, undefined, (err) => { - console.error(`[TextureLoader] ✗ Failed to load: ${path}`, err); + console.error(`[TextureLoader] [Checked] ✗ Failed to load: ${path}`, err); } ); tex.magFilter = THREE.NearestFilter; diff --git a/src/view/UIManager.js b/src/view/UIManager.js index fc88aad..6f2be69 100644 --- a/src/view/UIManager.js +++ b/src/view/UIManager.js @@ -5,9 +5,39 @@ export class UIManager { this.cameraManager = cameraManager; this.game = gameEngine; this.dungeon = gameEngine.dungeon; + this.selectedHero = null; this.createHUD(); + this.createGameStatusPanel(); // New Panel this.setupMinimapLoop(); + this.setupGameListeners(); // New Listeners + + // Hook into engine callbacks for UI updates + const originalSelect = this.game.onEntitySelect; + this.game.onEntitySelect = (id, isSelected) => { + // 1. Call Renderer (was in main.js) + if (this.cameraManager && this.cameraManager.renderer) { + this.cameraManager.renderer.toggleEntitySelection(id, isSelected); + } else if (window.RENDERER) { + window.RENDERER.toggleEntitySelection(id, isSelected); + } + + // 2. Update UI + if (isSelected) { + const hero = this.game.heroes.find(h => h.id === id); + this.selectedHero = hero; // Store state + this.updateHeroStats(hero); + } else { + this.selectedHero = null; + this.updateHeroStats(null); + } + }; + + const originalMove = this.game.onEntityMove; + this.game.onEntityMove = (entity, path) => { + if (originalMove) originalMove(entity, path); + this.updateHeroStats(entity); + }; } createHUD() { @@ -323,37 +353,17 @@ export class UIManager { ctx.clearRect(0, 0, w, h); - // Center the view on 0,0 or the average? - // Let's rely on fixed scale for now const cellSize = 5; const centerX = w / 2; const centerY = h / 2; - // Draw placed tiles - // We can access this.dungeon.grid.occupiedCells for raw occupied spots - // Or this.dungeon.placedTiles for structural info (type, color) - ctx.fillStyle = '#666'; // Generic floor - // Iterate over grid occupied cells - // But grid is a Map, iterating keys is slow. - // Better to iterate placedTiles which is an Array - - // Simpler approach: Iterate the Grid Map directly - // It's a Map<"x,y", tileId> - // Use an iterator for (const [key, tileId] of this.dungeon.grid.occupiedCells) { const [x, y] = key.split(',').map(Number); - - // Coordinate transformation to Canvas - // Dungeon (0,0) -> Canvas (CenterX, CenterY) - // Y in dungeon is Up/North. Y in Canvas is Down. - // So CanvasY = CenterY - (DungeonY * size) - const cx = centerX + (x * cellSize); const cy = centerY - (y * cellSize); - // Color based on TileId type? if (tileId.includes('room')) ctx.fillStyle = '#55a'; else ctx.fillStyle = '#aaa'; @@ -379,6 +389,7 @@ export class UIManager { ctx.lineTo(centerX, centerY + 5); ctx.stroke(); } + showModal(title, message) { // Overlay const overlay = document.createElement('div'); @@ -414,7 +425,7 @@ export class UIManager { // Message const msgEl = document.createElement('p'); - msgEl.textContent = message; + msgEl.innerHTML = message; msgEl.style.fontSize = '16px'; msgEl.style.lineHeight = '1.5'; content.appendChild(msgEl); @@ -437,6 +448,7 @@ export class UIManager { overlay.appendChild(content); this.container.appendChild(overlay); } + showConfirm(title, message, onConfirm) { // Overlay const overlay = document.createElement('div'); @@ -472,7 +484,7 @@ export class UIManager { // Message const msgEl = document.createElement('p'); - msgEl.textContent = message; + msgEl.innerHTML = message; msgEl.style.fontSize = '16px'; msgEl.style.lineHeight = '1.5'; content.appendChild(msgEl); @@ -516,4 +528,186 @@ export class UIManager { overlay.appendChild(content); this.container.appendChild(overlay); } + + createGameStatusPanel() { + // Top Center Panel + this.statusPanel = document.createElement('div'); + this.statusPanel.style.position = 'absolute'; + this.statusPanel.style.top = '20px'; + this.statusPanel.style.left = '50%'; + this.statusPanel.style.transform = 'translateX(-50%)'; + this.statusPanel.style.display = 'flex'; + this.statusPanel.style.flexDirection = 'column'; + this.statusPanel.style.alignItems = 'center'; + this.statusPanel.style.pointerEvents = 'none'; + + // Turn/Phase Info + this.phaseInfo = document.createElement('div'); + this.phaseInfo.style.backgroundColor = 'rgba(0, 0, 0, 0.8)'; + this.phaseInfo.style.padding = '10px 20px'; + this.phaseInfo.style.border = '2px solid #daa520'; // GoldenRod + this.phaseInfo.style.borderRadius = '5px'; + this.phaseInfo.style.color = '#fff'; + this.phaseInfo.style.fontFamily = '"Cinzel", serif'; + this.phaseInfo.style.fontSize = '20px'; + this.phaseInfo.style.textAlign = 'center'; + this.phaseInfo.style.textTransform = 'uppercase'; + this.phaseInfo.style.minWidth = '200px'; + this.phaseInfo.innerHTML = ` +
Turn 1
+
Setup
+ `; + + this.statusPanel.appendChild(this.phaseInfo); + + // End Phase Button + this.endPhaseBtn = document.createElement('button'); + this.endPhaseBtn.textContent = 'ACABAR FASE AVENTUREROS'; + this.endPhaseBtn.style.marginTop = '10px'; + this.endPhaseBtn.style.width = '100%'; + this.endPhaseBtn.style.padding = '8px'; + this.endPhaseBtn.style.backgroundColor = '#daa520'; // Gold + this.endPhaseBtn.style.color = '#000'; + this.endPhaseBtn.style.border = '1px solid #8B4513'; + this.endPhaseBtn.style.borderRadius = '3px'; + this.endPhaseBtn.style.fontWeight = 'bold'; + this.endPhaseBtn.style.cursor = 'pointer'; + this.endPhaseBtn.style.display = 'none'; // Hidden by default + this.endPhaseBtn.style.fontFamily = '"Cinzel", serif'; + this.endPhaseBtn.style.fontSize = '12px'; + this.endPhaseBtn.style.pointerEvents = 'auto'; // Enable clicking + + this.endPhaseBtn.onmouseover = () => { this.endPhaseBtn.style.backgroundColor = '#ffd700'; }; + this.endPhaseBtn.onmouseout = () => { this.endPhaseBtn.style.backgroundColor = '#daa520'; }; + + this.endPhaseBtn.onclick = () => { + console.log('[UIManager] End Phase Button Clicked', this.game.turnManager.currentPhase); + this.game.turnManager.nextPhase(); + }; + this.statusPanel.appendChild(this.endPhaseBtn); + + // Notification Area (Power Roll results, etc) + this.notificationArea = document.createElement('div'); + this.notificationArea.style.marginTop = '10px'; + this.notificationArea.style.transition = 'opacity 0.5s'; + this.notificationArea.style.opacity = '0'; + this.statusPanel.appendChild(this.notificationArea); + + this.container.appendChild(this.statusPanel); + + // Inject Font + if (!document.getElementById('game-font')) { + const link = document.createElement('link'); + link.id = 'game-font'; + link.href = 'https://fonts.googleapis.com/css2?family=Cinzel:wght@400;700&display=swap'; + link.rel = 'stylesheet'; + document.head.appendChild(link); + } + } + + setupGameListeners() { + if (this.game.turnManager) { + this.game.turnManager.on('phase_changed', (phase) => { + this.updatePhaseDisplay(phase); + }); + + this.game.turnManager.on('POWER_RESULT', (data) => { + this.showPowerRollResult(data); + }); + } + } + + updatePhaseDisplay(phase) { + if (!this.phaseInfo) return; + const turn = this.game.turnManager.currentTurn; + + let content = ` +
Turn ${turn}
+
${phase.replace('_', ' ')}
+ `; + + if (this.selectedHero) { + content += this.getHeroStatsHTML(this.selectedHero); + } + + this.phaseInfo.innerHTML = content; + + if (this.endPhaseBtn) { + if (phase === 'hero') { + this.endPhaseBtn.style.display = 'block'; + this.endPhaseBtn.textContent = 'ACABAR FASE AVENTUREROS'; + this.endPhaseBtn.title = "Pasar a la Fase de Monstruos"; + } else if (phase === 'monster') { + this.endPhaseBtn.style.display = 'block'; + this.endPhaseBtn.textContent = 'ACABAR FASE MONSTRUOS'; + this.endPhaseBtn.title = "Pasar a Fase de Exploración"; + } else if (phase === 'exploration') { + this.endPhaseBtn.style.display = 'block'; + this.endPhaseBtn.textContent = 'ACABAR TURNO'; + this.endPhaseBtn.title = "Finalizar turno y comenzar Fase de Poder"; + } else { + this.endPhaseBtn.style.display = 'none'; + } + } + } + + updateHeroStats(hero) { + if (!this.phaseInfo) return; + + const turn = this.game.turnManager.currentTurn; + const phase = this.game.turnManager.currentPhase; + + let content = ` +
Turn ${turn}
+
${phase.replace('_', ' ')}
+ `; + + if (hero) { + content += this.getHeroStatsHTML(hero); + } + + this.phaseInfo.innerHTML = content; + } + + getHeroStatsHTML(hero) { + const portraitUrl = hero.texturePath || ''; + + const lanternIcon = hero.hasLantern ? '🏮' : ''; + + return ` +
+
+ ${hero.name} +
+
+
+ ${hero.name} ${lanternIcon} +
+
+ Moves: ${hero.currentMoves} / ${hero.stats.move} +
+
+
+ `; + } + + showPowerRollResult(data) { + if (!this.notificationArea) return; + const { roll, message, eventTriggered } = data; + const color = eventTriggered ? '#ff4444' : '#44ff44'; + + this.notificationArea.innerHTML = ` +
+
Power Phase
+
${roll}
+
${message}
+
+ `; + + this.notificationArea.style.opacity = '1'; + + setTimeout(() => { + if (this.notificationArea) this.notificationArea.style.opacity = '0'; + }, 3000); + } }