Sesión 16: Refinado de reglas de exploración y eventos, restauración de aleatoriedad y corrección de colapsos.
This commit is contained in:
34
DEVLOG.md
34
DEVLOG.md
@@ -1,5 +1,39 @@
|
||||
# Devlog - Warhammer Quest (Versión Web 3D)
|
||||
|
||||
## Sesión 16: Reglas de Exploración y Refinado de Eventos
|
||||
**Fecha:** 11 de Enero de 2026
|
||||
|
||||
### Objetivos
|
||||
- Ajustar la progresión de la fase de exploración según el manual original (1995).
|
||||
- Refinar el evento de "Derrumbamiento" para permitir una huida táctica.
|
||||
- Restaurar la aleatoriedad total en los mazos de juego (Eventos y Mazmorra).
|
||||
|
||||
### Cambios Realizados
|
||||
|
||||
#### 1. Exploración Diferida
|
||||
- **Revelación en Fase de Héroes**: Ahora los héroes pueden entrar en estancias nuevas durante su fase. La habitación se marca como "visitada", pero no se genera el encuentro inmediatamente.
|
||||
- **Resolución en Fase de Monstruos**: La carta de evento se roba al inicio de la Fase de Monstruos, antes de que estos actúen.
|
||||
- **Movimiento Completo**: Los aventureros pueden completar todo su movimiento al entrar en una sala nueva (no se bloquean en la primera casilla), pero su turno termina al llegar a su destino.
|
||||
- **Finalización Manual**: Se ha eliminado el salto automático de turno al entrar en una sala, permitiendo al jugador gestionar el orden de sus héroes manualmente antes de pasar al siguiente.
|
||||
|
||||
#### 2. Refinado del "Derrumbamiento" (Collapse)
|
||||
- **Margen de Huida**: Se ha ajustado el contador de colapso a 2 turnos para dar tiempo real a los héroes a salir de la estancia.
|
||||
- **Exención de Pinning**: Siguiendo el reglamento, los héroes no pueden ser trabados en combate mientras la habitación se derrumba (pueden huir ignorando a los monstruos).
|
||||
- **Zonas Intransitables**: Una vez colapsada, la estancia se marca físicamente como bloqueada en la cuadrícula, impidiendo cualquier re-entrada.
|
||||
|
||||
#### 3. Restauración de Aleatoriedad
|
||||
- **Mazo de Eventos**: Eliminados los "cheats" de desarrollo que forzaban el Enano y el Rastrillo. Ahora el mazo es 100% aleatorio.
|
||||
- **Mazo de Mazmorra**: Reintroducidas todas las secciones especiales (esquinas, cruces en T, escaleras). La composición del mazo vuelve a ser equilibrada y variada.
|
||||
|
||||
#### 4. Correcciones y Mejoras Técnicas
|
||||
- **Fix tile_0**: La habitación inicial se marca como explorada por defecto para evitar disparos de eventos fantasmas al inicio.
|
||||
- **Corregida lógica de nombres**: Se ha arreglado un error donde el nombre de la carta robada no se mostraba correctamente en los logs.
|
||||
- **Restaurada GridSystem**: Corregido un error que impedía colocar tiles tras la última actualización.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## Sesión 15: Evento del Rastrillo, Llave del Enano e Inventario
|
||||
**Fecha:** 10 de Enero de 2026
|
||||
|
||||
|
||||
BIN
public/assets/sfx/gate_chains_close.mp3
Normal file
BIN
public/assets/sfx/gate_chains_close.mp3
Normal file
Binary file not shown.
BIN
public/assets/sfx/gate_chains_open.mp3
Normal file
BIN
public/assets/sfx/gate_chains_open.mp3
Normal file
Binary file not shown.
@@ -17,30 +17,5 @@ const shuffleDeck = (deck) => {
|
||||
[deck[i], deck[j]] = [deck[j], deck[i]];
|
||||
}
|
||||
|
||||
// DEBUG: Force ENANO and RASTRILLO to TOP
|
||||
const enanoIdx = deck.findIndex(c => c.id === 'evt_enano_moribundo');
|
||||
const rastrilloIdx = deck.findIndex(c => c.id === 'evt_rastrillo');
|
||||
|
||||
// Reverse order for unshift
|
||||
if (rastrilloIdx !== -1) {
|
||||
const card = deck.splice(rastrilloIdx, 1)[0];
|
||||
deck.unshift(card);
|
||||
}
|
||||
if (enanoIdx !== -1) {
|
||||
const card = deck.splice(enanoIdx, 1)[0];
|
||||
deck.unshift(card);
|
||||
console.log("DEBUG: Forced ENANO MORIBUNDO and RASTRILLO to top of deck.");
|
||||
}
|
||||
|
||||
// DEBUG: Force Chaos Warrior to TOP
|
||||
/*
|
||||
const cwIdx = deck.findIndex(c => c.id === 'mon_chaosWarrior');
|
||||
if (cwIdx !== -1) {
|
||||
const card = deck.splice(cwIdx, 1)[0];
|
||||
deck.unshift(card);
|
||||
console.log("DEBUG: Forced CHAOS WARRIOR to top of deck.");
|
||||
}
|
||||
*/
|
||||
|
||||
return deck;
|
||||
};
|
||||
|
||||
@@ -15,9 +15,11 @@ export class DungeonDeck {
|
||||
// 1. Create a "Pool" of standard dungeon tiles
|
||||
let pool = [];
|
||||
const composition = [
|
||||
{ id: 'room_dungeon', count: 18 }, // Rigged: Only Rooms
|
||||
{ id: 'corridor_straight', count: 0 },
|
||||
{ id: 'junction_t', count: 0 }
|
||||
{ id: 'room_dungeon', count: 12 },
|
||||
{ id: 'corridor_straight', count: 8 },
|
||||
{ id: 'corridor_steps', count: 4 },
|
||||
{ id: 'corridor_corner', count: 4 },
|
||||
{ id: 'junction_t', count: 4 }
|
||||
];
|
||||
|
||||
composition.forEach(item => {
|
||||
@@ -44,23 +46,19 @@ export class DungeonDeck {
|
||||
return drawn;
|
||||
};
|
||||
|
||||
// --- Step 1 & 2: Bottom Pool ---
|
||||
// --- Step 1 & 2: Bottom Pool (6 random tiles + Objective) ---
|
||||
const bottomPool = drawRandom(pool, 6);
|
||||
|
||||
const objectiveDef = TILES[objectiveTileId];
|
||||
if (objectiveDef) {
|
||||
bottomPool.push(objectiveDef);
|
||||
} else {
|
||||
console.error("Objective Tile ID not found:", objectiveTileId);
|
||||
}
|
||||
|
||||
this.shuffleArray(bottomPool);
|
||||
|
||||
// --- Step 4: Top Pool ---
|
||||
const topPool = drawRandom(pool, 6);
|
||||
// --- Step 3: Top Pool (All remaining tiles in the pool) ---
|
||||
const topPool = [...pool]; // pool already has those 6 removed by drawRandom
|
||||
this.shuffleArray(topPool);
|
||||
|
||||
// --- Step 5: Stack ---
|
||||
// --- Step 4: Final Stack ---
|
||||
this.cards = [...topPool, ...bottomPool];
|
||||
|
||||
|
||||
|
||||
@@ -13,6 +13,9 @@ export class GridSystem {
|
||||
// Set of "x,y" strings that are door/exit cells (can cross room boundaries)
|
||||
this.doorCells = new Set();
|
||||
|
||||
// Set of "x,y" strings that are blocked (e.g. collapsed)
|
||||
this.blockedCells = new Set();
|
||||
|
||||
this.tiles = [];
|
||||
}
|
||||
|
||||
@@ -141,7 +144,12 @@ export class GridSystem {
|
||||
* Helper to see if a specific global coordinate is occupied
|
||||
*/
|
||||
isOccupied(x, y) {
|
||||
return this.occupiedCells.has(`${x},${y}`);
|
||||
const key = `${x},${y}`;
|
||||
return this.occupiedCells.has(key) && !this.blockedCells.has(key);
|
||||
}
|
||||
|
||||
isBlocked(x, y) {
|
||||
return this.blockedCells.has(`${x},${y}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -162,8 +170,9 @@ export class GridSystem {
|
||||
const data1 = this.cellData.get(key1);
|
||||
const data2 = this.cellData.get(key2);
|
||||
|
||||
// Both cells must exist
|
||||
// Both cells must exist and not be blocked
|
||||
if (!data1 || !data2) return false;
|
||||
if (this.blockedCells.has(key1) || this.blockedCells.has(key2)) return false;
|
||||
|
||||
const sameTile = data1.tileId === data2.tileId;
|
||||
const isDoor1 = this.doorCells.has(key1);
|
||||
|
||||
@@ -275,9 +275,11 @@ export class EventInterpreter {
|
||||
h.inventory.push(action.id_item);
|
||||
if (this.game.onEntityUpdate) this.game.onEntityUpdate(h);
|
||||
});
|
||||
await this.log("Hallazgo", `<b>${msg}</b>: ${targets.map(t => t.name).join(", ")} obtiene <b>${action.id_item}</b>.`);
|
||||
|
||||
if (this.game.onShowMessage) {
|
||||
// One prominent message. The log will also receive it if main.js is configured to mirror it or if we call log separately.
|
||||
// Let's call log with a type that ensures it's NOT a popup, and use onShowMessage for the popup.
|
||||
await this.log("Efecto", `${targets.map(t => t.name).join(", ")} obtiene <b>${action.id_item}</b>.`);
|
||||
this.game.onShowMessage("OBJETO", `<b>${msg}</b>`);
|
||||
}
|
||||
}
|
||||
@@ -312,9 +314,11 @@ export class EventInterpreter {
|
||||
contextTileId = this.game.currentEventContext.tileId;
|
||||
}
|
||||
|
||||
const skip = this.game.currentEventContext && this.game.currentEventContext.source === 'exploration';
|
||||
|
||||
const spots = this.game.findSpawnPoints(count, contextTileId);
|
||||
spots.forEach(spot => {
|
||||
this.game.spawnMonster(def, spot.x, spot.y, { skipTurn: false });
|
||||
this.game.spawnMonster(def, spot.x, spot.y, { skipTurn: skip });
|
||||
});
|
||||
|
||||
// KEEP MODAL for Spawn - it's a major event that requires immediate player attention
|
||||
|
||||
@@ -27,6 +27,8 @@ export class GameEngine {
|
||||
this.visitedRoomIds = new Set(); // Track tiles triggered
|
||||
this.eventDeck = createEventDeck();
|
||||
this.lastEntranceUsed = null;
|
||||
this.pendingExploration = null;
|
||||
this.exploredRoomIds = new Set();
|
||||
|
||||
// Callbacks
|
||||
this.onEntityUpdate = null;
|
||||
@@ -46,7 +48,10 @@ export class GameEngine {
|
||||
|
||||
this.dungeon.startDungeon(missionConfig);
|
||||
|
||||
// Create Party (4 Heroes)
|
||||
// Starting room is already explored
|
||||
this.exploredRoomIds.add('tile_0');
|
||||
this.visitedRoomIds.add('tile_0');
|
||||
|
||||
// Create Party (4 Heroes)
|
||||
this.createParty();
|
||||
|
||||
@@ -84,7 +89,7 @@ export class GameEngine {
|
||||
if (data.eventTriggered) {
|
||||
console.log("[GameEngine] Power Event Triggered! Waiting to handle...");
|
||||
// Determine if we need to draw a card or if it's a specific message
|
||||
setTimeout(() => this.handlePowerEvent(), 1500);
|
||||
setTimeout(() => this.handlePowerEvent({ source: 'power' }), 1500);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -640,6 +645,12 @@ export class GameEngine {
|
||||
// If already escaped this turn, not pinned
|
||||
if (entity.hasEscapedPin) return false;
|
||||
|
||||
// RULE: No pinning in a collapsing room (Panic/Rubble distraction)
|
||||
if (this.state && this.state.collapsingRoom) {
|
||||
const tileId = this.dungeon.grid.occupiedCells.get(`${entity.x},${entity.y}`);
|
||||
if (tileId === this.state.collapsingRoom.tileId) return false;
|
||||
}
|
||||
|
||||
return this.monsters.some(m => {
|
||||
if (m.isDead) return false;
|
||||
const dx = Math.abs(entity.x - m.x);
|
||||
@@ -791,63 +802,43 @@ export class GameEngine {
|
||||
// 2. Check for New Tile Entry
|
||||
const tileId = this.dungeon.grid.occupiedCells.get(`${step.x},${step.y}`);
|
||||
|
||||
if (tileId && !this.visitedRoomIds.has(tileId)) {
|
||||
|
||||
// Mark as visited immediatley
|
||||
this.visitedRoomIds.add(tileId);
|
||||
|
||||
// Check Tile Type (Room vs Corridor)
|
||||
if (tileId) {
|
||||
const tileInfo = this.dungeon.placedTiles.find(t => t.id === tileId);
|
||||
const isRoom = tileInfo && (tileInfo.defId.startsWith('room') || tileInfo.defId.includes('objective'));
|
||||
const isUnexploredRoom = isRoom && !this.exploredRoomIds.has(tileId);
|
||||
|
||||
if (isRoom) {
|
||||
console.log(`[GameEngine] Hero entered NEW ROOM: ${tileId}`);
|
||||
|
||||
triggeredEvents = true; // Stop movement forces end of hero action
|
||||
entity.currentMoves = 0;
|
||||
|
||||
// Send PARTIAL path to renderer (from 0 to current step i+1)
|
||||
if (this.onEntityMove) {
|
||||
this.onEntityMove(entity, fullPath.slice(0, i + 1));
|
||||
if (isUnexploredRoom) {
|
||||
if (!this.pendingExploration) {
|
||||
console.log(`[GameEngine] First hero ${entity.name} entered UNEXPLORED ROOM: ${tileId}`);
|
||||
if (this.onShowMessage) this.onShowMessage("¡Estancia Revelada!", "Preparando encuentro...", 2000);
|
||||
this.pendingExploration = { tileId: tileId, source: 'exploration' };
|
||||
}
|
||||
|
||||
if (this.onShowMessage) this.onShowMessage("¡Estancia Revelada!", "Explorando...", 2000);
|
||||
|
||||
// IMMEDIATE EVENT RESOLUTION
|
||||
this.handlePowerEvent({ tileId: tileId }, () => {
|
||||
console.log("[GameEngine] Room Event Resolved.");
|
||||
|
||||
// Check for Monsters
|
||||
const hasMonsters = this.monsters.some(m => !m.isDead);
|
||||
if (hasMonsters) {
|
||||
console.log("[GameEngine] Monsters Spawned! Ending Hero Phase.");
|
||||
this.turnManager.setPhase('monster');
|
||||
} else {
|
||||
console.log("[GameEngine] No Monsters. Staying in Hero Phase.");
|
||||
}
|
||||
});
|
||||
|
||||
break; // Stop loop
|
||||
} else {
|
||||
console.log(`[GameEngine] Hero entered Corridor: ${tileId} (No Stop)`);
|
||||
triggeredEvents = true; // Use this flag to end turn AFTER movement
|
||||
} else if (!this.visitedRoomIds.has(tileId)) {
|
||||
this.visitedRoomIds.add(tileId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If NO interruption, send full path
|
||||
if (!triggeredEvents) {
|
||||
if (this.onEntityMove) {
|
||||
this.onEntityMove(entity, fullPath);
|
||||
}
|
||||
// Always send full path to renderer since we no longer interrupt movement
|
||||
if (this.onEntityMove) {
|
||||
this.onEntityMove(entity, fullPath);
|
||||
}
|
||||
|
||||
// Deduct Moves
|
||||
if (entity.currentMoves !== undefined) {
|
||||
// Only deduct steps actually taken. No penalty.
|
||||
entity.currentMoves -= stepsTaken;
|
||||
if (entity.currentMoves < 0) entity.currentMoves = 0;
|
||||
// If we entered a new room, moves drop to 0 immediately upon completion.
|
||||
if (triggeredEvents) {
|
||||
entity.currentMoves = 0;
|
||||
} else {
|
||||
entity.currentMoves -= stepsTaken;
|
||||
if (entity.currentMoves < 0) entity.currentMoves = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Notify UI of move change
|
||||
if (this.onEntityUpdate) this.onEntityUpdate(entity);
|
||||
|
||||
// AUTO-DESELECT LOGIC
|
||||
// In Hero Phase, we want to KEEP the active hero selected to avoid re-selecting.
|
||||
const isHeroPhase = this.turnManager.currentPhase === 'hero';
|
||||
@@ -909,6 +900,9 @@ export class GameEngine {
|
||||
|
||||
const [x, y] = key.split(',').map(Number);
|
||||
|
||||
// Check if cell is blocked (collapsed)
|
||||
if (this.dungeon.grid.blockedCells.has(key)) continue;
|
||||
|
||||
// Check Collision: Do not spawn on Heroes or existing Monsters
|
||||
const isHero = this.heroes.some(h => h.x === x && h.y === y);
|
||||
const isMonster = this.monsters.some(m => m.x === x && m.y === y && !m.isDead);
|
||||
@@ -1024,6 +1018,18 @@ export class GameEngine {
|
||||
// =========================================
|
||||
|
||||
async playMonsterTurn() {
|
||||
// 1. Resolve pending exploration from Hero Phase
|
||||
if (this.pendingExploration) {
|
||||
const context = { ...this.pendingExploration };
|
||||
this.pendingExploration = null;
|
||||
|
||||
console.log("[GameEngine] Resolving pending exploration at start of Monster Phase.");
|
||||
await new Promise(resolve => {
|
||||
this.handlePowerEvent(context, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Execute AI for existing/new monsters
|
||||
if (this.ai) {
|
||||
await this.ai.executeTurn();
|
||||
}
|
||||
@@ -1373,16 +1379,20 @@ export class GameEngine {
|
||||
// (Maybe put back in deck? Rules say 'ignore and draw another immediately', usually means discard this one)
|
||||
if (this.eventDeck.length === 0) this.eventDeck = createEventDeck();
|
||||
card = this.eventDeck.shift();
|
||||
console.log(`[GameEngine] Redrawn Card: ${card.name}`);
|
||||
console.log(`[GameEngine] Redrawn Card: ${card.titulo}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[GameEngine] Drawn Card: ${card.name}`, card);
|
||||
console.log(`[GameEngine] Drawn Card: ${card.titulo}`, card);
|
||||
|
||||
// Delegate execution to the modular interpreter
|
||||
if (this.events) {
|
||||
this.events.processEvent(card, () => {
|
||||
this.currentEventContext = null;
|
||||
// Mark room as explored if it was an exploration source
|
||||
if (context && context.tileId) {
|
||||
this.exploredRoomIds.add(context.tileId);
|
||||
}
|
||||
if (onComplete) onComplete();
|
||||
else this.turnManager.resumeFromEvent();
|
||||
});
|
||||
@@ -1402,7 +1412,13 @@ export class GameEngine {
|
||||
console.log(`[GameEngine] Collapsing Room Timer: ${this.state.collapsingRoom.turnsLeft}`);
|
||||
|
||||
if (this.state.collapsingRoom.turnsLeft > 0) {
|
||||
if (this.onShowMessage) this.onShowMessage("¡PELIGRO!", `El techo cruje... ¡Queda ${this.state.collapsingRoom.turnsLeft} turno para salir!`, 4000);
|
||||
const msg = this.state.collapsingRoom.turnsLeft === 1 ?
|
||||
"¡ÚLTIMO AVISO! El techo está a punto de ceder..." :
|
||||
`El techo cruje peligrosamente... Tenéis ${this.state.collapsingRoom.turnsLeft} turnos para salir.`;
|
||||
|
||||
if (this.onShowMessage) {
|
||||
this.onShowMessage("¡PELIGRO!", msg, 5000);
|
||||
}
|
||||
} else {
|
||||
// TIME'S UP - KILL EVERYONE IN ROOM
|
||||
this.killEntitiesInCollapsingRoom(this.state.collapsingRoom.tileId);
|
||||
@@ -1451,7 +1467,18 @@ export class GameEngine {
|
||||
if (this.onEntityDeath) this.onEntityDeath(e.id);
|
||||
});
|
||||
|
||||
// Clear state so it doesn't kill again (optional, room is gone)
|
||||
// Mark room as INTRANSITABLE (Blocked cells)
|
||||
for (const [key, tid] of this.dungeon.grid.occupiedCells.entries()) {
|
||||
if (tid === tileId) {
|
||||
this.dungeon.grid.blockedCells.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.onShowMessage) {
|
||||
this.onShowMessage("¡DERRUMBE TOTAL!", "La estancia ha colapsado. Ahora es un montón de escombros intransitable.");
|
||||
}
|
||||
|
||||
// Clear state so it doesn't kill again
|
||||
this.state.collapsingRoom = null;
|
||||
}
|
||||
collapseExits() {
|
||||
@@ -1473,7 +1500,7 @@ export class GameEngine {
|
||||
if (!this.state) this.state = {};
|
||||
this.state.collapsingRoom = {
|
||||
tileId: currentTileId,
|
||||
turnsLeft: 1 // Next monster phase ends turn -> Next power phase -> Next monster phase = Death
|
||||
turnsLeft: 2 // Gives them Turn N + Turn N+1 to escape.
|
||||
};
|
||||
|
||||
// 2. Scan Available Exits to see which align with this Room
|
||||
@@ -1502,9 +1529,13 @@ export class GameEngine {
|
||||
|
||||
if (isAdjacent) {
|
||||
exitsToRemove.push(index);
|
||||
const exitKey = `${exit.x},${exit.y}`;
|
||||
|
||||
this.placeEventMarker("escombros", exit.x, exit.y);
|
||||
blockedLocations.add(`${exit.x},${exit.y}`);
|
||||
blockedLocations.add(exitKey);
|
||||
|
||||
// Immediately block movement through these cells
|
||||
this.dungeon.grid.blockedCells.add(exitKey);
|
||||
|
||||
// Also block the door visually in Renderer
|
||||
if (window.RENDERER) {
|
||||
@@ -1563,6 +1594,9 @@ export class GameEngine {
|
||||
blockPortcullisAtEntrance() {
|
||||
if (this.lastEntranceUsed && window.RENDERER) {
|
||||
window.RENDERER.blockDoorWithPortcullis(this.lastEntranceUsed);
|
||||
if (window.SOUND_MANAGER) {
|
||||
window.SOUND_MANAGER.playSound('gate_chains');
|
||||
}
|
||||
if (this.onShowMessage) {
|
||||
this.onShowMessage("¡RASTRILLO!", "Un pesado rastrillo de hierro cae a vuestras espaldas, bloqueando la entrada.");
|
||||
}
|
||||
|
||||
@@ -61,8 +61,7 @@ export class TurnManager {
|
||||
}
|
||||
|
||||
rollPowerDice() {
|
||||
// const roll = Math.floor(Math.random() * 6) + 1;
|
||||
const roll = 1; // DEBUG: Force Event for testing
|
||||
const roll = Math.floor(Math.random() * 6) + 1;
|
||||
this.currentPowerRoll = roll;
|
||||
console.log(`Power Roll: ${roll}`);
|
||||
|
||||
|
||||
24
src/main.js
24
src/main.js
@@ -189,11 +189,17 @@ game.onRangedTarget = (targetMonster, losResult) => {
|
||||
|
||||
game.onShowMessage = (title, message, duration) => {
|
||||
// Filter specific game flow messages to Log instead of popup
|
||||
if (title.startsWith('Turno de') || title.includes('Fase') || title.includes('Efecto') || title.includes('Evento')) {
|
||||
const lowerTitle = title.toLowerCase();
|
||||
if (title.startsWith('Turno de') ||
|
||||
lowerTitle.includes('fase') ||
|
||||
lowerTitle.includes('efecto') ||
|
||||
lowerTitle.includes('evento') ||
|
||||
lowerTitle.includes('selección')) {
|
||||
|
||||
let icon = '👉';
|
||||
let type = 'system';
|
||||
|
||||
if (title.includes('Evento')) {
|
||||
if (lowerTitle.includes('evento')) {
|
||||
icon = '⚡';
|
||||
type = 'event-log';
|
||||
}
|
||||
@@ -290,6 +296,8 @@ const handleClick = (x, y, doorMesh) => {
|
||||
// 2. Check Adjacency
|
||||
if (game.isLeaderAdjacentToDoor(doorMesh.userData.cells)) {
|
||||
|
||||
const wasPortcullis = doorMesh.userData.isPortcullis;
|
||||
|
||||
// 3. Check Key Requirement for Portcullis
|
||||
if (doorMesh.userData.requiresKey) {
|
||||
const hasKey = game.heroes.some(h => h.inventory && h.inventory.includes('llave_rastrillo'));
|
||||
@@ -298,12 +306,22 @@ const handleClick = (x, y, doorMesh) => {
|
||||
return;
|
||||
} else {
|
||||
ui.showModal('¡Rastrillo Abierto!', 'Utilizáis la llave del enano para levantar el pesado rastrillo.');
|
||||
// Clear flags so renderer allows opening
|
||||
doorMesh.userData.requiresKey = false;
|
||||
doorMesh.userData.isPortcullis = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Open door visually
|
||||
renderer.openDoor(doorMesh);
|
||||
if (window.SOUND_MANAGER) window.SOUND_MANAGER.playSound('door_open');
|
||||
|
||||
if (window.SOUND_MANAGER) {
|
||||
if (wasPortcullis) {
|
||||
window.SOUND_MANAGER.playSound('gate_chains');
|
||||
} else {
|
||||
window.SOUND_MANAGER.playSound('door_open');
|
||||
}
|
||||
}
|
||||
|
||||
// Get proper exit data with direction
|
||||
const exitData = doorMesh.userData.exitData;
|
||||
|
||||
@@ -15,7 +15,8 @@ export class SoundManager {
|
||||
'door_open': '/assets/sfx/opendoor.mp3',
|
||||
'footsteps': '/assets/sfx/footsteps.mp3',
|
||||
'sword': '/assets/sfx/sword1.mp3',
|
||||
'arrow': '/assets/sfx/arrow.mp3'
|
||||
'arrow': '/assets/sfx/arrow.mp3',
|
||||
'gate_chains': '/assets/sfx/gate_chains_open.mp3'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -22,11 +22,17 @@ export class EntityRenderer {
|
||||
addEntity(entity) {
|
||||
if (this.entities.has(entity.id)) return;
|
||||
|
||||
// Mark as "loading" or "reserved" to prevent race conditions
|
||||
this.entities.set(entity.id, 'PENDING');
|
||||
|
||||
const w = 1.04;
|
||||
const h = 1.56;
|
||||
const geometry = new THREE.PlaneGeometry(w, h);
|
||||
|
||||
this.getTexture(entity.texturePath, (texture) => {
|
||||
// Check if we were removed while loading
|
||||
if (!this.entities.has(entity.id)) return;
|
||||
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
map: texture,
|
||||
transparent: true,
|
||||
@@ -129,14 +135,14 @@ export class EntityRenderer {
|
||||
|
||||
moveEntityAlongPath(entity, path) {
|
||||
const mesh = this.entities.get(entity.id);
|
||||
if (mesh) {
|
||||
if (mesh instanceof THREE.Object3D) {
|
||||
mesh.userData.pathQueue = [...path];
|
||||
}
|
||||
}
|
||||
|
||||
updateEntityPosition(entity) {
|
||||
const mesh = this.entities.get(entity.id);
|
||||
if (mesh) {
|
||||
if (mesh instanceof THREE.Object3D) {
|
||||
if (mesh.userData.isMoving || mesh.userData.pathQueue.length > 0) return;
|
||||
mesh.position.set(entity.x, 1.56 / 2, -entity.y);
|
||||
|
||||
@@ -151,7 +157,7 @@ export class EntityRenderer {
|
||||
|
||||
toggleEntitySelection(entityId, isSelected) {
|
||||
const mesh = this.entities.get(entityId);
|
||||
if (mesh) {
|
||||
if (mesh instanceof THREE.Object3D) {
|
||||
const ring = mesh.getObjectByName("SelectionRing");
|
||||
if (ring) ring.visible = isSelected;
|
||||
}
|
||||
@@ -159,7 +165,7 @@ export class EntityRenderer {
|
||||
|
||||
setEntityActive(entityId, isActive) {
|
||||
const mesh = this.entities.get(entityId);
|
||||
if (!mesh) return;
|
||||
if (!(mesh instanceof THREE.Object3D)) return;
|
||||
|
||||
const oldRing = mesh.getObjectByName("ActiveRing");
|
||||
if (oldRing) mesh.remove(oldRing);
|
||||
@@ -183,7 +189,7 @@ export class EntityRenderer {
|
||||
|
||||
setEntityTarget(entityId, isTarget) {
|
||||
const mesh = this.entities.get(entityId);
|
||||
if (!mesh) return;
|
||||
if (!(mesh instanceof THREE.Object3D)) return;
|
||||
|
||||
const oldRing = mesh.getObjectByName("TargetRing");
|
||||
if (oldRing) mesh.remove(oldRing);
|
||||
@@ -217,7 +223,7 @@ export class EntityRenderer {
|
||||
|
||||
triggerDamageEffect(entityId) {
|
||||
const mesh = this.entities.get(entityId);
|
||||
if (!mesh) return;
|
||||
if (!(mesh instanceof THREE.Object3D)) return;
|
||||
|
||||
mesh.traverse((child) => {
|
||||
if (child.material && child.material.map) {
|
||||
@@ -245,7 +251,7 @@ export class EntityRenderer {
|
||||
|
||||
triggerDeathAnimation(entityId) {
|
||||
const mesh = this.entities.get(entityId);
|
||||
if (!mesh) return;
|
||||
if (!(mesh instanceof THREE.Object3D)) return;
|
||||
|
||||
const startTime = performance.now();
|
||||
const duration = 1500;
|
||||
@@ -272,6 +278,7 @@ export class EntityRenderer {
|
||||
let isAnyMoving = false;
|
||||
|
||||
this.entities.forEach((mesh, id) => {
|
||||
if (!(mesh instanceof THREE.Object3D)) return;
|
||||
const data = mesh.userData;
|
||||
|
||||
if (!data.isMoving && data.pathQueue.length > 0) {
|
||||
|
||||
Reference in New Issue
Block a user