import { GameEngine } from './engine/game/GameEngine.js'; import { GameRenderer } from './view/GameRenderer.js'; import { CameraManager } from './view/CameraManager.js'; import { UIManager } from './view/UIManager.js'; import { SoundManager } from './view/SoundManager.js'; import { MissionConfig, MISSION_TYPES } from './engine/dungeon/MissionConfig.js'; // 1. Setup Mission const mission = new MissionConfig({ id: 'mission_1', name: 'Manual Construction', type: MISSION_TYPES.ESCAPE, minTiles: 13 }); // 2. Initialize Core Systems const renderer = new GameRenderer('app'); const cameraManager = new CameraManager(renderer); const game = new GameEngine(); const ui = new UIManager(cameraManager, game); const soundManager = new SoundManager(); // Start Music (Autoplay handling included in manager) soundManager.playMusic('exploration'); // Global Access window.GAME = game; window.RENDERER = renderer; window.SOUND_MANAGER = soundManager; // 3. Connect Dungeon Generator to Renderer const generator = game.dungeon; const originalPlaceTile = generator.grid.placeTile.bind(generator.grid); generator.grid.placeTile = (instance, variant, card) => { originalPlaceTile(instance, variant); const cells = generator.grid.calculateCells(variant, instance.x, instance.y); renderer.addTile(cells, card.type, card, instance); setTimeout(() => { renderer.renderExits(generator.availableExits); // Don't show modal if we are not in Exploration phase (e.g. during Setup) if (game.turnManager.currentPhase !== 'exploration') { return; } // NEW RULE: Exploration ends turn immediately. No monsters yet. // Monsters appear when a hero ENTERS the new room in the next turn. ui.showModal('Exploración Completada', 'Has colocado una nueva sección de mazmorra.
El turno termina aquí.', () => { game.turnManager.endTurn(); } ); }, 50); }; // 4. Connect Player to Renderer game.onEntityUpdate = (entity) => { renderer.addEntity(entity); renderer.updateEntityPosition(entity); // 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); window._cameraCentered = true; } }; game.turnManager.on('phase_changed', (phase) => { if (phase === 'monster') { setTimeout(async () => { await game.playMonsterTurn(); // Logic: Skip Exploration if monsters are alive const hasActiveMonsters = game.monsters && game.monsters.some(m => !m.isDead); if (hasActiveMonsters) { ui.showModal('¡Combate en curso!', 'Aún quedan monstruos vivos. Se salta la Fase de Exploración.
Preparaos para la Fase de Poder del siguiente turno.', () => { // Combat Loop: Power -> Hero -> Monster -> (Skip Exp) -> Power... game.turnManager.endTurn(); } ); } else { ui.showModal('Zona Despejada', 'Fase de Monstruos Finalizada.
Pulsa para continuar a la Fase de Exploración.', () => { game.turnManager.nextPhase(); // Go to Exploration } ); } }, 500); // Slight delay for visual impact } }); game.onCombatResult = (log) => { ui.showCombatLog(log); // 1. Show Attack Roll on Attacker // Find Attacker pos const attacker = game.heroes.find(h => h.id === log.attackerId) || game.monsters.find(m => m.id === log.attackerId); if (attacker) { const rollColor = log.hitSuccess ? '#00ff00' : '#888888'; // Green vs Gray renderer.showFloatingText(attacker.x, attacker.y, `🎲 ${log.hitRoll}`, rollColor); } // 2. Show Damage on Defender const defender = game.heroes.find(h => h.id === log.defenderId) || game.monsters.find(m => m.id === log.defenderId); if (defender) { setTimeout(() => { // Slight delay for cause-effect if (log.hitSuccess) { if (log.woundsCaused > 0) { // HIT and WOUND renderer.triggerDamageEffect(log.defenderId); renderer.showFloatingText(defender.x, defender.y, `💥 -${log.woundsCaused}`, '#ff0000'); } else { // BLOCKED (Hit but Toughness saved) renderer.showFloatingText(defender.x, defender.y, `🛡️ Block`, '#ffff00'); } } else { // MISS renderer.showFloatingText(defender.x, defender.y, `💨 Miss`, '#aaaaaa'); } }, 500); } }; game.onEntityMove = (entity, path) => { renderer.moveEntityAlongPath(entity, path); }; game.onEntityActive = (entityId, isActive) => { renderer.setEntityActive(entityId, isActive); }; game.onEntityHit = (entityId) => { renderer.triggerDamageEffect(entityId); }; game.onEntityDeath = (entityId) => { renderer.triggerDeathAnimation(entityId); }; game.onRangedTarget = (targetMonster, losResult) => { // 1. Draw Visuals renderer.showRangedTargeting(game.selectedEntity, targetMonster, losResult); // 2. UI if (targetMonster && losResult && losResult.clear) { ui.showRangedAttackUI(targetMonster); } else { ui.hideMonsterCard(); if (targetMonster && losResult && !losResult.clear && losResult.blocker) { let msg = 'Línea de visión bloqueada.'; if (losResult.blocker.type === 'hero') msg = `Bloqueado por aliado: ${losResult.blocker.entity.name}`; if (losResult.blocker.type === 'monster') msg = `Bloqueado por enemigo: ${losResult.blocker.entity.name}`; if (losResult.blocker.type === 'wall') msg = `Bloqueado por muro/obstáculo.`; ui.showTemporaryMessage('Objetivo Bloqueado', msg, 1500); } } }; game.onShowMessage = (title, message, duration) => { ui.showTemporaryMessage(title, message, duration); }; // game.onEntitySelect is now handled by UIManager to wrap the renderer call renderer.onHeroFinishedMove = (x, y) => { cameraManager.centerOn(x, y); }; // 5. Connect Generator State to UI generator.onStateChange = (state) => { if (state === 'PLACING_TILE') { ui.showPlacementControls(true); } else { ui.showPlacementControls(false); } }; generator.onPlacementUpdate = (preview) => { if (preview) { renderer.showPlacementPreview(preview); ui.updatePlacementStatus(preview.isValid); } else { renderer.hidePlacementPreview(); } }; generator.onDoorBlocked = (exitData) => { renderer.blockDoor(exitData); }; game.onPathChange = (path) => { renderer.updatePathVisualization(path); }; // 6. Handle Clicks const handleClick = (x, y, doorMesh) => { const currentPhase = game.turnManager.currentPhase; const hasActiveMonsters = game.monsters && game.monsters.some(m => !m.isDead); // PRIORITY 1: Tile Placement Mode - ignore all clicks if (generator.state === 'PLACING_TILE') { return; } // PRIORITY 2: Door Click (must be adjacent to player) if (doorMesh && doorMesh.userData.isDoor) { if (doorMesh.userData.isBlocked) { ui.showModal('¡Derrumbe!', 'Esta puerta está bloqueada por un derrumbe. No se puede pasar.'); return; } if (!doorMesh.userData.isOpen) { // CHECK PHASE: Exploration Only if (currentPhase !== 'exploration') { ui.showModal('Fase Incorrecta', 'Solo puedes explorar (abrir puertas) durante la Fase de Exploración.'); return; } // CHECK MONSTERS: Must be clear if (hasActiveMonsters) { ui.showModal('¡Peligro!', 'No puedes explorar mientras hay Monstruos cerca. ¡Acaba con ellos primero!'); return; } // 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 if (game.isLeaderAdjacentToDoor(doorMesh.userData.cells)) { // Open door visually renderer.openDoor(doorMesh); if (window.SOUND_MANAGER) window.SOUND_MANAGER.playSound('door_open'); // Get proper exit data with direction const exitData = doorMesh.userData.exitData; if (exitData) { generator.selectDoor(exitData); } else { console.error('[Main] Door missing exitData'); } } else { ui.showModal('Demasiado lejos', 'El Líder debe estar adyacente a la puerta para abrirla.'); } return; } } // PRIORITY 3: Normal cell click (player selection/movement) if (x !== null && y !== null) { // Restrict Hero Selection/Movement to Hero Phase (and verify logic in GameEngine handle selection) // Actually, we might want to select heroes in other phases to see stats, but MOVE only in Hero Phase. // GameEngine.planStep handles planning. // We let GameEngine handle selection. But for movement planning... // Let's modify onCellClick inside GameEngine or just block here? // Blocking execution is safer. // Wait, onCellClick handles Selection AND Planning. // We'll let it select. But we hook executeMovePath separately. game.onCellClick(x, y); } }; renderer.setupInteraction( () => cameraManager.getCamera(), handleClick, () => { // Right Click Handler game.executeMovePath(); } ); // 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"); } } }); game.onEventTriggered = (eventResult) => { if (eventResult) { if (eventResult.type === 'MONSTER_SPAWN') { const count = eventResult.count || 0; ui.showModal('¡Emboscada!', `Al entrar en la estancia, aparecen ${count} Enemigos!
Tu movimiento se detiene.`); } else if (eventResult.message) { ui.showModal('Zona Explorada', `${eventResult.message}
Tu movimiento se detiene.`); } } }; // 7. Start game.startMission(mission); // Mark initial tile as visited to prevent immediate trigger if (game.heroes && game.heroes.length > 0) { const h = game.heroes[0]; const initialTileId = game.dungeon.grid.occupiedCells.get(`${h.x},${h.y}`); if (initialTileId) { game.visitedRoomIds.add(initialTileId); console.log(`[Main] Initial tile ${initialTileId} marked as visited.`); } } // 8. Render Loop const animate = (time) => { requestAnimationFrame(animate); game.update(time); cameraManager.update(time); renderer.updateAnimations(time); renderer.render(cameraManager.getCamera()); }; animate(0);