diff --git a/DEVLOG.md b/DEVLOG.md
index e9039ab..9461a9a 100644
--- a/DEVLOG.md
+++ b/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
diff --git a/public/assets/sfx/gate_chains_close.mp3 b/public/assets/sfx/gate_chains_close.mp3
new file mode 100644
index 0000000..20c6513
Binary files /dev/null and b/public/assets/sfx/gate_chains_close.mp3 differ
diff --git a/public/assets/sfx/gate_chains_open.mp3 b/public/assets/sfx/gate_chains_open.mp3
new file mode 100644
index 0000000..d7e649b
Binary files /dev/null and b/public/assets/sfx/gate_chains_open.mp3 differ
diff --git a/src/engine/data/Events.js b/src/engine/data/Events.js
index 6686997..c9cd7a0 100644
--- a/src/engine/data/Events.js
+++ b/src/engine/data/Events.js
@@ -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;
};
diff --git a/src/engine/dungeon/DungeonDeck.js b/src/engine/dungeon/DungeonDeck.js
index 49ad4df..8a23a03 100644
--- a/src/engine/dungeon/DungeonDeck.js
+++ b/src/engine/dungeon/DungeonDeck.js
@@ -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];
diff --git a/src/engine/dungeon/GridSystem.js b/src/engine/dungeon/GridSystem.js
index da3cb7c..f15ed37 100644
--- a/src/engine/dungeon/GridSystem.js
+++ b/src/engine/dungeon/GridSystem.js
@@ -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);
diff --git a/src/engine/events/EventInterpreter.js b/src/engine/events/EventInterpreter.js
index 39cf54e..9199775 100644
--- a/src/engine/events/EventInterpreter.js
+++ b/src/engine/events/EventInterpreter.js
@@ -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", `${msg}: ${targets.map(t => t.name).join(", ")} obtiene ${action.id_item}.`);
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 ${action.id_item}.`);
this.game.onShowMessage("OBJETO", `${msg}`);
}
}
@@ -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
diff --git a/src/engine/game/GameEngine.js b/src/engine/game/GameEngine.js
index 01cd463..fc85a83 100644
--- a/src/engine/game/GameEngine.js
+++ b/src/engine/game/GameEngine.js
@@ -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.");
}
diff --git a/src/engine/game/TurnManager.js b/src/engine/game/TurnManager.js
index 160dde1..f00b891 100644
--- a/src/engine/game/TurnManager.js
+++ b/src/engine/game/TurnManager.js
@@ -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}`);
diff --git a/src/main.js b/src/main.js
index 73464e2..6d14580 100644
--- a/src/main.js
+++ b/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;
diff --git a/src/view/SoundManager.js b/src/view/SoundManager.js
index 616407a..5c21f59 100644
--- a/src/view/SoundManager.js
+++ b/src/view/SoundManager.js
@@ -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'
}
};
diff --git a/src/view/render/EntityRenderer.js b/src/view/render/EntityRenderer.js
index bfcc2c2..fa18edf 100644
--- a/src/view/render/EntityRenderer.js
+++ b/src/view/render/EntityRenderer.js
@@ -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) {