348 lines
12 KiB
JavaScript
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);
|
|
|
|
|