Feat: Interactive doors with physical wall cutouts

- Implemented door selection and interaction model (walk-to + click).
- Added modal for opening doors.
- Refactored wall rendering to create physical holes (CSG-like wall segments).
- Aligned door meshes to perfectly fit wall cutouts.
- Managed door visibility states to prevent Z-fighting on open doors.
This commit is contained in:
2025-12-23 13:50:05 +01:00
parent 3c599093cf
commit 0e5b885236
4 changed files with 515 additions and 60 deletions

View File

@@ -12,13 +12,22 @@
<div id="app"></div>
<div id="hud">
<div id="minimap-container">
<canvas id="minimap" width="200" height="200"></canvas>
<canvas id="minimap"></canvas>
</div>
<div id="compass">
<div id="compass-n" class="compass-btn active" data-direction="N">N</div>
<div id="compass-s" class="compass-btn" data-direction="S">S</div>
<div id="compass-e" class="compass-btn" data-direction="E">E</div>
<div id="compass-w" class="compass-btn" data-direction="W">W</div>
<div class="compass-btn" data-dir="N">N</div>
<div class="compass-row">
<div class="compass-btn" data-dir="W">W</div>
<div class="compass-btn" data-dir="E">E</div>
</div>
<div class="compass-btn" data-dir="S">S</div>
</div>
<div id="door-modal" class="hidden">
<div class="modal-content">
<p>¿Quieres abrir la puerta?</p>
<button id="btn-open-yes"></button>
<button id="btn-open-no">No</button>
</div>
</div>
</div>
<script type="module" src="/src/main.js"></script>

View File

@@ -206,19 +206,14 @@ function generateDungeon() {
// E/W: Alineados en Y -> puerta en Y relativo es igual para ambos.
const doorConfig = dir.side === 'N' || dir.side === 'S'
? { side: dir.side, gridX: doorGridPos, leadsTo: newRoomId }
: { side: dir.side, gridY: doorGridPos, leadsTo: newRoomId };
? { side: dir.side, gridX: doorGridPos, leadsTo: newRoomId, isOpen: false, id: `door_${currentRoom.id}_to_${newRoomId}` }
: { side: dir.side, gridY: doorGridPos, leadsTo: newRoomId, isOpen: false, id: `door_${currentRoom.id}_to_${newRoomId}` };
currentRoom.doors.push(doorConfig);
// Puerta en la sala nueva (destino)
// Necesitamos calcular la posición relativa correcta.
// Al estar alineados top/left, el offset relativo es el mismo (doorGridPos).
// (Si hubieramos centrado las salas, esto sería más complejo)
const oppositeDoorConfig = dir.opposite === 'N' || dir.opposite === 'S'
? { side: dir.opposite, gridX: doorGridPos, leadsTo: currentRoom.id }
: { side: dir.opposite, gridY: doorGridPos, leadsTo: currentRoom.id };
? { side: dir.opposite, gridX: doorGridPos, leadsTo: currentRoom.id, isOpen: false, id: `door_${newRoomId}_to_${currentRoom.id}` }
: { side: dir.opposite, gridY: doorGridPos, leadsTo: currentRoom.id, isOpen: false, id: `door_${newRoomId}_to_${currentRoom.id}` };
newRoom.doors.push(oppositeDoorConfig);
}
@@ -241,12 +236,13 @@ const ROOMS = generateDungeon();
const SESSION = {
selectedUnitId: null,
path: [], // Array de {x, y}
pathMeshes: [], // Array de meshes visuales
roomMeshes: {}, // { roomId: { tile: mesh, walls: [], doors: [], entities: [] } }
selectedDoorId: null, // Nuevo: ID de la puerta seleccionada
path: [],
pathMeshes: [],
roomMeshes: {},
isAnimating: false,
textureCache: {}, // Cache de texturas cargadas
currentView: 'N' // Vista actual: N, S, E, W
textureCache: {},
currentView: 'N'
};
// --- CONFIGURACIÓN BÁSICA THREE.JS ---
@@ -489,24 +485,25 @@ function isPositionDoor(x, y, room) {
return false;
}
// Verificar si una celda es transitable
// Verificar si una celda es transitable (bloquear puertas cerradas)
function isWalkable(x, y) {
// Verificar en todas las salas visitadas
for (const roomId of ROOMS.visitedRooms) {
const room = ROOMS.rooms.find(r => r.id === roomId);
if (!room) continue;
// Si está dentro de la sala, es transitable
if (isPositionInRoom(x, y, room)) {
return true;
}
// Si es una puerta de la sala, es transitable
if (isPositionDoor(x, y, room)) {
return true;
// Verificar puertas
for (const door of room.doors) {
const doorPos = getDoorGridPosition(room, door);
if (doorPos.x === x && doorPos.y === y) {
return door.isOpen; // Solo transitable si está abierta
}
}
}
return false;
}
@@ -562,7 +559,7 @@ function updatePathVisuals() {
// --- MANEJO VISUAL DE SELECCIÓN ---
function updateSelectionVisuals() {
// Buscar en todas las salas visitadas
// Unidades
ROOMS.visitedRooms.forEach(roomId => {
const room = ROOMS.rooms.find(r => r.id === roomId);
if (!room) return;
@@ -580,6 +577,132 @@ function updateSelectionVisuals() {
}
});
});
// Puertas
Object.keys(SESSION.roomMeshes).forEach(roomId => {
const roomData = SESSION.roomMeshes[roomId];
if (roomData.doors) {
roomData.doors.forEach(doorMesh => {
// Asumimos que guardamos el ID de la puerta en userData al crear el mesh
if (doorMesh.userData.id === SESSION.selectedDoorId) {
doorMesh.material.color.setHex(0xffff00);
doorMesh.material.opacity = 0.5;
doorMesh.material.transparent = true;
} else {
doorMesh.material.color.setHex(0xffffff);
// Restaurar opacidad original (si era transparente) o 1.0
// Por simplicidad, puertas cerradas opacas, abiertas transparentes?
// No, el modal decide. Dejamos como estaba por defecto.
doorMesh.material.opacity = 1.0;
}
});
}
});
}
// --- LOGICA MODAL PUERTAS ---
const modal = document.getElementById('door-modal');
const btnYes = document.getElementById('btn-open-yes');
const btnNo = document.getElementById('btn-open-no');
btnYes.addEventListener('click', confirmOpenDoor);
btnNo.addEventListener('click', closeDoorModal);
function openDoorModal() {
modal.classList.remove('hidden');
}
function closeDoorModal() {
modal.classList.add('hidden');
// Deseleccionar si cancela
if (SESSION.selectedDoorId) {
SESSION.selectedDoorId = null;
updateSelectionVisuals();
}
}
function confirmOpenDoor() {
if (!SESSION.selectedDoorId) return;
// Buscar la puerta
let targetDoor = null;
let originRoom = null;
for (const room of ROOMS.rooms) {
const found = room.doors.find(d => d.id === SESSION.selectedDoorId);
if (found) {
targetDoor = found;
originRoom = room;
break;
}
}
if (targetDoor && originRoom) {
console.log("Abriendo puerta:", targetDoor.id);
targetDoor.isOpen = true;
// Abrir también la puerta inversa (la de la otra sala)
const targetRoom = ROOMS.rooms.find(r => r.id === targetDoor.leadsTo);
if (targetRoom) {
const oppositeDoor = targetRoom.doors.find(d => d.leadsTo === originRoom.id);
if (oppositeDoor) {
oppositeDoor.isOpen = true;
// Si la sala destino YA está renderizada, ocultar visualmente su puerta también
if (SESSION.roomMeshes[targetRoom.id]) {
const oppDoorMesh = SESSION.roomMeshes[targetRoom.id].doors.find(m => m.userData.id === oppositeDoor.id);
if (oppDoorMesh) {
oppDoorMesh.visible = false;
}
}
}
// Revelar sala destino
if (!ROOMS.visitedRooms.has(targetRoom.id)) {
ROOMS.visitedRooms.add(targetRoom.id);
renderRoom(targetRoom);
}
}
// Actualizar visual del mesh (hacerla invisible o rotarla)
// Buscamos el mesh en roomMeshes
if (SESSION.roomMeshes[originRoom.id]) {
const doorMesh = SESSION.roomMeshes[originRoom.id].doors.find(m => m.userData.id === targetDoor.id);
if (doorMesh) {
doorMesh.visible = false; // "Abrir" visualmente desapareciendo
}
}
// Limipiar selección y cerrar modal
SESSION.selectedDoorId = null;
updateSelectionVisuals();
closeDoorModal();
drawMinimap();
}
}
function checkDoorInteraction(unit) {
if (!SESSION.selectedDoorId) return;
// Buscar puerta seleccionada
let targetDoor = null;
let room = null;
for (const r of ROOMS.rooms) {
targetDoor = r.doors.find(d => d.id === SESSION.selectedDoorId);
if (targetDoor) {
room = r;
break;
}
}
if (targetDoor && !targetDoor.isOpen) {
const doorPos = getDoorGridPosition(room, targetDoor);
// Verificar adyacencia
if (isAdjacent({ x: unit.x, y: unit.y }, doorPos)) {
openDoorModal();
}
}
}
// --- ANIMACIÓN DE MOVIMIENTO ---
@@ -612,7 +735,7 @@ async function animateMovement() {
const targetWorldPos = gridToWorld(targetGridPos.x, targetGridPos.y);
const endPos = { x: targetWorldPos.x, z: targetWorldPos.z };
const duration = 300;
const duration = 200;
const startTime = Date.now();
const standeeHeight = ASSETS.standees[unit.type].height;
@@ -627,7 +750,8 @@ async function animateMovement() {
unit.mesh.position.x = startPos.x + (endPos.x - startPos.x) * eased;
unit.mesh.position.z = startPos.z + (endPos.z - startPos.z) * eased;
const hopHeight = 0.8;
// Salto visual más sutil
const hopHeight = 0.5;
const hopProgress = Math.sin(progress * Math.PI);
unit.mesh.position.y = (standeeHeight / 2) + (hopProgress * hopHeight);
@@ -653,10 +777,10 @@ async function animateMovement() {
unit.x = step.x;
unit.y = step.y;
// 1. Verificar si hemos pisado una puerta (Para renderizar lo siguiente antes de entrar)
checkDoorTransition(unit, unitRoom);
// YA NO USAMOS checkDoorTransition automática para revelar/teletransportar
// en su lugar usamos la lógica de puertas interactivas
// 2. AUTO-CORRECCIÓN: Verificar en qué sala estamos FÍSICAMENTE
// 2. AUTO-CORRECCIÓN: Seguir usándola por seguridad si entramos
const actualRoom = detectRoomChange(unit, unitRoom);
if (actualRoom) {
unitRoom = actualRoom;
@@ -666,6 +790,9 @@ async function animateMovement() {
updatePathVisuals();
}
// Al terminar movimiento, verificar interacción con puerta
checkDoorInteraction(unit);
// Centrar cámara en el personaje manteniendo el offset de la vista actual
const newTarget = unit.mesh.position.clone();
newTarget.y = 0;
@@ -675,7 +802,7 @@ async function animateMovement() {
controls.target.copy(newTarget);
camera.position.copy(newTarget).add(currentOffset);
SESSION.selectedUnitId = null;
SESSION.selectedUnitId = null; // Deseleccionar unidad al terminar de mover
updateSelectionVisuals();
SESSION.isAnimating = false;
drawMinimap(); // Actualizar posición final del jugador
@@ -848,37 +975,127 @@ async function renderRoom(room) {
{ side: 'W', width: worldHeight, offset: { x: -halfSizeX, z: 0 }, rotation: Math.PI / 2 }
];
// Calcular posiciones de puertas para procesar paredes
// Mapa: Side -> Door (solo soportamos 1 puerta por pared por ahora para simplificar)
const doorsOnSides = {};
room.doors.forEach(d => { doorsOnSides[d.side] = d; });
for (const config of wallConfigs) {
if (room.walls.includes(config.side)) {
const opacity = getWallOpacity(config.side, SESSION.currentView);
const wallSide = config.side;
const door = doorsOnSides[wallSide];
// Textura adaptada al ancho específico de esta pared
const materialTex = wallTex.clone();
// Ajustar repetición horizontal según longitud de la pared (aprox 1 repetición cada 2 celdas grandes)
materialTex.repeat.set(config.width / (CONFIG.CELL_SIZE * 2), 2);
// Función helper para crear un segmento de pared
const createWallSegment = (w, h, xOffset, yOffset, opacity, name) => {
if (w <= 0.01) return; // Evitar segmentos degenerados
const wallMaterial = new THREE.MeshStandardMaterial({
map: materialTex,
const segmentGeometry = new THREE.PlaneGeometry(w, h);
// Ajustar textura al tamaño del segmento
const segmentTex = wallTex.clone();
segmentTex.wrapS = THREE.RepeatWrapping;
segmentTex.wrapT = THREE.RepeatWrapping;
segmentTex.repeat.set(w / 2, h / (wallHeight / 2)); // Mantener densidad aprox
const segmentMaterial = new THREE.MeshStandardMaterial({
map: segmentTex,
transparent: opacity < 1.0,
opacity: opacity,
side: THREE.DoubleSide
});
// Geometría específica para el ancho de ESTA pared
const wallGeometry = new THREE.PlaneGeometry(config.width, wallHeight);
const wall = new THREE.Mesh(segmentGeometry, segmentMaterial);
// Calculamos posición RELATIVA al centro de la pared "ideal"
// La pared ideal está en config.offset
// Rotamos el offset local del segmento según la rotación de la pared
const localX = xOffset;
const localZ = 0; // En el plano de la pared
// Rotar vector (localX, 0) por config.rotation
// Plane geometry is created at origin. We rotate it around Y.
// A segment meant to be at "xOffset" along the plane's width needs to be translated.
// Posición de la pared "Base"
const baseX = centerX + config.offset.x;
const baseZ = centerZ + config.offset.z;
// Vector dirección de la pared (Hacia la derecha de la pared)
// PlaneGeometry +X is "Right"
const dirX = Math.cos(config.rotation);
const dirZ = -Math.sin(config.rotation);
wall.position.x = baseX + (dirX * xOffset);
wall.position.z = baseZ + (dirZ * xOffset);
wall.position.y = yOffset; // Altura absoluta
const wall = new THREE.Mesh(wallGeometry, wallMaterial);
wall.position.set(
centerX + config.offset.x,
wallHeight / 2,
centerZ + config.offset.z
);
wall.rotation.y = config.rotation;
wall.castShadow = true;
wall.receiveShadow = true;
wall.userData.wallSide = config.side; // Metadata para identificar el lado
wall.userData.wallSide = config.side;
scene.add(wall);
roomMeshes.walls.push(wall);
};
const opacity = getWallOpacity(config.side, SESSION.currentView); // Se actualiza dinámicamente
if (!door) {
// PARED SOLIDA (Caso original simplificado)
createWallSegment(config.width, wallHeight, 0, wallHeight / 2, opacity, "FullWall");
} else {
// PARED CON HUECO
const doorWidth = 1.5;
const doorHeight = 2.0;
// Calcular posición relativa de la puerta en la pared
// config.width es el ancho total. El rango local es [-W/2, W/2]
// Obtener coordenadas de la puerta
const doorGridPos = getDoorGridPosition(room, door);
const doorWorldPos = gridToWorld(doorGridPos.x, doorGridPos.y);
// Necesitamos proyectar la posición de la puerta sobre el eje de la pared para saber su offset
// Pared N/S: Offset es diferencia en X.
// Pared E/W: Offset es diferencia en Z (pero ojo con la dirección del plano).
let doorOffset = 0; // Offset del centro de la puerta respecto al centro de la pared
if (config.side === 'N') {
doorOffset = doorWorldPos.x - centerX;
} else if (config.side === 'S') {
doorOffset = -(doorWorldPos.x - centerX); // S wall is rotated 180, local X is opposite world X
} else if (config.side === 'E') {
doorOffset = doorWorldPos.z - centerZ;
} else if (config.side === 'W') {
doorOffset = -(doorWorldPos.z - centerZ); // W wall is rotated 90, local X is opposite world Z
}
const w = config.width;
// Segmento Izquierdo: Desde -w/2 hasta (doorOffset - doorWidth/2)
const leftEnd = doorOffset - (doorWidth / 2);
const leftStart = -w / 2;
const leftWidth = leftEnd - leftStart;
const leftCenter = leftStart + (leftWidth / 2);
createWallSegment(leftWidth, wallHeight, leftCenter, wallHeight / 2, opacity, "LeftSeg");
// Segmento Derecho: Desde (doorOffset + doorWidth/2) hasta w/2
const rightStart = doorOffset + (doorWidth / 2);
const rightEnd = w / 2;
const rightWidth = rightEnd - rightStart;
const rightCenter = rightStart + (rightWidth / 2);
createWallSegment(rightWidth, wallHeight, rightCenter, wallHeight / 2, opacity, "RightSeg");
// Dintel (Arriba de la puerta)
// Ancho: doorWidth
// Altura: wallHeight - doorHeight (2.5 - 2.0 = 0.5)
// Centro X: doorOffset
// Centro Y: doorHeight + (dintelHeight / 2) -> 2.0 + 0.25 = 2.25
const lintelHeight = wallHeight - doorHeight;
if (lintelHeight > 0) {
createWallSegment(doorWidth, lintelHeight, doorOffset, doorHeight + (lintelHeight / 2), opacity, "Lintel");
}
}
}
@@ -904,24 +1121,30 @@ async function renderRoom(room) {
});
const doorMesh = new THREE.Mesh(doorGeometry, doorMaterial);
doorMesh.userData.id = door.id;
doorMesh.visible = !door.isOpen; // Ocultar si ya está abierta
const doorGridPos = getDoorGridPosition(room, door);
const doorWorldPos = gridToWorld(doorGridPos.x, doorGridPos.y);
switch (door.side) {
case 'N':
doorMesh.position.set(doorWorldPos.x, doorHeight / 2, centerZ - halfSizeZ + 0.05);
// Pared Norte: Alinear X con worldPos, Z con borde norte
doorMesh.position.set(doorWorldPos.x, doorHeight / 2, centerZ - halfSizeZ);
doorMesh.rotation.y = 0;
break;
case 'S':
doorMesh.position.set(doorWorldPos.x, doorHeight / 2, centerZ + halfSizeZ - 0.05);
doorMesh.rotation.y = 0;
// Pared Sur
doorMesh.position.set(doorWorldPos.x, doorHeight / 2, centerZ + halfSizeZ);
doorMesh.rotation.y = 0; // O Math.PI, visualmente igual para puerta plana 1.5x2
break;
case 'E':
doorMesh.position.set(centerX + halfSizeX - 0.05, doorHeight / 2, doorWorldPos.z);
// Pared Este: Alinear Z con worldPos, X con borde este
doorMesh.position.set(centerX + halfSizeX, doorHeight / 2, doorWorldPos.z);
doorMesh.rotation.y = Math.PI / 2;
break;
case 'W':
doorMesh.position.set(centerX - halfSizeX + 0.05, doorHeight / 2, doorWorldPos.z);
// Pared Oeste
doorMesh.position.set(centerX - halfSizeX, doorHeight / 2, doorWorldPos.z);
doorMesh.rotation.y = Math.PI / 2;
break;
}
@@ -984,7 +1207,7 @@ function updateCompassUI() {
document.querySelectorAll('.compass-btn').forEach(btn => {
btn.classList.remove('active');
});
const activeBtn = document.querySelector(`[data-direction="${SESSION.currentView}"]`);
const activeBtn = document.querySelector(`[data-dir="${SESSION.currentView}"]`);
if (activeBtn) {
activeBtn.classList.add('active');
}
@@ -1090,12 +1313,14 @@ function drawMinimap() {
}
}
// Event listeners para los botones del compás
document.querySelectorAll('.compass-btn').forEach(btn => {
btn.addEventListener('click', () => {
const direction = btn.getAttribute('data-direction');
setCameraView(direction);
const direction = btn.getAttribute('data-dir');
if (direction) {
setCameraView(direction);
updateCompassUI();
}
});
});
@@ -1133,6 +1358,7 @@ window.addEventListener('pointerdown', (event) => {
if (entity) {
console.log("Seleccionado:", entity.type);
SESSION.selectedUnitId = entity.id;
SESSION.selectedDoorId = null; // Deseleccionar puerta
SESSION.path = [];
updatePathVisuals();
updateSelectionVisuals();
@@ -1140,6 +1366,40 @@ window.addEventListener('pointerdown', (event) => {
}
}
// Detectar click en puertas
const allDoors = [];
Object.values(SESSION.roomMeshes).forEach(roomData => {
if (roomData.doors) {
allDoors.push(...roomData.doors);
}
});
const intersectsDoors = raycaster.intersectObjects(allDoors);
if (intersectsDoors.length > 0) {
const clickedDoor = intersectsDoors[0].object;
if (clickedDoor.userData.id) {
console.log("Puerta seleccionada:", clickedDoor.userData.id);
SESSION.selectedDoorId = clickedDoor.userData.id;
SESSION.selectedUnitId = null;
SESSION.path = [];
updatePathVisuals();
updateSelectionVisuals();
// Verificar interacción inmediata (si ya estamos al lado)
// Buscamos al héroe principal (asumimos que es el que controlamos)
let hero = null;
for (const r of ROOMS.rooms) {
hero = r.entities.find(e => e.type === 'hero_1');
if (hero) break;
}
if (hero) {
checkDoorInteraction(hero);
}
return;
}
}
// Procesar click en suelo
if (SESSION.selectedUnitId) {
const intersectsGround = raycaster.intersectObject(raycastPlane);

View File

@@ -121,4 +121,66 @@ canvas {
#compass-w {
grid-column: 1;
grid-row: 2;
}
}
/* Modal Styles */
#door-modal {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 2000;
pointer-events: auto;
}
#door-modal.hidden {
display: none;
}
.modal-content {
background: #2a2a2a;
padding: 20px;
border: 2px solid #555;
border-radius: 8px;
text-align: center;
color: #fff;
box-shadow: 0 4px 15px rgba(0,0,0,0.5);
}
.modal-content p {
margin-bottom: 20px;
font-size: 1.2rem;
}
.modal-content button {
padding: 8px 20px;
margin: 0 10px;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
font-size: 1rem;
}
#btn-open-yes {
background: #4CAF50;
color: white;
}
#btn-open-yes:hover {
background: #45a049;
}
#btn-open-no {
background: #f44336;
color: white;
}
#btn-open-no:hover {
background: #d32f2f;
}

124
src/style.css.bak Normal file
View File

@@ -0,0 +1,124 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
background-color: #242424;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
overflow: hidden;
/* Evitar scrollbars por el canvas */
}
#app {
width: 100%;
height: 100vh;
}
canvas {
display: block;
}
/* HUD Wrapper */
#hud {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
/* Dejar pasar clics al juego 3D */
z-index: 999;
}
/* UI Elements inside HUD (reactivate pointer events) */
#hud>* {
pointer-events: auto;
}
/* Minimap */
#minimap-container {
position: absolute;
top: 20px;
left: 20px;
width: 200px;
height: 200px;
background: rgba(0, 0, 0, 0.7);
border: 2px solid #444;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
}
#minimap {
width: 100%;
height: 100%;
}
/* Compass UI */
#compass {
position: absolute;
top: 20px;
right: 20px;
width: 100px;
height: 100px;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: 1fr 1fr 1fr;
gap: 2px;
}
.compass-btn {
background: rgba(50, 50, 50, 0.8);
border: 2px solid rgba(255, 255, 255, 0.3);
color: rgba(255, 255, 255, 0.6);
font-size: 18px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
user-select: none;
}
.compass-btn:hover {
background: rgba(70, 70, 70, 0.9);
border-color: rgba(255, 255, 255, 0.5);
color: rgba(255, 255, 255, 0.8);
}
.compass-btn.active {
background: rgba(255, 200, 0, 0.9);
border-color: rgba(255, 220, 0, 1);
color: rgba(0, 0, 0, 1);
box-shadow: 0 0 15px rgba(255, 200, 0, 0.6);
}
#compass-n {
grid-column: 2;
grid-row: 1;
}
#compass-s {
grid-column: 2;
grid-row: 3;
}
#compass-e {
grid-column: 3;
grid-row: 2;
}
#compass-w {
grid-column: 1;
grid-row: 2;
}