diff --git a/DEVLOG.md b/DEVLOG.md new file mode 100644 index 0000000..03296b3 --- /dev/null +++ b/DEVLOG.md @@ -0,0 +1,130 @@ +# Devlog del Proyecto: Masmorres (Physical-Web Crawler) + +Este documento sirve para llevar un control diario del desarrollo, decisiones técnicas y nuevas funcionalidades implementadas en el proyecto. + +## [2025-12-29] - Sistema Avanzado de Mapeo de Tiles + +### Funcionalidades Implementadas +- **TileDefinitions.js:** Nuevo módulo centralizado con definiciones de todas las tiles (rooms, corridors, L-shapes, T-junctions). + - Cada tile incluye: dimensiones, tipo, imagen, matriz de walkability, y exits. + - Matriz de walkability: 0 = no pisable, 1-8 = pisable con capa/altura, 9 = escaleras. + +- **Sistema de Deck Abstracto:** + - El deck ahora contiene tipos abstractos (e.g., 'L', 'corridor') en lugar de tiles específicas. + - Composición: 8 rooms 4x4, 4 rooms 4x6, 12 corridors, 10 L-shapes, 8 T-junctions. + - Cuando se dibuja un tipo, el sistema selecciona aleatoriamente entre las variantes que encajan. + +- **Validación de Conexiones:** + - `canConnectTiles()`: Verifica compatibilidad de tipos, dirección de salidas, y alineación de walkability. + - Reglas de conexión: Rooms ↔ Rooms/Corridors, Corridors ↔ Rooms/Corridors/L/T, L/T ↔ Corridors. + - Validación de dirección: Si sales por N, la nueva tile debe tener salida S. + +- **Alineación de Walkability:** + - `validateWalkabilityAlignment()`: Maneja tiles de diferentes tamaños (corridor 2x6 vs room 4x4). + - Prueba offset 0 primero, luego offset 2 (ancho del corridor) si es necesario. + - Sistema de offset para desplazar L-shapes y T-junctions y alinear áreas pisables. + +- **Filtrado de Orientación:** + - Corridors se filtran por orientación: puertas E/W requieren corridors EW, puertas N/S requieren corridors NS. + - Selección exhaustiva: Cuando se dibuja una L o T, se prueban todas las variantes antes de descartar. + +### Cambios Técnicos +- Modificado `DungeonDecks.js` para usar sistema de deck abstracto. +- Actualizado `exploreRoom()` en `main.js` para trabajar con tipos abstractos y seleccionar variantes concretas. +- Nuevas funciones: `validateWalkabilityAlignment()`, `canConnectTiles()`, `getEdgeCells()`, `shouldPlaceDoor()`. +- Actualizado `renderRoom()` para usar `room.tileDef` en lugar de `ASSETS.tiles`. + +### Problemas Conocidos +- **Offset de L/T:** La alineación de L-shapes y T-junctions todavía presenta desplazamientos incorrectos en algunos casos. +- **Frecuencia de L/T:** Aunque se aumentó la cantidad en el deck, las L y T solo aparecen cuando se conectan desde corridors, limitando su frecuencia. + +### Próximos Pasos +- Depurar y corregir el cálculo del offset para L-shapes y T-junctions. +- Revisar la lógica de aplicación del offset según la dirección de conexión (N/S vs E/W). +- Considerar ajustar las reglas de conexión para permitir más variedad en la generación. + +## [2025-12-28] - Fase 1: Arquitectura Híbrida y Servidor + +### Infraestructura +- **Game Server (`game-server.js`):** Implementado servidor WebSocket (Socket.io) en puerto 3001 para gestionar la comunicación PC-Móvil. +- **Docker:** Actualizado `docker-compose.yml` para ejecutar el servidor juego como servicio independiente. +- **Networking:** Configuración dinámica de IP en el cliente para permitir conexión desde dispositivos en la red local. + +### Datos +- **Esquemas JSON:** Definidos contratos de datos iniciales en `src/schemas/`: + - `CampaignSchema.js`: Estructura para campañas multijugador. + - `MissionSchema.js`: Configuración para generación procedural y scripting. + +## [2025-12-28] - Corrección Completa del Sistema de Puertas + +### Funcionalidades Implementadas +- **Refactorización de Posicionamiento de Puertas:** + - Creada función unificada `getDoorWorldPosition()` que centraliza el cálculo de posiciones. + - Eliminada duplicación de lógica entre generación de huecos en paredes y posicionamiento de meshes de puertas. + - Reducción de ~45 líneas de código duplicado. + +- **Corrección de Alineamiento E/W:** + - Identificado problema: Las paredes Este y Oeste tienen `rotation = π/2`, lo que hace que su eje X local apunte hacia -Z. + - Solución: Invertir el `wallOffset` para ambas paredes E/W: `wallOffset = -(doorWorldPos.z - centerZ)`. + - **Resultado:** Puertas y huecos perfectamente alineados en todas las direcciones (N, S, E, W). + +- **Corrección de Interacción con Puertas Abiertas:** + - Problema detectado: Las puertas abiertas (invisibles) seguían bloqueando clics del ratón. + - Solución: Filtrar puertas invisibles del raycast: `allDoors.push(...roomData.doors.filter(door => door.visible))`. + - **Resultado:** Los jugadores ahora pueden hacer clic "a través" de puertas abiertas para seleccionar baldosas. + +### Cambios Técnicos +- Nueva función `getDoorWorldPosition(room, door, centerX, centerZ, halfSizeX, halfSizeZ)`: + - Devuelve: `{ worldPos, meshPos, rotation, wallOffset }` + - Garantiza coherencia entre geometría de huecos y meshes visuales. +- Modificado raycast de puertas para excluir meshes invisibles (línea 1388). +- Commits: `8025d66`, `5852a97`, `57f6312`. + +### Lecciones Aprendidas +- **Geometría Rotada:** Cuando un `PlaneGeometry` se rota (e.g., π/2), su sistema de coordenadas local cambia. Es crucial calcular offsets considerando la dirección del eje X local tras la rotación. +- **Raycast e Invisibilidad:** `mesh.visible = false` solo oculta visualmente un objeto, pero Three.js sigue detectándolo en raycasts. Siempre filtrar objetos invisibles antes de `intersectObjects()`. + +## [2025-12-23] - Interacción con Puertas y Navegación + +### Funcionalidades Implementadas +- **Sistema de Puertas Interactivas:** + - Se eliminó la transición automática entre salas al pisar una puerta. + - Ahora las puertas actúan como bloqueos físicos hasta que son "abiertas" explícitamente. + - Lógica de selección: Click en una puerta cerrada para seleccionarla (feedback visual amarillo). + - **Modal de Interacción:** + - Al mover una unidad adyacente a una puerta seleccionada, se dispara un modal UI: "¿Quieres abrir la puerta?". + - **Confirmar:** La puerta visual se oculta, la sala destino se renderiza (si no lo estaba) y se permite el paso. + - **Cancelar:** Se deselecciona la puerta y se mantiene cerrada. + +### Cambios Técnicos +- Modificado `main.js` para incluir `checkDoorInteraction` al finalizar el movimiento. +- Nuevo estado en `SESSION`: `selectedDoorId`. +- Actualización de `isWalkable` para considerar el estado `isOpen` de las puertas. + +## [2025-12-20] - Sistema Visual Dinámico (Dynamic Wall Opacity) + +### Funcionalidades Implementadas +- **Opacidad de Muros Contextual:** + - Los muros ahora ajustan su opacidad dinámicamente basándose en la rotación de la cámara (N, S, E, W) para evitar obstruir la visión del jugador. + - **Regla General:** Los muros "frontales" a la cámara se vuelven semitransparentes (50%), mientras que los "traseros" permanecen opacos. + +### Cambios Técnicos +- Implementada función `getWallOpacity(wallSide, viewDirection)`. +- Integración en `setCameraView` para refrescar opacidades al girar la vista. +- Los muros ahora tienen la propiedad `userData.wallSide` asignada durante la generación. + +## [2025-12-19] - Feedback de Selección y UI + +### Funcionalidades Implementadas +- **Resaltado de Selección (Highlighting):** + - Unidades y objetos interactivos ahora muestran un aura/color amarillo al ser seleccionados. + - Opacidad reducida al 50% para indicar estado de selección activo. +- **Mejoras de Animación:** + - Refinamiento del "salto" de los standees al moverse entre casillas. + +## [Inicio del Proyecto] - Manifiesto y Core Loop + +### Visión General +- Definido el **Manifiesto Técnico (v2.0)**: Visión de un "Puente Híbrido" entre juego de mesa físico y motor narrativo digital (LLM). +- **Generación Procedural:** Algoritmo de mazmorras basado en tiles de 4x4 con expansión orgánica. +- **Motor Gráfico:** Three.js con cámara isométrica ortográfica y controles restringidos (N, S, E, W). diff --git a/assets/images/dungeons/L_NE.png b/assets/images/dungeons/L_NE.png new file mode 100644 index 0000000..1afce1a Binary files /dev/null and b/assets/images/dungeons/L_NE.png differ diff --git a/assets/images/dungeons/L_SE.png b/assets/images/dungeons/L_SE.png new file mode 100644 index 0000000..e490d33 Binary files /dev/null and b/assets/images/dungeons/L_SE.png differ diff --git a/assets/images/dungeons/L_WN.png b/assets/images/dungeons/L_WN.png new file mode 100644 index 0000000..9c42d97 Binary files /dev/null and b/assets/images/dungeons/L_WN.png differ diff --git a/assets/images/dungeons/L_WS.png b/assets/images/dungeons/L_WS.png new file mode 100644 index 0000000..eeab863 Binary files /dev/null and b/assets/images/dungeons/L_WS.png differ diff --git a/assets/images/dungeons/T_NES.png b/assets/images/dungeons/T_NES.png new file mode 100644 index 0000000..f016d47 Binary files /dev/null and b/assets/images/dungeons/T_NES.png differ diff --git a/assets/images/dungeons/T_WNE.png b/assets/images/dungeons/T_WNE.png new file mode 100644 index 0000000..b7341b5 Binary files /dev/null and b/assets/images/dungeons/T_WNE.png differ diff --git a/assets/images/dungeons/T_WNS.png b/assets/images/dungeons/T_WNS.png new file mode 100644 index 0000000..dd9ccc2 Binary files /dev/null and b/assets/images/dungeons/T_WNS.png differ diff --git a/assets/images/dungeons/T_WSE.png b/assets/images/dungeons/T_WSE.png new file mode 100644 index 0000000..2abcf28 Binary files /dev/null and b/assets/images/dungeons/T_WSE.png differ diff --git a/assets/images/dungeons/corridor1_EW.png b/assets/images/dungeons/corridor1_EW.png new file mode 100644 index 0000000..65a8830 Binary files /dev/null and b/assets/images/dungeons/corridor1_EW.png differ diff --git a/assets/images/dungeons/corridor1_NS.png b/assets/images/dungeons/corridor1_NS.png new file mode 100644 index 0000000..4ffcbf7 Binary files /dev/null and b/assets/images/dungeons/corridor1_NS.png differ diff --git a/assets/images/dungeons/corridor2_EW.png b/assets/images/dungeons/corridor2_EW.png new file mode 100644 index 0000000..0d62bfc Binary files /dev/null and b/assets/images/dungeons/corridor2_EW.png differ diff --git a/assets/images/dungeons/corridor2_NS.png b/assets/images/dungeons/corridor2_NS.png new file mode 100644 index 0000000..5f2932a Binary files /dev/null and b/assets/images/dungeons/corridor2_NS.png differ diff --git a/assets/images/dungeons/corridor3_EW.png b/assets/images/dungeons/corridor3_EW.png new file mode 100644 index 0000000..01c2e1b Binary files /dev/null and b/assets/images/dungeons/corridor3_EW.png differ diff --git a/assets/images/dungeons/corridor3_NS.png b/assets/images/dungeons/corridor3_NS.png new file mode 100644 index 0000000..f03f47d Binary files /dev/null and b/assets/images/dungeons/corridor3_NS.png differ diff --git a/assets/images/dungeons/room_4x4_circle.png b/assets/images/dungeons/room_4x4_circle.png new file mode 100644 index 0000000..8f7d08f Binary files /dev/null and b/assets/images/dungeons/room_4x4_circle.png differ diff --git a/assets/images/dungeons/room_4x4_normal.png b/assets/images/dungeons/room_4x4_normal.png new file mode 100644 index 0000000..48f2438 Binary files /dev/null and b/assets/images/dungeons/room_4x4_normal.png differ diff --git a/assets/images/dungeons/room_4x4_squeleton.png b/assets/images/dungeons/room_4x4_squeleton.png new file mode 100644 index 0000000..91f7acd Binary files /dev/null and b/assets/images/dungeons/room_4x4_squeleton.png differ diff --git a/assets/images/dungeons/room_4x6_altar.png b/assets/images/dungeons/room_4x6_altar.png new file mode 100644 index 0000000..37a69f6 Binary files /dev/null and b/assets/images/dungeons/room_4x6_altar.png differ diff --git a/assets/images/dungeons/room_4x6_tomb.png b/assets/images/dungeons/room_4x6_tomb.png new file mode 100644 index 0000000..80f1e86 Binary files /dev/null and b/assets/images/dungeons/room_4x6_tomb.png differ diff --git a/docker-compose.yml b/docker-compose.yml index 05e9ccf..8bccb13 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,3 +8,13 @@ services: - /app/node_modules environment: - CHOKIDAR_USEPOLLING=true + command: npm run dev + + server: + build: . + ports: + - "3001:3001" + volumes: + - .:/app + - /app/node_modules + command: node game-server.js diff --git a/game-server.js b/game-server.js new file mode 100644 index 0000000..1f57d11 --- /dev/null +++ b/game-server.js @@ -0,0 +1,81 @@ +import express from 'express'; +import { createServer } from 'http'; +import { Server } from 'socket.io'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const app = express(); +const httpServer = createServer(app); +const io = new Server(httpServer, { + cors: { + origin: "*", // Allow connections from any mobile device on local network + methods: ["GET", "POST"] + } +}); + +// Serve static files from 'dist' (production) or 'public' (dev partial) +// In a real setup, Vite handles dev serving, but this server handles the sockets. +app.use(express.static(join(__dirname, 'dist'))); + +// Game State Storage (In-Memory for now) +const LOBBIES = { + // "lobbyCode": { hostSocket: id, players: [{id, name, charId}] } +}; + +io.on('connection', (socket) => { + console.log('Client connected:', socket.id); + + // --- HOST EVENTS (PC) --- + socket.on('HOST_GAME', () => { + const lobbyCode = generateLobbyCode(); + LOBBIES[lobbyCode] = { + hostSocket: socket.id, + players: [] + }; + socket.join(lobbyCode); + socket.emit('LOBBY_CREATED', { code: lobbyCode }); + console.log(`Lobby ${lobbyCode} created by ${socket.id}`); + }); + + // --- PLAYER EVENTS (MOBILE) --- + socket.on('JOIN_GAME', ({ code, name }) => { + const lobby = LOBBIES[code.toUpperCase()]; + if (lobby) { + lobby.players.push({ id: socket.id, name, charId: null }); + socket.join(code.toUpperCase()); + + // Notify Host + io.to(lobby.hostSocket).emit('PLAYER_JOINED', { id: socket.id, name }); + // Confirm to Player + socket.emit('JOIN_SUCCESS', { code: code.toUpperCase() }); + console.log(`Player ${name} joined lobby ${code}`); + } else { + socket.emit('ERROR', { message: "Lobby not found" }); + } + }); + + socket.on('PLAYER_ACTION', ({ code, action, data }) => { + const lobby = LOBBIES[code]; + if (lobby) { + // Forward directly to Host + io.to(lobby.hostSocket).emit('PLAYER_ACTION', { playerId: socket.id, action, data }); + } + }); + + socket.on('disconnect', () => { + console.log('Client disconnected:', socket.id); + // Handle cleanup (remove player from lobby, notify host) + }); +}); + +function generateLobbyCode() { + return Math.random().toString(36).substring(2, 6).toUpperCase(); +} + +const PORT = 3001; +httpServer.listen(PORT, () => { + console.log(`Game Server running on http://localhost:${PORT}`); +}); diff --git a/index.html b/index.html index f1fef14..ec01994 100644 --- a/index.html +++ b/index.html @@ -12,13 +12,22 @@
- +
-
N
-
S
-
E
-
W
+
N
+
+
W
+
E
+
+
S
+
+
diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..f1ed21c --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2039 @@ +{ + "name": "masmorres-isometric", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "masmorres-isometric", + "version": "0.0.0", + "dependencies": { + "express": "^5.2.1", + "socket.io": "^4.8.3", + "socket.io-client": "^4.8.3", + "three": "^0.160.0" + }, + "devDependencies": { + "vite": "^5.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", + "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", + "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", + "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", + "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", + "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", + "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", + "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", + "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", + "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", + "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", + "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", + "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", + "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", + "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", + "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", + "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", + "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", + "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", + "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", + "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", + "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", + "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", + "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/body-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/engine.io": { + "version": "6.6.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.5.tgz", + "integrity": "sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz", + "integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz", + "integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/rollup": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", + "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.54.0", + "@rollup/rollup-android-arm64": "4.54.0", + "@rollup/rollup-darwin-arm64": "4.54.0", + "@rollup/rollup-darwin-x64": "4.54.0", + "@rollup/rollup-freebsd-arm64": "4.54.0", + "@rollup/rollup-freebsd-x64": "4.54.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", + "@rollup/rollup-linux-arm-musleabihf": "4.54.0", + "@rollup/rollup-linux-arm64-gnu": "4.54.0", + "@rollup/rollup-linux-arm64-musl": "4.54.0", + "@rollup/rollup-linux-loong64-gnu": "4.54.0", + "@rollup/rollup-linux-ppc64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-musl": "4.54.0", + "@rollup/rollup-linux-s390x-gnu": "4.54.0", + "@rollup/rollup-linux-x64-gnu": "4.54.0", + "@rollup/rollup-linux-x64-musl": "4.54.0", + "@rollup/rollup-openharmony-arm64": "4.54.0", + "@rollup/rollup-win32-arm64-msvc": "4.54.0", + "@rollup/rollup-win32-ia32-msvc": "4.54.0", + "@rollup/rollup-win32-x64-gnu": "4.54.0", + "@rollup/rollup-win32-x64-msvc": "4.54.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/socket.io": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz", + "integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==", + "license": "MIT", + "dependencies": { + "debug": "~4.4.1", + "ws": "~8.18.3" + } + }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz", + "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/three": { + "version": "0.160.1", + "resolved": "https://registry.npmjs.org/three/-/three-0.160.1.tgz", + "integrity": "sha512-Bgl2wPJypDOZ1stAxwfWAcJ0WQf7QzlptsxkjYiURPz+n5k4RBDLsq+6f9Y75TYxn6aHLcWz+JNmwTOXWrQTBQ==", + "license": "MIT" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + } + } +} diff --git a/package.json b/package.json index de7b408..b3c0434 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,9 @@ "vite": "^5.0.0" }, "dependencies": { + "express": "^5.2.1", + "socket.io": "^4.8.3", + "socket.io-client": "^4.8.3", "three": "^0.160.0" } -} \ No newline at end of file +} diff --git a/src/dungeon/DungeonDecks.js b/src/dungeon/DungeonDecks.js new file mode 100644 index 0000000..9190a9e --- /dev/null +++ b/src/dungeon/DungeonDecks.js @@ -0,0 +1,141 @@ +import { ROOMS, CORRIDORS, L_SHAPES, T_JUNCTIONS } from './TileDefinitions.js'; + +/** + * DungeonDeck - Manages the deck of dungeon tiles + * + * New approach: Deck contains abstract tile types, not specific tiles + * When a type is drawn, we select a fitting variant from available options + */ + +export class DungeonDeck { + constructor() { + this.deck = []; + this.discardPile = []; + this.shuffleDeck(); + } + + shuffleDeck() { + // Create a deck with abstract tile types + this.deck = [ + // 8 rooms 4x4 (abstract) + { type: 'room_4x4' }, + { type: 'room_4x4' }, + { type: 'room_4x4' }, + { type: 'room_4x4' }, + { type: 'room_4x4' }, + { type: 'room_4x4' }, + { type: 'room_4x4' }, + { type: 'room_4x4' }, + + // 4 rooms 4x6 (abstract) + { type: 'room_4x6' }, + { type: 'room_4x6' }, + { type: 'room_4x6' }, + { type: 'room_4x6' }, + + // 12 corridors (abstract) + { type: 'corridor' }, + { type: 'corridor' }, + { type: 'corridor' }, + { type: 'corridor' }, + { type: 'corridor' }, + { type: 'corridor' }, + { type: 'corridor' }, + { type: 'corridor' }, + { type: 'corridor' }, + { type: 'corridor' }, + { type: 'corridor' }, + { type: 'corridor' }, + + // 10 L-shapes (abstract) - increased from 6 + { type: 'L' }, + { type: 'L' }, + { type: 'L' }, + { type: 'L' }, + { type: 'L' }, + { type: 'L' }, + { type: 'L' }, + { type: 'L' }, + { type: 'L' }, + { type: 'L' }, + + // 8 T-junctions (abstract) - increased from 4 + { type: 'T' }, + { type: 'T' }, + { type: 'T' }, + { type: 'T' }, + { type: 'T' }, + { type: 'T' }, + { type: 'T' }, + { type: 'T' } + ]; + + // Fisher-Yates shuffle + for (let i = this.deck.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [this.deck[i], this.deck[j]] = [this.deck[j], this.deck[i]]; + } + console.log("Dungeon Deck Shuffled:", this.deck.length, "cards"); + } + + drawCard() { + if (this.deck.length === 0) { + console.warn("Deck empty! Reshuffling discards..."); + if (this.discardPile.length === 0) { + console.error("No cards left!"); + return null; + } + this.deck = [...this.discardPile]; + this.discardPile = []; + this.shuffleDeck(); + } + + const card = this.deck.pop(); + this.discardPile.push(card); + return card; + } + + /** + * Draw a compatible abstract tile type + * @param {string} originTileType - Type of the origin tile ('room', 'corridor', 'L', 'T') + * @param {number} maxAttempts - Maximum number of cards to try + * @returns {object|null} - Abstract tile card or null if none found + */ + drawCompatibleCard(originTileType, maxAttempts = 10) { + const validTypes = this.getCompatibleTypes(originTileType); + + for (let i = 0; i < maxAttempts && this.deck.length > 0; i++) { + const card = this.drawCard(); + + // Check if this abstract type is compatible + if (validTypes.includes(card.type)) { + return card; + } + + // Put incompatible card back at the bottom of the deck + this.deck.unshift(card); + this.discardPile.pop(); + } + + console.warn(`Could not find compatible tile for ${originTileType} after ${maxAttempts} attempts`); + return null; + } + + /** + * Get compatible tile types for a given origin type + * @param {string} originType - Type of the origin tile + * @returns {string[]} - Array of compatible abstract tile types + */ + getCompatibleTypes(originType) { + const connectionRules = { + 'room': ['room_4x4', 'room_4x6', 'corridor'], + 'corridor': ['room_4x4', 'room_4x6', 'corridor', 'L', 'T'], + 'L': ['corridor'], + 'T': ['corridor'] + }; + + return connectionRules[originType] || []; + } +} + +export const dungeonDeck = new DungeonDeck(); diff --git a/src/dungeon/EventDeck.js b/src/dungeon/EventDeck.js new file mode 100644 index 0000000..65d21a2 --- /dev/null +++ b/src/dungeon/EventDeck.js @@ -0,0 +1,41 @@ +export const EVENTS = [ + { + id: 'ev_nothing', + title: 'Silencio', + description: 'La mazmorra está en calma... sospechosamente tranquila.', + type: 'NADA' + }, + { + id: 'ev_monster_1', + title: '¡Emboscada!', + description: '¡Monstruos surgen de las sombras!', + type: 'MONSTRUO', + count: 2 + }, + { + id: 'ev_trap_1', + title: 'Trampa de Pinchos', + description: 'Un click resuena bajo tus pies. ¡Pinchos surgen del suelo!', + type: 'TRAMPA', + damage: 1 + }, + { + id: 'ev_wind', + title: 'Viento Helado', + description: 'Una corriente de aire apaga vuestras antorchas.', + type: 'PELIGRO' + } +]; + +export class EventDeck { + constructor() { + } + + drawCard() { + // Simple random draw for now + const randIndex = Math.floor(Math.random() * EVENTS.length); + return EVENTS[randIndex]; + } +} + +export const eventDeck = new EventDeck(); diff --git a/src/dungeon/TileDefinitions.js b/src/dungeon/TileDefinitions.js new file mode 100644 index 0000000..95eef71 --- /dev/null +++ b/src/dungeon/TileDefinitions.js @@ -0,0 +1,431 @@ +/** + * TileDefinitions.js + * + * Defines all dungeon tiles with their properties: + * - Dimensions (width x height in cells) + * - Walkability matrix (0 = not walkable, 1-8 = walkable layer/height, 9 = stairs) + * - Tile type (room, corridor, L, T) + * - Exit points + * - Image path + */ + +// ============================================================================ +// ROOMS (4x4 and 4x6) +// ============================================================================ + +const ROOM_4X4_NORMAL = { + id: 'room_4x4_normal', + tileType: 'room', + width: 4, + height: 4, + image: '/assets/images/dungeons/room_4x4_normal.png', + walkability: [ + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1] + ], + exits: ['N', 'S', 'E', 'W'] +}; + +const ROOM_4X4_CIRCLE = { + id: 'room_4x4_circle', + tileType: 'room', + width: 4, + height: 4, + image: '/assets/images/dungeons/room_4x4_circle.png', + walkability: [ + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1] + ], + exits: ['N', 'S', 'E', 'W'] +}; + +const ROOM_4X4_SKELETON = { + id: 'room_4x4_skeleton', + tileType: 'room', + width: 4, + height: 4, + image: '/assets/images/dungeons/room_4x4_squeleton.png', + walkability: [ + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1] + ], + exits: ['N', 'S', 'E', 'W'] +}; + +const ROOM_4X6_ALTAR = { + id: 'room_4x6_altar', + tileType: 'room', + width: 4, + height: 6, + image: '/assets/images/dungeons/room_4x6_altar.png', + walkability: [ + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1] + ], + exits: ['N', 'S', 'E', 'W'] +}; + +const ROOM_4X6_TOMB = { + id: 'room_4x6_tomb', + tileType: 'room', + width: 4, + height: 6, + image: '/assets/images/dungeons/room_4x6_tomb.png', + walkability: [ + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1] + ], + exits: ['N', 'S', 'E', 'W'] +}; + +// Example room with stairs (2 levels) +const ROOM_4X6_STAIRS = { + id: 'room_4x6_stairs', + tileType: 'room', + width: 4, + height: 6, + image: '/assets/images/dungeons/room_4x6_altar.png', // Using altar image as placeholder + walkability: [ + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 9, 9, 1], // Stairs connecting level 1 and 2 + [1, 2, 2, 1], + [2, 2, 2, 2], + [2, 2, 2, 2] + ], + exits: ['N', 'S'] +}; + +// ============================================================================ +// L-SHAPES (4x4 with 2-tile rows) +// ============================================================================ + +const L_NE = { + id: 'L_NE', + tileType: 'L', + width: 4, + height: 4, + image: '/assets/images/dungeons/L_NE.png', + walkability: [ + [1, 1, 0, 0], + [1, 1, 0, 0], + [1, 1, 1, 1], + [1, 1, 1, 1] + ], + exits: ['N', 'E'] +}; + +const L_SE = { + id: 'L_SE', + tileType: 'L', + width: 4, + height: 4, + image: '/assets/images/dungeons/L_SE.png', + walkability: [ + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 0, 0], + [1, 1, 0, 0] + ], + exits: ['S', 'E'] +}; + +const L_WS = { + id: 'L_WS', + tileType: 'L', + width: 4, + height: 4, + image: '/assets/images/dungeons/L_WS.png', + walkability: [ + [1, 1, 1, 1], + [1, 1, 1, 1], + [0, 0, 1, 1], + [0, 0, 1, 1] + ], + exits: ['W', 'S'] +}; + +const L_WN = { + id: 'L_WN', + tileType: 'L', + width: 4, + height: 4, + image: '/assets/images/dungeons/L_WN.png', + walkability: [ + [0, 0, 1, 1], + [0, 0, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1] + ], + exits: ['W', 'N'] +}; + +// ============================================================================ +// T-JUNCTIONS (4x6 or 6x4 with 2-tile rows) +// ============================================================================ + +const T_NES = { + id: 'T_NES', + tileType: 'T', + width: 4, + height: 6, + image: '/assets/images/dungeons/T_NES.png', + walkability: [ + [1, 1, 0, 0], + [1, 1, 0, 0], + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 0, 0], + [1, 1, 0, 0] + ], + exits: ['N', 'E', 'S'] +}; + +const T_WNE = { + id: 'T_WNE', + tileType: 'T', + width: 6, + height: 4, + image: '/assets/images/dungeons/T_WNE.png', + walkability: [ + [0, 0, 1, 1, 0, 0], + [0, 0, 1, 1, 0, 0], + [1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1] + ], + exits: ['W', 'N', 'E'] +}; + +const T_WSE = { + id: 'T_WSE', + tileType: 'T', + width: 6, + height: 4, + image: '/assets/images/dungeons/T_WSE.png', + walkability: [ + [1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1], + [0, 0, 1, 1, 0, 0], + [0, 0, 1, 1, 0, 0] + ], + exits: ['W', 'S', 'E'] +}; + +const T_WNS = { + id: 'T_WNS', + tileType: 'T', + width: 4, + height: 6, + image: '/assets/images/dungeons/T_WNS.png', + walkability: [ + [0, 0, 1, 1], + [0, 0, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1], + [0, 0, 1, 1], + [0, 0, 1, 1] + ], + exits: ['W', 'N', 'S'] +}; + +// ============================================================================ +// CORRIDORS (2x6 or 6x2 with 2-tile rows) +// ============================================================================ + +// Corridor 1 - East-West (horizontal) +const CORRIDOR1_EW = { + id: 'corridor1_EW', + tileType: 'corridor', + width: 6, + height: 2, + image: '/assets/images/dungeons/corridor1_EW.png', + walkability: [ + [1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1] + ], + exits: ['E', 'W'] +}; + +// Corridor 1 - North-South (vertical) +const CORRIDOR1_NS = { + id: 'corridor1_NS', + tileType: 'corridor', + width: 2, + height: 6, + image: '/assets/images/dungeons/corridor1_NS.png', + walkability: [ + [1, 1], + [1, 1], + [1, 1], + [1, 1], + [1, 1], + [1, 1] + ], + exits: ['N', 'S'] +}; + +// Corridor 2 - East-West (horizontal) +const CORRIDOR2_EW = { + id: 'corridor2_EW', + tileType: 'corridor', + width: 6, + height: 2, + image: '/assets/images/dungeons/corridor2_EW.png', + walkability: [ + [1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1] + ], + exits: ['E', 'W'] +}; + +// Corridor 2 - North-South (vertical) +const CORRIDOR2_NS = { + id: 'corridor2_NS', + tileType: 'corridor', + width: 2, + height: 6, + image: '/assets/images/dungeons/corridor2_NS.png', + walkability: [ + [1, 1], + [1, 1], + [1, 1], + [1, 1], + [1, 1], + [1, 1] + ], + exits: ['N', 'S'] +}; + +// Corridor 3 - East-West (horizontal) +const CORRIDOR3_EW = { + id: 'corridor3_EW', + tileType: 'corridor', + width: 6, + height: 2, + image: '/assets/images/dungeons/corridor3_EW.png', + walkability: [ + [1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1] + ], + exits: ['E', 'W'] +}; + +// Corridor 3 - North-South (vertical) +const CORRIDOR3_NS = { + id: 'corridor3_NS', + tileType: 'corridor', + width: 2, + height: 6, + image: '/assets/images/dungeons/corridor3_NS.png', + walkability: [ + [1, 1], + [1, 1], + [1, 1], + [1, 1], + [1, 1], + [1, 1] + ], + exits: ['N', 'S'] +}; + +// ============================================================================ +// TILE COLLECTIONS +// ============================================================================ + +export const TILE_DEFINITIONS = { + // Rooms + room_4x4_normal: ROOM_4X4_NORMAL, + room_4x4_circle: ROOM_4X4_CIRCLE, + room_4x4_skeleton: ROOM_4X4_SKELETON, + room_4x6_altar: ROOM_4X6_ALTAR, + room_4x6_tomb: ROOM_4X6_TOMB, + room_4x6_stairs: ROOM_4X6_STAIRS, + + // L-shapes + L_NE: L_NE, + L_SE: L_SE, + L_WS: L_WS, + L_WN: L_WN, + + // T-junctions + T_NES: T_NES, + T_WNE: T_WNE, + T_WSE: T_WSE, + T_WNS: T_WNS, + + // Corridors + corridor1_EW: CORRIDOR1_EW, + corridor1_NS: CORRIDOR1_NS, + corridor2_EW: CORRIDOR2_EW, + corridor2_NS: CORRIDOR2_NS, + corridor3_EW: CORRIDOR3_EW, + corridor3_NS: CORRIDOR3_NS +}; + +// Collections by type for easy filtering +export const ROOMS = [ + ROOM_4X4_NORMAL, + ROOM_4X4_CIRCLE, + ROOM_4X4_SKELETON, + ROOM_4X6_ALTAR, + ROOM_4X6_TOMB, + ROOM_4X6_STAIRS +]; + +export const L_SHAPES = [ + L_NE, + L_SE, + L_WS, + L_WN +]; + +export const T_JUNCTIONS = [ + T_NES, + T_WNE, + T_WSE, + T_WNS +]; + +export const CORRIDORS = [ + CORRIDOR1_EW, + CORRIDOR1_NS, + CORRIDOR2_EW, + CORRIDOR2_NS, + CORRIDOR3_EW, + CORRIDOR3_NS +]; + +// Helper function to get tile definition by ID +export function getTileDefinition(tileId) { + return TILE_DEFINITIONS[tileId] || null; +} + +// Helper function to get tiles by type +export function getTilesByType(tileType) { + switch (tileType) { + case 'room': + return ROOMS; + case 'L': + return L_SHAPES; + case 'T': + return T_JUNCTIONS; + case 'corridor': + return CORRIDORS; + default: + return []; + } +} diff --git a/src/main.js b/src/main.js index bdb0c78..eca5071 100644 --- a/src/main.js +++ b/src/main.js @@ -1,6 +1,55 @@ import './style.css'; import * as THREE from 'three'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; +import { io } from "socket.io-client"; +import { turnManager, PHASES } from './systems/TurnManager.js'; +import { dungeonDeck } from './dungeon/DungeonDecks.js'; +import * as TileDefinitions from './dungeon/TileDefinitions.js'; +import { eventDeck } from './dungeon/EventDeck.js'; // Import Event Deck +import { TILE_DEFINITIONS, getTileDefinition } from './dungeon/TileDefinitions.js'; + +// --- NETWORK SETUP --- +// Dynamic connection to support playing from mobile on the same network +const socketUrl = `http://${window.location.hostname}:3001`; +const socket = io(socketUrl); +let lobbyCode = null; + +socket.on("connect", () => { + console.log("Connected to Game Server!", socket.id); + socket.emit("HOST_GAME"); +}); + +socket.on("LOBBY_CREATED", ({ code }) => { + console.log("GAME HOSTED. LOBBY CODE:", code); + lobbyCode = code; + // Temporary UI for Lobby Code + const codeDisplay = document.createElement('div'); + codeDisplay.style.position = 'absolute'; + codeDisplay.style.top = '10px'; + codeDisplay.style.right = '10px'; + codeDisplay.style.color = 'gold'; + codeDisplay.style.fontFamily = 'monospace'; + codeDisplay.style.fontSize = '24px'; + codeDisplay.style.fontWeight = 'bold'; + codeDisplay.style.background = 'rgba(0,0,0,0.5)'; + codeDisplay.style.padding = '10px'; + codeDisplay.innerText = `SALA: ${code}`; + document.body.appendChild(codeDisplay); +}); + +socket.on("PLAYER_JOINED", ({ name }) => { + console.log(`Player ${name} has joined!`); + // TODO: Show notification +}); + +socket.on("PLAYER_ACTION", ({ playerId, action, data }) => { + console.log(`Action received from ${playerId}: ${action}`, data); + // Placeholder interaction + if (action === 'MOVE') { + // Example: data = { x: 1, y: 0 } + // Implement logic here + } +}); // --- CONFIGURACIÓN DE LA ESCENA --- const CONFIG = { @@ -28,225 +77,308 @@ const ASSETS = { // Sistema de salas // --- GENERADOR PROCEDURAL DE MAZMORRAS --- +// --- GENERACIÓN DE MAZMORRA (DINÁMICA) --- function generateDungeon() { - const rooms = []; - const maxRooms = 15; - - // Configuración de reglas de generación (Pesos y Límites) - const GENERATION_RULES = { - 'tile_base': { weight: 60, max: Infinity }, // 4x4 (Muy común) - 'tile_8x4': { weight: 15, max: Infinity }, // Pasillo H (Medio) - 'tile_4x8': { weight: 15, max: Infinity }, // Pasillo V (Medio) - 'tile_8x8': { weight: 10, max: 2 } // Sala Grande (Rara, max 2) + // Start with a single entry room 4x4 + const startTileDef = getTileDefinition('room_4x4_normal'); + const startRoom = { + id: 1, + tileDef: startTileDef, // Store the full tile definition + tile: { id: startTileDef.id, x: 0, y: 0 }, + walls: [], // Walls calculated dynamically later or fixed for start + doors: [ + { side: 'N', gridX: 2, gridY: 0, leadsTo: null, id: 'door_start_N', isOpen: false } + ], + entities: [ + { id: 101, type: 'hero_1', x: 2, y: 2 } + ] }; - let entityIdCounter = 100; - - // Helper para verificar si un área está libre - // Ocupación se guarda como strings "x,y" para cada bloque de 4x4 - const occupied = new Set(); - - function markOccupied(x, y, width, height) { - for (let i = 0; i < width; i += 4) { - for (let j = 0; j < height; j += 4) { - occupied.add(`${x + i},${y + j}`); - } - } - } - - function isAreaFree(x, y, width, height) { - for (let i = 0; i < width; i += 4) { - for (let j = 0; j < height; j += 4) { - if (occupied.has(`${x + i},${y + j}`)) return false; - } - } - return true; - } - - // Helper para elegir tipo de sala según pesos - function pickRandomRoomType() { - // 1. Contar cuántas de cada tipo tenemos ya - const currentCounts = {}; - Object.keys(GENERATION_RULES).forEach(k => currentCounts[k] = 0); - rooms.forEach(r => { - if (currentCounts[r.tile.type] !== undefined) { - currentCounts[r.tile.type]++; - } - }); - - // 2. Filtrar candidatos válidos (que no superen su max) - const candidates = Object.keys(GENERATION_RULES).filter(type => { - return currentCounts[type] < GENERATION_RULES[type].max; - }); - - // 3. Calcular peso total de los candidatos - const totalWeight = candidates.reduce((sum, type) => sum + GENERATION_RULES[type].weight, 0); - - // 4. Elegir aleatoriamente - let random = Math.random() * totalWeight; - - for (const type of candidates) { - random -= GENERATION_RULES[type].weight; - if (random <= 0) { - return type; - } - } - - return 'tile_base'; // Fallback por seguridad - } - - // Sala inicial (siempre 4x4 en 0,0 con el héroe) - const startTileKey = 'tile_base'; - rooms.push({ - id: 1, - tile: { type: startTileKey, x: 0, y: 0 }, - walls: ['N', 'S', 'E', 'W'], - doors: [], - entities: [{ id: entityIdCounter++, type: 'hero_1', x: 1, y: 1 }] - }); - // Marcar ocupado el área de la sala inicial - markOccupied(0, 0, ASSETS.tiles[startTileKey].width, ASSETS.tiles[startTileKey].height); - - // Direcciones posibles: N, S, E, W - // Nota: dx/dy se calcularán dinámicamente - const directions = [ - { side: 'N', opposite: 'S' }, - { side: 'S', opposite: 'N' }, - { side: 'E', opposite: 'W' }, - { side: 'W', opposite: 'E' } - ]; - - // Cola de salas para expandir - const queue = [rooms[0]]; - - while (rooms.length < maxRooms && queue.length > 0) { - const currentRoom = queue.shift(); - const currentTileDef = ASSETS.tiles[currentRoom.tile.type]; - - // Intentar añadir salas en direcciones aleatorias - const shuffledDirections = [...directions].sort(() => Math.random() - 0.5); - - for (const dir of shuffledDirections) { - if (rooms.length >= maxRooms) break; - - // Selección ponderada del tipo de sala - const nextTileType = pickRandomRoomType(); - const nextTileDef = ASSETS.tiles[nextTileType]; - - // Calcular posición de la nueva sala según la dirección - let newX, newY; - - // Estrategia de alineación: Alineamos siempre a "top-left" relativo a la dirección de crecimiento. - // Esto asegura que al menos un segmento de 4x4 coincida para poner la puerta. - if (dir.side === 'N') { - newX = currentRoom.tile.x; // Alineado a la izquierda - newY = currentRoom.tile.y - nextTileDef.height; - } else if (dir.side === 'S') { - newX = currentRoom.tile.x; // Alineado a la izquierda - newY = currentRoom.tile.y + currentTileDef.height; - } else if (dir.side === 'E') { - newX = currentRoom.tile.x + currentTileDef.width; - newY = currentRoom.tile.y; // Alineado arriba - } else if (dir.side === 'W') { - newX = currentRoom.tile.x - nextTileDef.width; - newY = currentRoom.tile.y; // Alineado arriba - } - - // Verificar si el área está libre - if (!isAreaFree(newX, newY, nextTileDef.width, nextTileDef.height)) continue; - - // 40% de probabilidad de no crear sala en esta dirección (si hay espacio) - // reducimos la probabilidad de fallo para fomentar estructura más densa con salas grandes - if (Math.random() < 0.3) continue; - - // Crear nueva sala - const newRoomId = rooms.length + 1; - - // Generar entidades (esqueletos) - // En salas grandes ponemos más bichos potencialmente - const areaFactor = (nextTileDef.width * nextTileDef.height) / 16; - const maxSkeletons = Math.floor(2 * areaFactor); - const numSkeletons = Math.floor(Math.random() * (maxSkeletons + 1)); - const newEntities = []; - - for (let i = 0; i < numSkeletons; i++) { - const randomX = newX + Math.floor(Math.random() * nextTileDef.width); - const randomY = newY + Math.floor(Math.random() * nextTileDef.height); - - newEntities.push({ - id: entityIdCounter++, - type: 'hero_2', - x: randomX, - y: randomY - }); - } - - const newRoom = { - id: newRoomId, - tile: { type: nextTileType, x: newX, y: newY }, - walls: ['N', 'S', 'E', 'W'], - doors: [], - entities: newEntities - }; - - // Añadir y marcar - rooms.push(newRoom); - markOccupied(newX, newY, nextTileDef.width, nextTileDef.height); - queue.push(newRoom); - - // CREAR PUERTAS - // Siempre ponemos la puerta en los primeros 4 tiles de la conexión, que sabemos que existen por la alineación. - // gridPos entre 1 y 2 (dejando margenes de 1 celda en bordes de 4) - const doorGridPos = Math.floor(Math.random() * 2) + 1; - - // Puerta en la sala actual (origen) - // Ojo: gridX/Y es relativo al origen de la sala. - // Como alineamos coordenadas: - // N/S: Alineados en X -> puerta en X relativo es igual para ambos. - // 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 }; - - 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 }; - - newRoom.doors.push(oppositeDoorConfig); - } - } - - // Limpiar puertas inválidas (paranoia check) - const existingRoomIds = new Set(rooms.map(r => r.id)); - rooms.forEach(room => { - room.doors = room.doors.filter(door => existingRoomIds.has(door.leadsTo)); - }); - return { - rooms: rooms, + rooms: [startRoom], visitedRooms: new Set([1]), currentRoom: 1 }; } +// --- EXPLORACIÓN DINÁMICA --- +function exploreRoom(originRoom, door) { + // Draw an event card + const eventCard = eventDeck.drawCard(); + if (eventCard) { + console.log("Event Card Drawn:", eventCard); + showUIEvent(eventCard); + } + + // Determine entry side (opposite of exit) + let entrySide; + if (door.side === 'N') entrySide = 'S'; + else if (door.side === 'S') entrySide = 'N'; + else if (door.side === 'E') entrySide = 'W'; + else if (door.side === 'W') entrySide = 'E'; + + // Try to draw a compatible abstract tile type (up to 10 attempts) + let card = null; + let alignmentOffset = 0; + let attempts = 0; + const maxAttempts = 10; + + while (attempts < maxAttempts && !card) { + const abstractCard = dungeonDeck.drawCompatibleCard(originRoom.tileDef.tileType); + if (!abstractCard) { + console.warn("Could not draw compatible card"); + return null; + } + + console.log(`Drew abstract type: ${abstractCard.type}`); + + // Select concrete tile variant based on abstract type + let candidates = []; + + switch (abstractCard.type) { + case 'room_4x4': + candidates = TileDefinitions.ROOMS.filter(r => r.width === 4 && r.height === 4); + break; + case 'room_4x6': + candidates = TileDefinitions.ROOMS.filter(r => r.width === 4 && r.height === 6); + break; + case 'corridor': + // Filter by orientation (EW or NS based on exit direction) + const isExitHorizontal = door.side === 'E' || door.side === 'W'; + candidates = TileDefinitions.CORRIDORS.filter(c => { + const isCorridorHorizontal = c.exits.includes('E') && c.exits.includes('W'); + return isExitHorizontal === isCorridorHorizontal; + }); + break; + case 'L': + candidates = [...TileDefinitions.L_SHAPES]; + break; + case 'T': + candidates = [...TileDefinitions.T_JUNCTIONS]; + break; + } + + if (candidates.length === 0) { + console.warn(`No candidates found for type ${abstractCard.type}`); + attempts++; + continue; + } + + // Try all candidates and collect those that fit + const fittingVariants = []; + for (const variant of candidates) { + const connectionResult = canConnectTiles(originRoom, variant, door.side); + if (connectionResult.valid) { + fittingVariants.push({ variant, offset: connectionResult.offset }); + } + } + + if (fittingVariants.length > 0) { + // RANDOM selection from fitting variants + const selected = fittingVariants[Math.floor(Math.random() * fittingVariants.length)]; + card = selected.variant; + alignmentOffset = selected.offset; + console.log(`✓ Selected ${card.id} (${card.tileType}) randomly from ${fittingVariants.length} fitting variants, offset ${alignmentOffset}`); + } else { + console.log(`✗ No ${abstractCard.type} variant fits, trying another tile type...`); + attempts++; + } + } + + if (!card) { + console.error("Could not find valid tile after", maxAttempts, "attempts"); + return null; + } + + const nextTileDef = card; + const newRoomId = ROOMS.rooms.length + 1; + + // Calculate entry door position in the new tile + let entryGridX, entryGridY; + + if (entrySide === 'N') { + entryGridX = Math.floor(nextTileDef.width / 2); + entryGridY = 0; + } else if (entrySide === 'S') { + entryGridX = Math.floor(nextTileDef.width / 2); + entryGridY = nextTileDef.height; + } else if (entrySide === 'E') { + entryGridX = nextTileDef.width; + entryGridY = Math.floor(nextTileDef.height / 2); + } else if (entrySide === 'W') { + entryGridX = 0; + entryGridY = Math.floor(nextTileDef.height / 2); + } + + // Calculate absolute position for the new tile + let newX = originRoom.tile.x + door.gridX - entryGridX; + let newY = originRoom.tile.y + door.gridY - entryGridY; + + // Apply alignment offset based on exit direction + if (door.side === 'N' || door.side === 'S') { + // Vertical connection - offset horizontally + newX += alignmentOffset; + } else { + // Horizontal connection - offset vertically + newY += alignmentOffset; + } + + // Check for collisions + if (!isAreaFree(newX, newY, nextTileDef.width, nextTileDef.height)) { + console.warn("Cannot place room: Collision detected!"); + return null; + } + + const newRoom = { + id: newRoomId, + tileDef: nextTileDef, + tile: { id: nextTileDef.id, x: newX, y: newY }, + walls: ['N', 'S', 'E', 'W'], + doors: [], + entities: [] + }; + + // Determine if we should place a door or open connection + const placeDoor = shouldPlaceDoor(originRoom.tileDef.tileType, nextTileDef.tileType); + + // Create the entry door/connection + const entryDoor = { + side: entrySide, + leadsTo: originRoom.id, + isOpen: !placeDoor, // Open if no door, closed if door + isDoor: placeDoor, + id: `door_${newRoomId}_to_${originRoom.id}`, + gridX: entryGridX, + gridY: entryGridY + }; + + newRoom.doors.push(entryDoor); + + // Generate additional exits based on the tile definition + nextTileDef.exits.forEach(exitDir => { + if (exitDir === entrySide) return; // Already have this connection + + const exitDoor = { + side: exitDir, + leadsTo: null, + isOpen: false, + isDoor: true, // Will be determined when connected + id: `door_${newRoomId}_${exitDir}` + }; + + // Calculate door coordinates + if (exitDir === 'N') { + exitDoor.gridX = Math.floor(nextTileDef.width / 2); + exitDoor.gridY = 0; + } else if (exitDir === 'S') { + exitDoor.gridX = Math.floor(nextTileDef.width / 2); + exitDoor.gridY = nextTileDef.height; + } else if (exitDir === 'E') { + exitDoor.gridX = nextTileDef.width; + exitDoor.gridY = Math.floor(nextTileDef.height / 2); + } else if (exitDir === 'W') { + exitDoor.gridX = 0; + exitDoor.gridY = Math.floor(nextTileDef.height / 2); + } + + newRoom.doors.push(exitDoor); + }); + + ROOMS.rooms.push(newRoom); + console.log(`✓ Tile ${newRoomId} (${nextTileDef.tileType}) created: ${nextTileDef.id} at (${newX}, ${newY})`); + return newRoom; +} + const ROOMS = generateDungeon(); +// --- TURN SYSTEM UI --- +const phaseDisplay = document.createElement('div'); +phaseDisplay.style.position = 'absolute'; +phaseDisplay.style.top = '10px'; +phaseDisplay.style.left = '50%'; +phaseDisplay.style.transform = 'translateX(-50%)'; +phaseDisplay.style.color = '#fff'; +phaseDisplay.style.fontSize = '24px'; +phaseDisplay.style.fontWeight = 'bold'; +phaseDisplay.style.textShadow = '0 0 5px #000'; +phaseDisplay.style.pointerEvents = 'none'; +document.body.appendChild(phaseDisplay); + +function showUIEvent(event) { + const toast = document.createElement('div'); + toast.style.position = 'absolute'; + toast.style.top = '20%'; + toast.style.left = '50%'; + toast.style.transform = 'translateX(-50%)'; + toast.style.background = 'rgba(0, 0, 0, 0.8)'; + toast.style.color = '#ffcc00'; + toast.style.padding = '20px'; + toast.style.border = '2px solid #ffcc00'; + toast.style.borderRadius = '10px'; + toast.style.textAlign = 'center'; + toast.style.zIndex = '1000'; + + toast.innerHTML = ` +

${event.title}

+

${event.description}

+ ${event.type} + `; + + document.body.appendChild(toast); + + setTimeout(() => { + toast.remove(); + }, 4000); +} + +const PHASE_TRANSLATIONS = { + 'POWER': 'PODER', + 'HERO': 'HÉROES', + 'EXPLORATION': 'EXPLORACIÓN', + 'MONSTER': 'MONSTRUOS', + 'END': 'FIN' +}; + +turnManager.addEventListener('phaseChange', (e) => { + const phaseName = PHASE_TRANSLATIONS[e.detail] || e.detail; + phaseDisplay.innerText = `FASE: ${phaseName}`; + + // Example: Update lighting or UI based on phase + if (e.detail === PHASES.EXPLORATION) { + console.log("Entering Exploration Mode - Waiting for door interaction..."); + } +}); + +turnManager.addEventListener('message', (e) => { + // Show smaller toast for messages + const msg = document.createElement('div'); + msg.style.position = 'absolute'; + msg.style.bottom = '100px'; + msg.style.left = '50%'; + msg.style.transform = 'translateX(-50%)'; + msg.style.color = '#fff'; + msg.style.textShadow = '0 0 2px #000'; + msg.innerText = e.detail; + document.body.appendChild(msg); + setTimeout(() => msg.remove(), 2000); +}); + +turnManager.addEventListener('eventTriggered', (e) => { + showUIEvent(e.detail.event); +}); + +// Start the game loop +turnManager.startTurn(); + 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 --- @@ -469,7 +601,9 @@ function isAdjacent(p1, p2) { // Verificar si una posición está dentro de una sala function isPositionInRoom(x, y, room) { const tile = room.tile; - const tileDef = ASSETS.tiles[tile.type]; + const tileDef = room.tileDef; + if (!tileDef) return false; + const minX = tile.x; const maxX = tile.x + tileDef.width - 1; const minY = tile.y; @@ -478,6 +612,36 @@ function isPositionInRoom(x, y, room) { return x >= minX && x <= maxX && y >= minY && y <= maxY; } +// Verificar colisión entre un rectángulo propuesto y las salas existentes +// x, y: Coordenadas Grid (Top-Left) +// width, height: Dimensiones Grid +function isAreaFree(x, y, width, height) { + const aMinX = x; + const aMaxX = x + width; + const aMinY = y; + const aMaxY = y + height; + + for (const room of ROOMS.rooms) { + const tileDef = room.tileDef; + if (!tileDef) continue; + + // Rectángulo B (Existente) + const bMinX = room.tile.x; + const bMaxX = room.tile.x + tileDef.width; + const bMinY = room.tile.y; + const bMaxY = room.tile.y + tileDef.height; + + // Check Overlap (Intersección de AABB) + const noOverlap = aMaxX <= bMinX || aMinX >= bMaxX || aMaxY <= bMinY || aMinY >= bMaxY; + + if (!noOverlap) { + console.log(`Collision detected with Room ${room.id} [${bMinX},${bMinY},${bMaxX},${bMaxY}] vs New [${aMinX},${aMinY},${aMaxX},${aMaxY}]`); + return false; + } + } + return true; +} + // Verificar si una posición es una puerta function isPositionDoor(x, y, room) { for (const door of room.doors) { @@ -489,25 +653,281 @@ function isPositionDoor(x, y, room) { return false; } -// Verificar si una celda es transitable + + +// Verificar si una celda es transitable usando la matriz de walkability 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; + if (!room || !room.tileDef) continue; - // Si está dentro de la sala, es transitable if (isPositionInRoom(x, y, room)) { - return true; + // Get local coordinates within the tile + const localX = x - room.tile.x; + const localY = y - room.tile.y; + + // Check walkability matrix + const walkValue = room.tileDef.walkability[localY][localX]; + return walkValue > 0; // 0 = not walkable, >0 = walkable } - // 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; +} + +// Get the layer/height of a specific position +function getTileLayer(x, y) { + for (const roomId of ROOMS.visitedRooms) { + const room = ROOMS.rooms.find(r => r.id === roomId); + if (!room || !room.tileDef) continue; + + if (isPositionInRoom(x, y, room)) { + const localX = x - room.tile.x; + const localY = y - room.tile.y; + + return room.tileDef.walkability[localY][localX]; + } + } + return 0; // Not in any room +} + +// Check if movement between two positions with different layers is allowed +function canTransitionLayers(fromX, fromY, toX, toY) { + const fromLayer = getTileLayer(fromX, fromY); + const toLayer = getTileLayer(toX, toY); + + // Same layer or one is a stair + if (fromLayer === toLayer || fromLayer === 9 || toLayer === 9) { + return true; + } + + // Different layers - check if there's a stair adjacent + const layerDiff = Math.abs(fromLayer - toLayer); + if (layerDiff > 1) { + return false; // Can't jump more than 1 layer + } + + // Check if there's a stair (9) adjacent to either position + const adjacentPositions = [ + { x: fromX - 1, y: fromY }, + { x: fromX + 1, y: fromY }, + { x: fromX, y: fromY - 1 }, + { x: fromX, y: fromY + 1 }, + { x: toX - 1, y: toY }, + { x: toX + 1, y: toY }, + { x: toX, y: toY - 1 }, + { x: toX, y: toY + 1 } + ]; + + for (const pos of adjacentPositions) { + if (getTileLayer(pos.x, pos.y) === 9) { + return true; // Found a stair } } - return false; + return false; // No stair found +} + +// Determine if a door should be placed between two tile types +function shouldPlaceDoor(tileTypeA, tileTypeB) { + // Doors only between Room ↔ Corridor + return (tileTypeA === 'room' && tileTypeB === 'corridor') || + (tileTypeA === 'corridor' && tileTypeB === 'room'); +} + +// Validate walkability alignment between two tiles at their connection point +// Returns: { valid: boolean, offset: number } - offset is how much to shift tileB to align with tileA +function validateWalkabilityAlignment(tileDefA, posA, tileDefB, posB, exitSide) { + // Get the edge cells that will connect + const edgeA = getEdgeCells(tileDefA, exitSide); + const edgeB = getEdgeCells(tileDefB, getOppositeSide(exitSide)); + + console.log(`[ALIGN] Checking ${tileDefA.id} (${exitSide}) → ${tileDefB.id}`); + console.log(`[ALIGN] EdgeA (${tileDefA.id}):`, edgeA); + console.log(`[ALIGN] EdgeB (${tileDefB.id}):`, edgeB); + + // Special handling for corridor connections + // Corridors are 2 tiles wide, rooms/L/T are typically 4 tiles wide + // We need to find where the corridor's walkable area aligns with the room's walkable area + + if (edgeA.length !== edgeB.length) { + // Different edge lengths - need to find alignment + const smallerEdge = edgeA.length < edgeB.length ? edgeA : edgeB; + const largerEdge = edgeA.length < edgeB.length ? edgeB : edgeA; + const isASmaller = edgeA.length < edgeB.length; + + console.log(`[ALIGN] Different sizes: ${edgeA.length} vs ${edgeB.length}`); + console.log(`[ALIGN] isASmaller: ${isASmaller}`); + + // Find walkable cells in smaller edge + const smallerWalkable = smallerEdge.filter(cell => cell > 0); + if (smallerWalkable.length === 0) { + console.warn('[ALIGN] No walkable cells in smaller edge'); + return { valid: false, offset: 0 }; + } + + const smallerWidth = smallerEdge.length; + const largerWidth = largerEdge.length; + + // FIRST: Try offset 0 (no displacement needed) + let validAtZero = true; + for (let i = 0; i < smallerWidth; i++) { + const smallCell = smallerEdge[i]; + const largeCell = largerEdge[i]; + + const isSmallWalkable = smallCell > 0; + const isLargeWalkable = largeCell > 0; + + console.log(`[ALIGN] Offset 0, index ${i}: small=${smallCell}(${isSmallWalkable}) vs large=${largeCell}(${isLargeWalkable})`); + + if (isSmallWalkable !== isLargeWalkable) { + validAtZero = false; + console.log(`[ALIGN] ❌ Offset 0 FAILED at index ${i}: walkability mismatch`); + break; + } + } + + if (validAtZero) { + console.log(`✓ [ALIGN] Valid alignment at offset 0 (no displacement needed)`); + return { valid: true, offset: 0 }; + } + + // If offset 0 doesn't work, try offset of 2 (corridor width) + // This aligns the corridor with the other walkable section of the L/T + const offset = 2; + if (offset <= largerWidth - smallerWidth) { + let valid = true; + for (let i = 0; i < smallerWidth; i++) { + const smallCell = smallerEdge[i]; + const largeCell = largerEdge[offset + i]; + + const isSmallWalkable = smallCell > 0; + const isLargeWalkable = largeCell > 0; + + if (isSmallWalkable !== isLargeWalkable) { + valid = false; + console.log(`[ALIGN] Offset ${offset} failed at index ${i}: ${smallCell} vs ${largeCell}`); + break; + } + } + + if (valid) { + // Calculate final offset + // If A (corridor) is smaller than B (L/T), we need to shift B by +offset + // If B is smaller than A, we need to shift B by -offset + const finalOffset = isASmaller ? offset : -offset; + console.log(`✓ [ALIGN] Valid alignment at offset ${offset}, final offset: ${finalOffset} (isASmaller: ${isASmaller})`); + return { valid: true, offset: finalOffset }; + } + } + + console.warn('[ALIGN] Could not find valid alignment for edges of different sizes'); + return { valid: false, offset: 0 }; + } + + // Same length - check direct alignment + for (let i = 0; i < edgeA.length; i++) { + const cellA = edgeA[i]; + const cellB = edgeB[i]; + + // Rule: Cannot connect 0 (not walkable) with >0 (walkable) + const isAWalkable = cellA > 0; + const isBWalkable = cellB > 0; + + if (isAWalkable !== isBWalkable) { + console.warn(`[ALIGN] Walkability mismatch at index ${i}: ${cellA} vs ${cellB}`); + return { valid: false, offset: 0 }; + } + } + + return { valid: true, offset: 0 }; +} + +// Get edge cells from a tile definition for a given side +function getEdgeCells(tileDef, side) { + const { walkability, width, height } = tileDef; + const cells = []; + + switch (side) { + case 'N': + // Top row + for (let x = 0; x < width; x++) { + cells.push(walkability[0][x]); + } + break; + case 'S': + // Bottom row + for (let x = 0; x < width; x++) { + cells.push(walkability[height - 1][x]); + } + break; + case 'E': + // Right column + for (let y = 0; y < height; y++) { + cells.push(walkability[y][width - 1]); + } + break; + case 'W': + // Left column + for (let y = 0; y < height; y++) { + cells.push(walkability[y][0]); + } + break; + } + + return cells; +} + +// Get opposite side +function getOppositeSide(side) { + const opposites = { 'N': 'S', 'S': 'N', 'E': 'W', 'W': 'E' }; + return opposites[side]; +} + +// Check if two tiles can connect based on type rules and walkability alignment +// Returns: { valid: boolean, offset: number } - offset for positioning the new tile +function canConnectTiles(roomA, tileDefB, exitSide) { + const tileDefA = roomA.tileDef; + + // Check type compatibility + const typeA = tileDefA.tileType; + const typeB = tileDefB.tileType; + + const validConnections = { + 'room': ['room', 'corridor'], + 'corridor': ['room', 'corridor', 'L', 'T'], + 'L': ['corridor'], + 'T': ['corridor'] + }; + + if (!validConnections[typeA] || !validConnections[typeA].includes(typeB)) { + console.warn(`Invalid connection: ${typeA} cannot connect to ${typeB}`); + return { valid: false, offset: 0 }; + } + + // CRITICAL: Check that tileB has an exit in the opposite direction + // If we exit through N, the new tile must have S in its exits + const requiredExit = getOppositeSide(exitSide); + if (!tileDefB.exits.includes(requiredExit)) { + console.warn(`Exit direction mismatch: ${tileDefB.id} doesn't have required exit '${requiredExit}' (exiting via '${exitSide}')`); + return { valid: false, offset: 0 }; + } + + // Check walkability alignment and get offset + const alignmentResult = validateWalkabilityAlignment(tileDefA, roomA.tile, tileDefB, null, exitSide); + if (!alignmentResult.valid) { + console.warn('Walkability alignment failed'); + return { valid: false, offset: 0 }; + } + + return alignmentResult; } // --- CREACIÓN DE MARCADORES --- @@ -562,7 +982,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 +1000,178 @@ 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); + const originalDoorId = targetDoor.id; // Guardar ID original para buscar el mesh luego + targetDoor.isOpen = true; + + // Revelar sala destino (o generarla si no existe) + if (!targetDoor.leadsTo) { + // FASE DE EXPLORACIÓN: Generar nueva sala + console.log("Explorando nueva zona..."); + turnManager.setPhase(PHASES.EXPLORATION); + const newRoom = exploreRoom(originRoom, targetDoor); + if (newRoom) { + targetDoor.leadsTo = newRoom.id; + targetDoor.id = `door_${originRoom.id}_to_${newRoom.id}`; // Update ID + + // Actualizar también la puerta inversa en la nueva sala + const oppDoor = newRoom.doors.find(d => d.leadsTo === originRoom.id); + if (oppDoor) { + oppDoor.isOpen = true; + } + } else { + console.log("No se pudo generar sala (bloqueado)"); + // Feedback visual de bloqueo + const toast = document.createElement('div'); + toast.style.position = 'absolute'; + toast.style.top = '50%'; + toast.style.left = '50%'; + toast.style.transform = 'translate(-50%, -50%)'; + toast.style.background = 'rgba(100, 0, 0, 0.8)'; + toast.style.color = 'white'; + toast.style.padding = '20px'; + toast.style.border = '2px solid red'; + toast.style.fontSize = '20px'; + toast.innerText = "¡CAMINO BLOQUEADO!"; + document.body.appendChild(toast); + setTimeout(() => toast.remove(), 2000); + + targetDoor.isOpen = false; // Mantener cerrada + targetDoor.isBlocked = true; // Marcar como bloqueada permanentemente + + // Resetear estado + SESSION.selectedDoorId = null; + updateSelectionVisuals(); + closeDoorModal(); + return; // Cancelar apertura + } + } + + const targetRoom = ROOMS.rooms.find(r => r.id === targetDoor.leadsTo); + + // Revelar sala destino + if (targetRoom && !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 usando el ID ORIGINAL (porque el del objeto puede haber cambiado) + if (SESSION.roomMeshes[originRoom.id]) { + const doorMesh = SESSION.roomMeshes[originRoom.id].doors.find(m => m.userData.id === originalDoorId); + if (doorMesh) { + doorMesh.visible = false; // "Abrir" visualmente desapareciendo + doorMesh.userData.id = targetDoor.id; // Sincronizar ID del mesh con el nuevo ID + } + } + + // 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)) { + if (targetDoor.isBlocked) { + // Mostrar aviso de bloqueo + const toast = document.createElement('div'); + toast.style.position = 'absolute'; + toast.style.top = '50%'; + toast.style.left = '50%'; + toast.style.transform = 'translate(-50%, -50%)'; + toast.style.background = 'rgba(100, 0, 0, 0.8)'; + toast.style.color = 'white'; + toast.style.padding = '10px'; + toast.style.border = '1px solid red'; + toast.innerText = "¡Puerta bloqueada!"; + document.body.appendChild(toast); + setTimeout(() => toast.remove(), 1000); + } else { + openDoorModal(); + } + } + } } // --- ANIMACIÓN DE MOVIMIENTO --- @@ -612,7 +1204,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 +1219,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 +1246,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 +1259,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 +1271,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 @@ -741,10 +1337,17 @@ function checkDoorTransition(unit, currentRoom) { } } + function getDoorGridPosition(room, door) { const tile = room.tile; - const tileWidth = ASSETS.tiles[tile.type].width; - const tileHeight = ASSETS.tiles[tile.type].height; + const tileDef = room.tileDef; + if (!tileDef) { + console.error("Room", room.id, "has no tileDef in getDoorGridPosition!"); + return { x: tile.x, y: tile.y }; + } + + const tileWidth = tileDef.width; + const tileHeight = tileDef.height; switch (door.side) { case 'N': @@ -758,6 +1361,63 @@ function getDoorGridPosition(room, door) { } } +// Calcula la posición completa de la puerta en el mundo 3D +// Devuelve: { worldPos: {x, z}, meshPos: {x, y, z}, rotation: number, wallOffset: number } +function getDoorWorldPosition(room, door, centerX, centerZ, halfSizeX, halfSizeZ) { + const doorGridPos = getDoorGridPosition(room, door); + const doorWorldPos = gridToWorld(doorGridPos.x, doorGridPos.y); + + const doorHeight = 2.0; + let meshPos = { x: 0, y: doorHeight / 2, z: 0 }; + let rotation = 0; + let wallOffset = 0; + + switch (door.side) { + case 'N': + // Pared Norte: puerta alineada en X, Z en el borde norte + meshPos.x = doorWorldPos.x; + meshPos.z = centerZ - halfSizeZ; + rotation = 0; + // Offset relativo al centro de la pared (para el hueco) + wallOffset = doorWorldPos.x - centerX; + break; + case 'S': + // Pared Sur: puerta alineada en X, Z en el borde sur + meshPos.x = doorWorldPos.x; + meshPos.z = centerZ + halfSizeZ; + rotation = 0; + // Para pared Sur, el offset es directo (sin inversión) + wallOffset = doorWorldPos.x - centerX; + break; + case 'E': + // Pared Este: puerta alineada en Z, X en el borde este + meshPos.x = centerX + halfSizeX; + meshPos.z = doorWorldPos.z; + rotation = Math.PI / 2; + // Offset relativo al centro de la pared + // Con rotation=π/2, el eje X local apunta hacia -Z, entonces: + // offset_local = -(doorZ - centerZ) + wallOffset = -(doorWorldPos.z - centerZ); + break; + case 'W': + // Pared Oeste: puerta alineada en Z, X en el borde oeste + meshPos.x = centerX - halfSizeX; + meshPos.z = doorWorldPos.z; + rotation = Math.PI / 2; + // Con rotation=π/2, el eje X local apunta hacia -Z (igual que pared E) + // Por tanto, también necesita offset invertido + wallOffset = -(doorWorldPos.z - centerZ); + break; + } + + return { + worldPos: doorWorldPos, + meshPos: meshPos, + rotation: rotation, + wallOffset: wallOffset + }; +} + // --- CARGA Y RENDERIZADO --- const textureLoader = new THREE.TextureLoader(); @@ -790,21 +1450,22 @@ async function renderRoom(room) { entities: [] }; - // Renderizar tile - // Renderizar tile - const tileDef = ASSETS.tiles[room.tile.type]; - const baseTex = await loadTexture(tileDef.src); + // Renderizar tile usando la nueva definición + const tileDef = room.tileDef; + if (!tileDef) { + console.error("Room", room.id, "has no tileDef!"); + return; + } + + const baseTex = await loadTexture(tileDef.image); const tileTex = baseTex.clone(); // CLONAR para no afectar a otras salas tileTex.needsUpdate = true; // Asegurar que Three.js sepa que es nueva tileTex.wrapS = THREE.RepeatWrapping; tileTex.wrapT = THREE.RepeatWrapping; - // Lógica de repetición: La textura base es de 4x4 celdas. - // Si la sala es 8x4, repetimos 2 en X, 1 en Y. - const repeatX = tileDef.width / 4; - const repeatY = tileDef.height / 4; - tileTex.repeat.set(repeatX, repeatY); + // No repetir la textura - cada tile tiene su propia imagen completa + tileTex.repeat.set(1, 1); const worldWidth = tileDef.width * CONFIG.CELL_SIZE; const worldHeight = tileDef.height * CONFIG.CELL_SIZE; @@ -821,10 +1482,14 @@ async function renderRoom(room) { tileMesh.receiveShadow = true; const originPos = gridToWorld(room.tile.x, room.tile.y); + console.log(`[DEBUG] renderRoom ${room.id} | Tile:`, room.tile, `| WorldOrigin:`, originPos); + tileMesh.position.x = originPos.x + (worldWidth / 2) - (CONFIG.CELL_SIZE / 2); tileMesh.position.z = originPos.z + (worldHeight / 2) - (CONFIG.CELL_SIZE / 2); tileMesh.position.y = 0; + console.log(`[DEBUG] renderRoom ${room.id} | MeshPos:`, tileMesh.position); + scene.add(tileMesh); roomMeshes.tile = tileMesh; @@ -848,37 +1513,105 @@ 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; + + // Usar función unificada para obtener la posición de la puerta + const doorInfo = getDoorWorldPosition(room, door, centerX, centerZ, halfSizeX, halfSizeZ); + const doorOffset = doorInfo.wallOffset; + + 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) + const lintelHeight = wallHeight - doorHeight; + if (lintelHeight > 0) { + createWallSegment(doorWidth, lintelHeight, doorOffset, doorHeight + (lintelHeight / 2), opacity, "Lintel"); + } } } @@ -888,12 +1621,8 @@ async function renderRoom(room) { const doorHeight = 2.0; for (const door of room.doors) { - // Verificar que la sala destino existe - const targetRoom = ROOMS.rooms.find(r => r.id === door.leadsTo); - if (!targetRoom) { - console.warn(`Puerta en sala ${room.id} apunta a sala inexistente ${door.leadsTo}`); - continue; // Saltar esta puerta - } + // Renderizar puerta independientemente de si tiene destino conocido o no + // (Las puertas leadsTo: null son zonas inexploradas) const doorGeometry = new THREE.PlaneGeometry(doorWidth, doorHeight); const doorMaterial = new THREE.MeshStandardMaterial({ @@ -904,27 +1633,13 @@ async function renderRoom(room) { }); const doorMesh = new THREE.Mesh(doorGeometry, doorMaterial); - const doorGridPos = getDoorGridPosition(room, door); - const doorWorldPos = gridToWorld(doorGridPos.x, doorGridPos.y); + doorMesh.userData.id = door.id; + doorMesh.visible = !door.isOpen; // Ocultar si ya está abierta - switch (door.side) { - case 'N': - doorMesh.position.set(doorWorldPos.x, doorHeight / 2, centerZ - halfSizeZ + 0.05); - doorMesh.rotation.y = 0; - break; - case 'S': - doorMesh.position.set(doorWorldPos.x, doorHeight / 2, centerZ + halfSizeZ - 0.05); - doorMesh.rotation.y = 0; - break; - case 'E': - doorMesh.position.set(centerX + halfSizeX - 0.05, doorHeight / 2, doorWorldPos.z); - doorMesh.rotation.y = Math.PI / 2; - break; - case 'W': - doorMesh.position.set(centerX - halfSizeX + 0.05, doorHeight / 2, doorWorldPos.z); - doorMesh.rotation.y = Math.PI / 2; - break; - } + // Usar función unificada para posicionar la puerta + const doorInfo = getDoorWorldPosition(room, door, centerX, centerZ, halfSizeX, halfSizeZ); + doorMesh.position.set(doorInfo.meshPos.x, doorInfo.meshPos.y, doorInfo.meshPos.z); + doorMesh.rotation.y = doorInfo.rotation; scene.add(doorMesh); roomMeshes.doors.push(doorMesh); @@ -984,7 +1699,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'); } @@ -1008,7 +1723,8 @@ function drawMinimap() { let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity; ROOMS.rooms.forEach(room => { - const tileDef = ASSETS.tiles[room.tile.type]; + const tileDef = room.tileDef; + if (!tileDef) return; minX = Math.min(minX, room.tile.x); maxX = Math.max(maxX, room.tile.x + tileDef.width); minY = Math.min(minY, room.tile.y); @@ -1040,7 +1756,8 @@ function drawMinimap() { // 2. Dibujar TODAS las Salas ROOMS.rooms.forEach(room => { - const tileDef = ASSETS.tiles[room.tile.type]; + const tileDef = room.tileDef; + if (!tileDef) return; const pos = toCanvas(room.tile.x, room.tile.y); const w = tileDef.width * scale; @@ -1090,12 +1807,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 +1852,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 +1860,41 @@ window.addEventListener('pointerdown', (event) => { } } + // Detectar click en puertas + const allDoors = []; + Object.values(SESSION.roomMeshes).forEach(roomData => { + if (roomData.doors) { + // Solo incluir puertas visibles (cerradas) en el raycast + allDoors.push(...roomData.doors.filter(door => door.visible)); + } + }); + + 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); diff --git a/src/main_old.js b/src/main_old.js deleted file mode 100644 index 553be67..0000000 --- a/src/main_old.js +++ /dev/null @@ -1,596 +0,0 @@ -import './style.css'; -import * as THREE from 'three'; -import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; - -// --- CONFIGURACIÓN DE LA ESCENA --- -const CONFIG = { - CELL_SIZE: 2, // Unidades de Three.js por celda lógica - TILE_DIMENSIONS: 4, // Una tile es de 4x4 celdas -}; - -// --- ESTADO DEL JUEGO (DATA MODEL) --- -const ASSETS = { - tiles: { - 'tile_base': { src: '/assets/images/tiles/tile4x4.png', width: 4, height: 4 }, - 'tile_cyan': { src: '/assets/images/tiles/tile4x4_blue.png', width: 4, height: 4 }, - 'tile_orange': { src: '/assets/images/tiles/tile4x4_orange.png', width: 4, height: 4 }, - 'tile_8x2': { src: '/assets/images/tiles/tile4x4.png', width: 8, height: 2 }, - 'wall_1': { src: '/assets/images/tiles/pared1.png' }, - 'door_1': { src: '/assets/images/tiles/puerta1.png' }, - }, - standees: { - 'hero_1': { src: '/assets/images/standees/barbaro.png', height: 3 }, - 'hero_2': { src: '/assets/images/standees/esqueleto.png', height: 3 }, - } -}; - -// Sistema de salas -const ROOMS = { - rooms: [ - { - id: 1, - tile: { type: 'tile_base', x: 0, y: 0 }, - walls: ['N', 'S', 'E', 'W'], - doors: [ - { side: 'N', gridPos: { x: 1, y: -1 }, leadsTo: 2 } - ], - entities: [{ id: 101, type: 'hero_1', x: 1, y: 1 }] - }, - { - id: 2, - tile: { type: 'tile_cyan', x: 0, y: -4 }, - walls: ['N', 'S', 'E', 'W'], - doors: [ - { side: 'S', gridPos: { x: 1, y: -1 }, leadsTo: 1 } - ], - entities: [{ id: 102, type: 'hero_2', x: 1, y: -5 }] - }, - { - id: 3, - tile: { type: 'tile_orange', x: -4, y: 0 }, - walls: ['N', 'S', 'E', 'W'], - doors: [ - { side: 'E', gridPos: { x: -1, y: 1 }, leadsTo: 1 } - ], - entities: [] - } - ], - visitedRooms: [1], // Empezamos en la sala 1 - currentRoom: 1 -}; - -const GAME_STATE = { - placedTiles: [], - entities: [] -}; - -// State de la sesión (UI) -const SESSION = { - selectedUnitId: null, - path: [], // Array de {x, y} - pathMeshes: [], // Array de meshes visuales - roomMeshes: {}, // { roomId: { tile: mesh, walls: [], doors: [], entities: [] } } - isAnimating: false // Flag para bloquear interacciones durante animación -}; - -// --- CONFIGURACIÓN BÁSICA THREE.JS --- -const scene = new THREE.Scene(); -scene.background = new THREE.Color(0x202020); - -// Renderer -const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); -renderer.setSize(window.innerWidth, window.innerHeight); -renderer.shadowMap.enabled = true; -document.querySelector('#app').appendChild(renderer.domElement); - -// Cámara -const aspect = window.innerWidth / window.innerHeight; -const d = 15; -const camera = new THREE.OrthographicCamera(-d * aspect, d * aspect, d, -d, 1, 1000); -camera.position.set(20, 20, 20); -camera.lookAt(scene.position); - -// --- CONTROLES MODIFICADOS --- -// Roto con el ratón derecho, zoom con la rueda del ratón y si hago presión en la rueda, hago el paneo. -const controls = new OrbitControls(camera, renderer.domElement); -controls.enableDamping = true; -controls.dampingFactor = 0.05; -controls.screenSpacePanning = true; -controls.maxPolarAngle = Math.PI / 2; - -// Reasignación de botones -controls.mouseButtons = { - LEFT: null, // Dejamos el click izquierdo libre para nuestra lógica - MIDDLE: THREE.MOUSE.PAN, // Paneo con botón central/rueda - RIGHT: THREE.MOUSE.ROTATE // Rotación con derecho -}; -controls.zoomToCursor = true; // Zoom a donde apunta el ratón - -// Luces -const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); -scene.add(ambientLight); - -const dirLight = new THREE.DirectionalLight(0xffffff, 0.8); -dirLight.position.set(10, 20, 5); -dirLight.castShadow = true; -scene.add(dirLight); - -const gridHelper = new THREE.GridHelper(40, 40, 0x444444, 0x111111); -scene.add(gridHelper); - -// Plano invisible para Raycasting en Y=0 -const planeGeometry = new THREE.PlaneGeometry(200, 200); -const planeMaterial = new THREE.MeshBasicMaterial({ visible: false }); -const raycastPlane = new THREE.Mesh(planeGeometry, planeMaterial); -raycastPlane.rotation.x = -Math.PI / 2; -scene.add(raycastPlane); - - -// --- HELPERS LÓGICOS --- -function worldToGrid(x, z) { - return { - x: Math.floor(x / CONFIG.CELL_SIZE), - y: Math.floor(z / CONFIG.CELL_SIZE) - }; -} - -function gridToWorld(gridX, gridY) { - return { - x: (gridX * CONFIG.CELL_SIZE) + (CONFIG.CELL_SIZE / 2), - z: (gridY * CONFIG.CELL_SIZE) + (CONFIG.CELL_SIZE / 2) - }; -} - -function isAdjacent(p1, p2) { - const dx = Math.abs(p1.x - p2.x); - const dy = Math.abs(p1.y - p2.y); - // Adyacencia ortogonal (cruz) - return (dx === 1 && dy === 0) || (dx === 0 && dy === 1); -} - -// --- CREACIÓN DE MARCADORES (CANVAS TEXTURE) --- -function createPathMarker(stepNumber) { - const canvas = document.createElement('canvas'); - canvas.width = 128; - canvas.height = 128; - const ctx = canvas.getContext('2d'); - - // Fondo Amarillo Semi-transparente - ctx.fillStyle = 'rgba(255, 255, 0, 0.5)'; - ctx.fillRect(0, 0, 128, 128); - - // Borde - ctx.strokeStyle = 'rgba(255, 200, 0, 0.8)'; - ctx.lineWidth = 10; - ctx.strokeRect(0, 0, 128, 128); - - // Número - ctx.fillStyle = '#000000'; - ctx.font = 'bold 60px Arial'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillText(stepNumber.toString(), 64, 64); - - const texture = new THREE.CanvasTexture(canvas); - // Importante para pixel art o gráficos nítidos, aunque aquí es texto - texture.minFilter = THREE.LinearFilter; - - // Crear el mesh - const geometry = new THREE.PlaneGeometry(CONFIG.CELL_SIZE * 0.9, CONFIG.CELL_SIZE * 0.9); - const material = new THREE.MeshBasicMaterial({ - map: texture, - transparent: true, - side: THREE.DoubleSide // Visible desde ambos lados - }); - - const mesh = new THREE.Mesh(geometry, material); - mesh.rotation.x = -Math.PI / 2; - mesh.position.y = 0.05; // Ligeramente elevado sobre el suelo - return mesh; -} - -function updatePathVisuals() { - // 1. Limpiar anteriores - SESSION.pathMeshes.forEach(mesh => scene.remove(mesh)); - SESSION.pathMeshes = []; - - // 2. Crear nuevos - SESSION.path.forEach((pos, index) => { - const marker = createPathMarker(index + 1); - const worldPos = gridToWorld(pos.x, pos.y); - marker.position.x = worldPos.x; - marker.position.z = worldPos.z; - scene.add(marker); - SESSION.pathMeshes.push(marker); - }); -} - -// --- MANEJO VISUAL DE SELECCIÓN --- -function updateSelectionVisuals() { - GAME_STATE.entities.forEach(entity => { - if (!entity.mesh) return; - - if (entity.id === SESSION.selectedUnitId) { - // SELECCIONADO: Amarillo + Opacidad 50% - entity.mesh.material.color.setHex(0xffff00); - entity.mesh.material.opacity = 0.5; - entity.mesh.material.transparent = true; - } else { - // NO SELECCIONADO: Blanco (color original) + Opacidad 100% - entity.mesh.material.color.setHex(0xffffff); - entity.mesh.material.opacity = 1.0; - } - }); -} - -// --- ANIMACIÓN DE MOVIMIENTO --- -async function animateMovement() { - if (SESSION.path.length === 0 || !SESSION.selectedUnitId) return; - - SESSION.isAnimating = true; - - const unit = GAME_STATE.entities.find(e => e.id === SESSION.selectedUnitId); - if (!unit || !unit.mesh) { - SESSION.isAnimating = false; - return; - } - - // Copiar el path para ir consumiéndolo - const pathCopy = [...SESSION.path]; - - // Función helper para animar un solo paso - const animateStep = (targetGridPos) => { - return new Promise((resolve) => { - const startPos = { x: unit.mesh.position.x, z: unit.mesh.position.z }; - const targetWorldPos = gridToWorld(targetGridPos.x, targetGridPos.y); - const endPos = { x: targetWorldPos.x, z: targetWorldPos.z }; - - const duration = 300; // ms por paso - const startTime = Date.now(); - const standeeHeight = ASSETS.standees[unit.type].height; - - const hop = () => { - const elapsed = Date.now() - startTime; - const progress = Math.min(elapsed / duration, 1); - - // Easing suave (ease-in-out) - const eased = progress < 0.5 - ? 2 * progress * progress - : 1 - Math.pow(-2 * progress + 2, 2) / 2; - - // Interpolación lineal en X y Z - unit.mesh.position.x = startPos.x + (endPos.x - startPos.x) * eased; - unit.mesh.position.z = startPos.z + (endPos.z - startPos.z) * eased; - - // Saltito parabólico en Y - const hopHeight = 0.8; // Altura del salto - const hopProgress = Math.sin(progress * Math.PI); // 0 -> 1 -> 0 - unit.mesh.position.y = (standeeHeight / 2) + (hopProgress * hopHeight); - - if (progress < 1) { - requestAnimationFrame(hop); - } else { - // Asegurar posición final exacta - unit.mesh.position.x = endPos.x; - unit.mesh.position.z = endPos.z; - unit.mesh.position.y = standeeHeight / 2; - resolve(); - } - }; - - hop(); - }); - }; - - // Mover paso a paso - for (let i = 0; i < pathCopy.length; i++) { - const step = pathCopy[i]; - - // Animar el movimiento - await animateStep(step); - - // Actualizar posición lógica de la unidad - unit.x = step.x; - unit.y = step.y; - - // Borrar el marcador de esta celda (el primero del array) - SESSION.path.shift(); - updatePathVisuals(); - } - - // Centrar la cámara en la posición final (manteniendo el ángulo/zoom) - const endTarget = unit.mesh.position.clone(); - endTarget.y = 0; // Target siempre a nivel de suelo - const currentCameraOffset = camera.position.clone().sub(controls.target); - - controls.target.copy(endTarget); - camera.position.copy(endTarget).add(currentCameraOffset); - - // Al terminar, deseleccionar - SESSION.selectedUnitId = null; - updateSelectionVisuals(); - SESSION.isAnimating = false; -} - -// --- INTERACCIÓN --- -const raycaster = new THREE.Raycaster(); -const pointer = new THREE.Vector2(); - -window.addEventListener('pointerdown', (event) => { - // Bloquear interacciones durante animación - if (SESSION.isAnimating) return; - - // CLICK IZQUIERDO: Selección y Pathfinding - if (event.button === 0) { - - // Calcular coordenadas normalizadas (-1 a +1) - pointer.x = (event.clientX / window.innerWidth) * 2 - 1; - pointer.y = -(event.clientY / window.innerHeight) * 2 + 1; - - raycaster.setFromCamera(pointer, camera); - - // 1. Detectar Click en Entidades (Selección) - // Buscamos intersecciones con los meshes de las entidades - const entityMeshes = GAME_STATE.entities.map(e => e.mesh).filter(m => m); - const intersectsEntities = raycaster.intersectObjects(entityMeshes); - - if (intersectsEntities.length > 0) { - // Hemos clickado una entidad - const clickedMesh = intersectsEntities[0].object; - const entity = GAME_STATE.entities.find(e => e.mesh === clickedMesh); - if (entity) { - console.log("Seleccionado:", entity.type); - SESSION.selectedUnitId = entity.id; - SESSION.path = []; // Resetear camino - updatePathVisuals(); - updateSelectionVisuals(); // Actualizar color del standee - return; // Cortamos aquí para no procesar click de suelo a la vez - } - } - - // 2. Si hay unidad seleccionada, procesar Click en Suelo (Move) - if (SESSION.selectedUnitId) { - const intersectsGround = raycaster.intersectObject(raycastPlane); - - if (intersectsGround.length > 0) { - const point = intersectsGround[0].point; - const gridPos = worldToGrid(point.x, point.z); - - // LOGICA DEL PATHFINDING MANUAL - - // Punto de Origen: La última casilla del path, O la casilla de la unidad si empieza - let prevNode; - if (SESSION.path.length > 0) { - prevNode = SESSION.path[SESSION.path.length - 1]; - } else { - const unit = GAME_STATE.entities.find(e => e.id === SESSION.selectedUnitId); - prevNode = { x: unit.x, y: unit.y }; - } - - // A. Caso Deshacer (Click en la última) - if (SESSION.path.length > 0) { - const lastNode = SESSION.path[SESSION.path.length - 1]; - if (lastNode.x === gridPos.x && lastNode.y === gridPos.y) { - SESSION.path.pop(); // Borrar último - updatePathVisuals(); - return; - } - } - - // B. Caso Añadir (Tiene que ser adyacente al anterior) - if (isAdjacent(prevNode, gridPos)) { - // Comprobación opcional: Evitar bucles (no clickar en uno que ya está en el path) - const alreadyInPath = SESSION.path.some(p => p.x === gridPos.x && p.y === gridPos.y); - const isUnitPos = (gridPos.x === prevNode.x && gridPos.y === prevNode.y && SESSION.path.length === 0); - - if (!alreadyInPath && !isUnitPos) { - SESSION.path.push(gridPos); - updatePathVisuals(); - } - } - } - } - } - - // CLICK DERECHO: Ejecutar movimiento - if (event.button === 2) { - event.preventDefault(); // Evitar menú contextual - - if (SESSION.selectedUnitId && SESSION.path.length > 0) { - animateMovement(); - } - } -}); - -// Prevenir menú contextual del navegador -window.addEventListener('contextmenu', (event) => { - event.preventDefault(); -}); - - -// --- CARGA Y RENDERIZADO --- -const textureLoader = new THREE.TextureLoader(); - -function loadTexture(path) { - return new Promise((resolve) => { - textureLoader.load(path, (tex) => { - tex.colorSpace = THREE.SRGBColorSpace; - tex.magFilter = THREE.NearestFilter; - tex.minFilter = THREE.NearestFilter; - resolve(tex); - }); - }); -} - -async function initWorld() { - const tileTextures = {}; - const standeeTextures = {}; - - // Cargar Tiles - for (const [key, def] of Object.entries(ASSETS.tiles)) { - const tex = await loadTexture(def.src); - tex.wrapS = THREE.RepeatWrapping; - tex.wrapT = THREE.RepeatWrapping; - // Repetición dinámica basada en tamaño (supone 2 unidades por repetición de textura base) - tex.repeat.set(def.width / 2, def.height / 2); - tileTextures[key] = tex; - } - // Cargar Standees - for (const [key, def] of Object.entries(ASSETS.standees)) { - standeeTextures[key] = await loadTexture(def.src); - } - - // Instanciar Tiles (Suelo) - GAME_STATE.placedTiles.forEach(tileData => { - const def = ASSETS.tiles[tileData.type]; - const tex = tileTextures[tileData.type]; - const worldWidth = def.width * CONFIG.CELL_SIZE; - const worldHeight = def.height * CONFIG.CELL_SIZE; - - const geometry = new THREE.PlaneGeometry(worldWidth, worldHeight); - const material = new THREE.MeshStandardMaterial({ - map: tex, - transparent: true, - side: THREE.DoubleSide - }); - const mesh = new THREE.Mesh(geometry, material); - - mesh.rotation.x = -Math.PI / 2; - mesh.receiveShadow = true; - - const originPos = gridToWorld(tileData.x, tileData.y); - - // Ajuste de centro - mesh.position.x = originPos.x + (worldWidth / 2) - (CONFIG.CELL_SIZE / 2); - mesh.position.z = originPos.z + (worldHeight / 2) - (CONFIG.CELL_SIZE / 2); - mesh.position.y = 0; - - if (tileData.rotation) { - mesh.rotation.z = tileData.rotation; - } - - scene.add(mesh); - }); - - // Instanciar Entidades - GAME_STATE.entities.forEach(entity => { - const def = ASSETS.standees[entity.type]; - const tex = standeeTextures[entity.type]; - - const imgAspect = tex.image.width / tex.image.height; - const height = def.height; - const width = height * imgAspect; - - const geometry = new THREE.PlaneGeometry(width, height); - const material = new THREE.MeshStandardMaterial({ - map: tex, - transparent: true, - alphaTest: 0.5, - side: THREE.DoubleSide - }); - - const mesh = new THREE.Mesh(geometry, material); - mesh.castShadow = true; - - const pos = gridToWorld(entity.x, entity.y); - mesh.position.set(pos.x, height / 2, pos.z); - - scene.add(mesh); - entity.mesh = mesh; - }); - - // --- PAREDES DE PRUEBA (ALREDEDOR DE TILE 1) --- - // Tile 1 es 'tile_base' en 0,0. Tamaño 4x4 celdas -> 8x8 unidades world - const tile1 = GAME_STATE.placedTiles.find(t => t.id === 1); - if (tile1) { - const wallTex = await loadTexture(ASSETS.tiles['wall_1'].src); - wallTex.wrapS = THREE.RepeatWrapping; - wallTex.wrapT = THREE.RepeatWrapping; - wallTex.repeat.set(2, 2); // 2x2 repeticiones como solicitado - - const baseTileWorldSize = 4 * CONFIG.CELL_SIZE; // 8 unidades - const wallHeight = 2.5; // Altura de la pared - const halfSize = baseTileWorldSize / 2; - - // Calcular el centro exacto de la tile 1 tal como se hace al renderizarla - // Copiamos la lógica de renderizado de tiles: - const def = ASSETS.tiles[tile1.type]; - const worldWidth = def.width * CONFIG.CELL_SIZE; - const worldHeight = def.height * CONFIG.CELL_SIZE; - const originPos = gridToWorld(tile1.x, tile1.y); - - const centerX = originPos.x + (worldWidth / 2) - (CONFIG.CELL_SIZE / 2); - const centerZ = originPos.z + (worldHeight / 2) - (CONFIG.CELL_SIZE / 2); - - const wallGeometry = new THREE.PlaneGeometry(baseTileWorldSize, wallHeight); - const wallMaterial = new THREE.MeshStandardMaterial({ - map: wallTex, - transparent: true, - opacity: 1.0, - side: THREE.DoubleSide - }); - - const createWall = (offsetX, offsetZ, rotationY, opacity) => { - const wall = new THREE.Mesh(wallGeometry, wallMaterial.clone()); - wall.material.opacity = opacity; - wall.material.transparent = opacity < 1.0; // Solo transparente si opacity < 1 - // Posicionamos relativo al CENTRO de la tile - wall.position.set(centerX + offsetX, wallHeight / 2, centerZ + offsetZ); - wall.rotation.y = rotationY; - wall.castShadow = true; - wall.receiveShadow = true; - scene.add(wall); - SESSION.walls.push(wall); - }; - - // Norte (Arriba en pantalla, Z menor) -> 100% - createWall(0, -halfSize, 0, 1.0); - // Sur (Abajo en pantalla, Z mayor) -> 50% - createWall(0, halfSize, 0, 0.5); - // Este (Derecha en pantalla, X mayor) -> 50% - createWall(halfSize, 0, Math.PI / 2, 0.5); - // Oeste (Izquierda en pantalla, X menor) -> 100% - createWall(-halfSize, 0, Math.PI / 2, 1.0); - - // --- PUERTA EN PARED NORTE --- - const doorTex = await loadTexture(ASSETS.tiles['door_1'].src); - const doorWidth = 1.5; // Ancho de la puerta - const doorHeight = 2.0; // Alto de la puerta - - const doorGeometry = new THREE.PlaneGeometry(doorWidth, doorHeight); - const doorMaterial = new THREE.MeshStandardMaterial({ - map: doorTex, - transparent: true, - alphaTest: 0.1, - side: THREE.DoubleSide - }); - - const door = new THREE.Mesh(doorGeometry, doorMaterial); - // Posicionar en la celda (1, -1) - segunda celda de la pared norte - const doorGridPos = gridToWorld(1, -1); - door.position.set(doorGridPos.x, doorHeight / 2, centerZ - halfSize + 0.05); - door.rotation.y = 0; // Misma rotación que pared norte - scene.add(door); - } -} - -initWorld(); - -function animate() { - requestAnimationFrame(animate); - controls.update(); - - - renderer.render(scene, camera); -} -animate(); - -window.addEventListener('resize', () => { - const aspect = window.innerWidth / window.innerHeight; - camera.left = -d * aspect; - camera.right = d * aspect; - camera.top = d; - camera.bottom = -d; - camera.updateProjectionMatrix(); - renderer.setSize(window.innerWidth, window.innerHeight); -}); diff --git a/src/schemas/CampaignSchema.js b/src/schemas/CampaignSchema.js new file mode 100644 index 0000000..07c1226 --- /dev/null +++ b/src/schemas/CampaignSchema.js @@ -0,0 +1,51 @@ +/** + * @typedef {Object} LootTableEntry + * @property {string} itemId - ID of the item + * @property {number} weight - Probability weight + * @property {number} [minLevel] - Minimum level required + */ + +/** + * @typedef {Object} CampaignMissionNode + * @property {string} id - Unique ID of the mission reference + * @property {string} missionId - ID of the mission template to use + * @property {string} title - Display title for this step + * @property {string[]} [next] - IDs of potential next missions (for branching) + * @property {Object} [requirements] - Requirements to unlock + */ + +/** + * @typedef {Object} Campaign + * @property {string} id - Unique Campaign ID + * @property {string} title - Display Title + * @property {string} description - Brief description + * @property {string} author - Author name + * @property {string} version - Version string (e.g. "1.0.0") + * @property {CampaignMissionNode[]} missions - Graph of missions + * @property {Object.} lootTables - Global loot tables + */ + +export const CampaignSchema = { + type: "object", + required: ["id", "title", "missions"], + properties: { + id: { type: "string" }, + title: { type: "string" }, + description: { type: "string" }, + author: { type: "string" }, + version: { type: "string" }, + missions: { + type: "array", + items: { + type: "object", + required: ["id", "missionId"], + properties: { + id: { type: "string" }, + missionId: { type: "string" }, + title: { type: "string" }, + next: { type: "array", items: { type: "string" } } + } + } + } + } +}; diff --git a/src/schemas/MissionSchema.js b/src/schemas/MissionSchema.js new file mode 100644 index 0000000..9c696d3 --- /dev/null +++ b/src/schemas/MissionSchema.js @@ -0,0 +1,39 @@ +/** + * @typedef {Object} Mission + * @property {string} id - Unique Mission ID + * @property {string} type - "scripted" | "procedural" + * @property {string} biome - Tile set to use (e.g., "dungeon", "crypt") + * @property {Object} [genParams] - Parameters for procedural generation + * @property {number} [genParams.size] - Approximate number of rooms + * @property {number} [genParams.difficulty] - 1-10 scale + * @property {string[]} [genParams.forcedTiles] - Specific tiles that must appear + * @property {Object[]} [scriptedEvents] - Narrative triggers + */ + +export const MissionSchema = { + type: "object", + required: ["id", "type", "biome"], + properties: { + id: { type: "string" }, + type: { type: "string", enum: ["scripted", "procedural"] }, + biome: { type: "string" }, + genParams: { + type: "object", + properties: { + size: { type: "number", minimum: 5 }, + difficulty: { type: "number", minimum: 1, maximum: 10 } + } + }, + scriptedEvents: { + type: "array", + items: { + type: "object", + properties: { + trigger: { type: "string" }, + action: { type: "string" }, + data: { type: "object" } + } + } + } + } +}; diff --git a/src/style.css b/src/style.css index e5499d1..9e82c30 100644 --- a/src/style.css +++ b/src/style.css @@ -121,4 +121,66 @@ canvas { #compass-w { grid-column: 1; grid-row: 2; -} \ No newline at end of file +} +/* 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; +} + diff --git a/src/systems/TurnManager.js b/src/systems/TurnManager.js new file mode 100644 index 0000000..8478ef8 --- /dev/null +++ b/src/systems/TurnManager.js @@ -0,0 +1,68 @@ +export const PHASES = { + POWER: 'POWER', // Roll for events + HERO: 'HERO', // Player actions + EXPLORATION: 'EXPLORATION', // Room reveal + MONSTER: 'MONSTER', // AI actions + END: 'END' // Cleanup +}; + +class TurnManager extends EventTarget { + constructor() { + super(); + this.currentTurn = 1; + this.currentPhase = PHASES.POWER; + this.isCombat = false; + } + + startTurn() { + this.currentPhase = PHASES.POWER; + this.dispatchEvent(new CustomEvent('phaseChange', { detail: this.currentPhase })); + console.log(`--- TURN ${this.currentTurn} START ---`); + + // Simulating Power Phase trigger + this.processPowerPhase(); + } + + processPowerPhase() { + // Winds of Magic Roll (1d6) + const roll = Math.floor(Math.random() * 6) + 1; + console.log(`Winds of Magic Roll: ${roll}`); + + let message = `Vientos de Magia: ${roll}`; + this.dispatchEvent(new CustomEvent('message', { detail: message })); + + if (roll === 1) { + console.log("EVENTO INESPERADO!"); + this.dispatchEvent(new CustomEvent('eventTriggered', { + detail: { + source: 'POWER_PHASE', + event: { title: 'Evento Inesperado', description: 'Algo se mueve en la oscuridad...', type: 'MISTERIO' } + } + })); + // In a real game, we'd draw from a specific Event Deck here + } else { + console.log("¡Poder obtenido!"); + } + + // Auto-advance to Hero Phase after a brief pause to show the roll + setTimeout(() => this.setPhase(PHASES.HERO), 2000); + } + + setPhase(phase) { + this.currentPhase = phase; + this.dispatchEvent(new CustomEvent('phaseChange', { detail: this.currentPhase })); + console.log(`Phase changed to: ${phase}`); + + if (phase === PHASES.END) { + this.endTurn(); + } + } + + endTurn() { + console.log(`--- TURN ${this.currentTurn} END ---`); + this.currentTurn++; + this.startTurn(); + } +} + +export const turnManager = new TurnManager();