Files
WarhammerQuest/src/main.js

348 lines
12 KiB
JavaScript

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.<br>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.<br>Preparaos para la <b>Fase de Poder</b> del siguiente turno.',
() => {
// Combat Loop: Power -> Hero -> Monster -> (Skip Exp) -> Power...
game.turnManager.endTurn();
}
);
} else {
ui.showModal('Zona Despejada',
'Fase de Monstruos Finalizada.<br>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
renderer.showCombatFeedback(defender.x, defender.y, log.woundsCaused, log.hitSuccess);
}, 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 <b>Fase de Exploración</b>.');
return;
}
// CHECK MONSTERS: Must be clear
if (hasActiveMonsters) {
ui.showModal('¡Peligro!', 'No puedes explorar mientras hay <b>Monstruos</b> 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 <b>Líder (Portador de la Lámpara)</b> para abrir la puerta.');
return;
}
if (!selectedHero.hasLantern) {
ui.showModal('Acción no permitida', `<b>${selectedHero.name}</b> no lleva la Lámpara. Solo el <b>Líder</b> 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 <b>adyacente</b> 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
if (game.targetingMode === 'spell' || game.targetingMode === 'ranged') {
game.cancelTargeting();
if (window.RENDERER) window.RENDERER.hideAreaPreview();
ui.showTemporaryMessage('Cancelado', 'Lanzamiento de hechizo cancelado.', 1000);
return;
}
game.executeMovePath();
},
(x, y) => {
if (game.onCellHover) game.onCellHover(x, y);
}
);
// 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 <b>${count} Enemigos</b>!<br>Tu movimiento se detiene.`);
} else if (eventResult.message) {
ui.showModal('Zona Explorada', `${eventResult.message}<br>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);