Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 54 additions & 10 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ El contenido del juego (niveles, entidades, objetos) se deriva de la wiki backro

## Comandos

No hay tests ni linter. Los scripts del pipeline usan solo la stdlib de Node (requiere Node 18+ por `fetch` global).
No hay linter ni build del cliente. Los scripts del pipeline y los tests unitarios
usan solo la stdlib de Node (requiere Node 18+ por `fetch` global).

```
node pipeline/download.js # Fase 0: descarga la wiki → data/raw/<pageid>.json (re-ejecutable, salta lo ya descargado)
Expand All @@ -21,6 +22,8 @@ node pipeline/select-pilot.js # Fase 2a: elige los ~30 niveles del piloto (BFS
node pipeline/make-map.js # Fase 2b: regenera data/game/mapa-piloto.html (diagrama SVG del grafo) desde levels.es.json
node pipeline/build-data.js # empaqueta data/game/*.es.json → game/js/data.js ← RE-EJECUTAR tras editar cualquier ficha
node pipeline/build-assets-manifest.js # inventaría game/assets/ → game/js/assets-manifest.js ← RE-EJECUTAR tras añadir/quitar sprites/sonidos/iconos (el juego solo carga lo inventariado; sin sondeos ni 404)
node pipeline/test-unit.js # todos los tests unitarios, sin procesos hijo
node server/test-riesgo-void.js # ramas éxito/muerte del Vacío con d20 forzado, sin azar ni servidor externo
```

Para jugar (modo PRINCIPAL, online): `node server/server.js` → http://localhost:8080 (WebSocket `/ws`; `MMO_DEV=1` habilita `?nivel=`, `MMO_ADMIN` fija la clave de guardián). Ver la entrada v21-v22 más abajo.
Expand All @@ -43,14 +46,54 @@ wiki fandom → data/raw/ (crudo, 1100+ archivos, NO editar) → data/parsed/ (g
Sin módulos ES: cada archivo de `game/js/` es un IIFE que expone un global en `window` (`RNG`, `MapGen`, `GAME_DATA`...). **El orden de los `<script>` en `game/index.html` es la gestión de dependencias** — si añades un archivo, insértalo en el orden correcto:

```
data.js → engine/rng.js → mapgen/mapgen.js → engine/tiles.js → engine/sprites.js
data.js → engine/rng.js → engine/cargador.js → config/{options,identity}.js
→ input/{state,touch,camera}.js
→ levels/level.js → levels/<id>/<id>.js → mapgen/mapgen.js
→ engine/tiles.js → engine/sprites.js
→ engine/effects.js → audio-manifest.js → engine/sfx.js → engine/fov.js
→ systems/entities.js → systems/rules.js → engine/render.js
→ lib/three.min.js → lib/shaders/* → lib/postprocessing/* (postpro UMD r147)
→ engine/atmos3d.js → engine/render3d.js → systems/game.js
→ ui/icons.js → ui/ui.js → ui/minimap.js → main.js
→ sim/{mundo,shared-rules}.js → systems/entities.js → systems/rules.js → engine/render.js
→ systems/{profiles,savegame,game}.js → ui/icons.js → ui/controllers.js
→ input/{gamepad-settings,gamepad}.js
→ ui/{display-settings,modals,codex,ui,title-interface,changelog,minimap}.js
→ sim/fisica.js → sim/entidades.js → sim/sala.js → net/*
→ debug/selftest.js → ui/title-controller.js → main.js
```

El **chunk 3D NO está en index.html**: `lib/three.min.js` → `lib/shaders/*` →
`lib/postprocessing/*` (UMD r147) → `engine/{atmos3d,render3d-painters,render3d}.js`
son 721 KB (52% del JS) que la portada no usa (su fondo son los WebP del panorama),
así que los inyecta `CHUNK_3D` en main.js vía `Cargador.scripts()` — `async=false`
mantiene el MISMO orden de ejecución, y el `?v=` se hereda del `<script>` del propio
cargador (sin él, el edge de Cloudflare serviría JS viejo). Se pide al iniciar,
continuar o conectar (inmediato si hay arranque directo por URL); permanecer en la
portada no descarga Three.js. Si añades un archivo al 3D va en esa lista, no en el HTML. `use3D` se decide
sondeando WebGL con un canvas de usar y tirar (NO con `window.Render3D`, que aún no
existe); hasta que el chunk ejecuta, TODO acceso al 3D va tras
`use3D && window.Render3D?.` y el bucle salta el frame.

`input/` contiene adaptadores de dispositivo y un único estado combinado; no
debe implementar reglas del juego. `config/options.js` es el único propietario
de `backrooms-opts`; `config/identity.js`, de `mmo-token`. Cámara, táctil y mando
se conectan al juego desde sus adaptadores y `main.js` se limita a orquestarlos.
`ui/display-settings.js` administra resolución, pantalla completa, cámara y
contador de FPS.

`sim/shared-rules.js` contiene decisiones puras que deben coincidir en modo solo,
MMO y pipeline (destinos, riesgo del Vacío, posesión y planes de manos). No copies
esas reglas en `game.js`, `sala.js` ni scripts del pipeline. `systems/game.js`
conserva la fachada `Game`, pero delega perfiles en `profiles.js` y persistencia de
partidas en `savegame.js`. `ui/ui.js` conserva `world.ui` y compone controladores;
modales, códice y título viven en sus archivos propios.

`sim/mundo.js` es dual navegador/Node y construye los mapas MMO desde la semilla.
`server/sim/mundo.js` solo arranca las dependencias y reexporta esa implementación;
no se debe replicar la transformación de niveles en el cliente ni en el servidor.

`levels/level.js` es el registro de comportamiento por nivel. El código
exclusivo se coloca bajo `levels/<id>/`; los recursos van en
`assets/levels/<id>/` y quedan inventariados en `ASSETS_MANIFEST`, igual que sprites,
iconos y sonidos. Level 0 es la primera implementación de este patrón.

`audio-manifest.js` lo genera `pipeline/download-audio.js` (audios ambientales reales de la wiki
→ `game/assets/sounds/niveles/`). `sfx.js` sintetiza el resto con WebAudio (overrides en
`game/assets/sounds/`, tecla M silencia).
Expand Down Expand Up @@ -203,8 +246,9 @@ los archivos generados (data.js byte a byte) y correr tests/auditoría tras el m
**v21-v22 — BACKROOMS MMO**: el juego es un sandbox multijugador en tiempo real. Servidor en
`server/` (única carpeta con `package.json`: dep `ws` + SQLite en `datos/mmo.db`):
`server.js` (estáticos + WebSocket `/ws` + comandos `/admin /anuncio /kick /mute /ban /tp` +
`cambiarDeSala`), `sala.js` (una sala = instancia de nivel, cap 60; tick 10 Hz vía
`tickTodas`), `sim/mundo.js` (puente Node↔motor: requiere data/rng/mapgen/fov del juego —
`cambiarDeSala`), `sala.js` (una sala = instancia de nivel, aforo `CAP_SALA`; simulación a
20 Hz vía `tickTodas` y posiciones remotas agrupadas a 10 Hz), `sim/mundo.js` (puente
Node↔motor: requiere data/rng/mapgen/fov del juego —
por la red NUNCA viaja un mapa, solo la semilla `mmo::<nivel>::<inst>`), `sim/entidades.js`
(IA continua), `protocolo.js` (validación + P.VERSION — súbela al cambiar mensajes; el
cliente manda `v` en `hola` y bots.js también), `bots.js` (carga), `db.js`, `filtro.js`.
Expand Down Expand Up @@ -319,8 +363,8 @@ servidor real + cliente ws): reproducirlo si se toca sala/protocolo.
riesgoVoid (salidas `arriesgada` con `riesgoVoid>0`, ej. `level-0→level-27`) ahora también
se tira en `server/sala.js::cruzar()` (antes solo en el modo solo) — mismo `this.rng` de
sala, mismo bonus de trébol, mensaje `dado` solo al afectado, `morir()` en fallo; test
dedicado `server/test-riesgo-void.js` (level-909, riesgoVoid:0.1, hasta 60 intentos hasta
observar ambos desenlaces). Nueva **Sala Manila** en Level 0 (fiel a la wiki real): salida
dedicado `server/test-riesgo-void.js` (level-909, riesgoVoid:0.1, fuerza d20=1 y d20=20
para cubrir ambos desenlaces sin depender del azar). Nueva **Sala Manila** en Level 0 (fiel a la wiki real): salida
sin casilla con `mecanica:'manila'` y `destino:'*opciones:level-1,level-2'` — nuevo sentinel
de destino (junto a `*aleatoria`/`*visitada`) resuelto en `crossExit`(game.js)/`cruzar`
(sala.js) con `rng.pick` sobre la lista separada por comas. `genPasillos` (mapgen.js) ahora
Expand Down
Binary file added game/assets/levels/level-0/textures/pared.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added game/assets/levels/level-0/textures/suelo-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added game/assets/levels/level-0/textures/suelo-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added game/assets/levels/level-0/textures/suelo-3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added game/assets/sounds/dado.ogg
Binary file not shown.
Binary file removed game/assets/sounds/dado.wav
Binary file not shown.
Binary file removed game/assets/sounds/dano.mp3
Binary file not shown.
Binary file added game/assets/sounds/dano.ogg
Binary file not shown.
Binary file removed game/assets/sounds/golpe.mp3
Binary file not shown.
Binary file added game/assets/sounds/golpe.ogg
Binary file not shown.
Binary file added game/assets/sounds/latido.ogg
Binary file not shown.
Binary file removed game/assets/sounds/latido.wav
Binary file not shown.
Binary file removed game/assets/sounds/muerte.mp3
Binary file not shown.
Binary file added game/assets/sounds/muerte.ogg
Binary file not shown.
Binary file removed game/assets/sounds/paso.mp3
Binary file not shown.
Binary file added game/assets/sounds/paso.ogg
Binary file not shown.
Binary file added game/assets/sounds/registrar.ogg
Binary file not shown.
Binary file removed game/assets/sounds/registrar.wav
Binary file not shown.
Binary file added game/assets/sounds/susurros-lvl-0-1.ogg
Binary file not shown.
Binary file added game/assets/sounds/susurros-lvl-0-2.ogg
Binary file not shown.
Binary file added game/assets/sounds/susurros-lvl-0-3.ogg
Binary file not shown.
34 changes: 24 additions & 10 deletions game/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ <h3><span data-icon="pergamino" data-icon-size="14"></span> Registro</h3>
<div class="dbg-fila"><span class="dbg-lab">Comida</span><div class="dbg-track"><div id="dbg-comida" class="dbg-fill"></div></div><span class="dbg-v" id="dbg-comida-v"></span></div>
<div class="dbg-fila"><span class="dbg-lab">Bebida</span><div class="dbg-track"><div id="dbg-bebida" class="dbg-fill"></div></div><span class="dbg-v" id="dbg-bebida-v"></span></div>
<div class="dbg-fila"><span class="dbg-lab">Cordura</span><div class="dbg-track"><div id="dbg-cordura" class="dbg-fill"></div></div><span class="dbg-v" id="dbg-cordura-v"></span></div>
<div class="dbg-fila"><span class="dbg-lab">Aguante</span><div class="dbg-track"><div id="dbg-aguante" class="dbg-fill"></div></div><span class="dbg-v" id="dbg-aguante-v"></span></div>
</div>
<div id="hud-equipo">
<div class="eq-hud-slot" id="hud-eq-cara"><span class="eq-ph">cara</span></div>
Expand Down Expand Up @@ -424,6 +425,14 @@ <h3>Tu recorrido</h3>
el origen no lo pida): SUBIR el número en cada versión, junto con VERSION_JUEGO -->
<script src="js/data.js?v=296"></script>
<script src="js/engine/rng.js?v=296"></script>
<script src="js/engine/cargador.js?v=296"></script>
<script src="js/config/options.js?v=296"></script>
<script src="js/config/identity.js?v=296"></script>
<script src="js/input/state.js?v=296"></script>
<script src="js/input/touch.js?v=296"></script>
<script src="js/input/camera.js?v=296"></script>
<script src="js/levels/level.js?v=296"></script>
<script src="js/levels/level-0/level-0.js?v=296"></script>
<script src="js/mapgen/mapgen.js?v=296"></script>
<script src="js/engine/tiles.js?v=296"></script>
<script src="js/engine/sprites.js?v=296"></script>
Expand All @@ -432,22 +441,25 @@ <h3>Tu recorrido</h3>
<script src="js/assets-manifest.js?v=296"></script>
<script src="js/engine/sfx.js?v=296"></script>
<script src="js/engine/fov.js?v=296"></script>
<script src="js/sim/mundo.js?v=296"></script>
<script src="js/sim/shared-rules.js?v=296"></script>
<script src="js/systems/entities.js?v=296"></script>
<script src="js/systems/rules.js?v=296"></script>
<script src="js/engine/render.js?v=296"></script>
<script src="js/lib/three.min.js?v=296"></script>
<script src="js/lib/shaders/CopyShader.js?v=296"></script>
<script src="js/lib/shaders/LuminosityHighPassShader.js?v=296"></script>
<script src="js/lib/shaders/GammaCorrectionShader.js?v=296"></script>
<script src="js/lib/postprocessing/EffectComposer.js?v=296"></script>
<script src="js/lib/postprocessing/RenderPass.js?v=296"></script>
<script src="js/lib/postprocessing/ShaderPass.js?v=296"></script>
<script src="js/lib/postprocessing/UnrealBloomPass.js?v=296"></script>
<script src="js/engine/atmos3d.js?v=296"></script>
<script src="js/engine/render3d.js?v=296"></script>
<!-- El chunk 3D (three.min + shaders + postpro + atmos3d + render3d*) NO va
aquí: son 721 KB (52% del JS) que la portada no usa. Lo inyecta CHUNK_3D
en main.js con Cargador.scripts(). Si añades un archivo al 3D, va en esa
lista, no en este HTML. -->
<script src="js/systems/profiles.js?v=296"></script>
<script src="js/systems/savegame.js?v=296"></script>
<script src="js/systems/game.js?v=296"></script>
<script src="js/ui/icons.js?v=296"></script>
<script src="js/ui/controllers.js?v=296"></script>
<script src="js/input/gamepad-settings.js?v=296"></script>
<script src="js/input/gamepad.js?v=296"></script>
<script src="js/ui/display-settings.js?v=296"></script>
<script src="js/ui/modals.js?v=296"></script>
<script src="js/ui/codex.js?v=296"></script>
<script src="js/ui/ui.js?v=296"></script>
<script src="js/ui/title-interface.js?v=296"></script>
<script src="js/ui/changelog.js?v=296"></script>
Expand All @@ -460,6 +472,8 @@ <h3>Tu recorrido</h3>
<script src="js/net/local.js?v=296"></script>
<script src="js/net/otros.js?v=296"></script>
<script src="js/net/cliente.js?v=296"></script>
<script src="js/debug/selftest.js?v=296"></script>
<script src="js/ui/title-controller.js?v=296"></script>
<script src="js/main.js?v=296"></script>
</body>
</html>
27 changes: 20 additions & 7 deletions game/js/assets-manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,16 @@ window.ASSETS_MANIFEST = {
},
"sonidos": {
"smiler": "assets/sounds/entidades/smiler.wav",
"dado": "assets/sounds/dado.wav",
"dano": "assets/sounds/dano.mp3",
"golpe": "assets/sounds/golpe.mp3",
"latido": "assets/sounds/latido.wav",
"muerte": "assets/sounds/muerte.mp3",
"paso": "assets/sounds/paso.mp3",
"registrar": "assets/sounds/registrar.wav"
"dado": "assets/sounds/dado.ogg",
"dano": "assets/sounds/dano.ogg",
"golpe": "assets/sounds/golpe.ogg",
"latido": "assets/sounds/latido.ogg",
"muerte": "assets/sounds/muerte.ogg",
"paso": "assets/sounds/paso.ogg",
"registrar": "assets/sounds/registrar.ogg",
"susurros-lvl-0-1": "assets/sounds/susurros-lvl-0-1.ogg",
"susurros-lvl-0-2": "assets/sounds/susurros-lvl-0-2.ogg",
"susurros-lvl-0-3": "assets/sounds/susurros-lvl-0-3.ogg"
},
"ambientes": {
"level-0": "assets/sounds/niveles/level-0.mp3",
Expand All @@ -49,5 +52,15 @@ window.ASSETS_MANIFEST = {
"level-6": "assets/sounds/niveles/level-6.mp3",
"level-777": "assets/sounds/niveles/level-777.mp3",
"the-hub": "assets/sounds/niveles/the-hub.mp3"
},
"texturasNiveles": {
"level-0": {
"pared": "assets/levels/level-0/textures/pared.png",
"suelos": [
"assets/levels/level-0/textures/suelo-1.png",
"assets/levels/level-0/textures/suelo-2.png",
"assets/levels/level-0/textures/suelo-3.png"
]
}
}
};
27 changes: 27 additions & 0 deletions game/js/config/identity.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Identidad persistente del cliente MMO. Cliente remoto y servidor local usan
// exactamente el mismo token y no conocen su almacenamiento.
(function () {
'use strict';

const TOKEN_KEY = 'mmo-token';

function crearToken() {
const bytes = window.crypto.getRandomValues(new Uint8Array(16));
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
}

function token() {
try {
let value = window.localStorage.getItem(TOKEN_KEY);
if (!value) {
value = crearToken();
window.localStorage.setItem(TOKEN_KEY, value);
}
return value;
} catch (e) {
return 'sin-token';
}
}

window.ClientIdentity = { token };
})();
35 changes: 35 additions & 0 deletions game/js/config/identity.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use strict';

const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');

const source = fs.readFileSync(path.join(__dirname, 'identity.js'), 'utf8');

function cargar(storage) {
const window = {
localStorage: storage,
crypto: { getRandomValues: (bytes) => { bytes.fill(10); return bytes; } },
};
vm.runInNewContext(source, { window, Uint8Array, Array });
return window.ClientIdentity;
}

test('crea una sola identidad y la reutiliza', () => {
const values = new Map();
const identity = cargar({
getItem: (key) => values.get(key) || null,
setItem: (key, value) => values.set(key, value),
});
const first = identity.token();
assert.equal(first.length, 32);
assert.equal(identity.token(), first);
assert.equal(values.get('mmo-token'), first);
});

test('devuelve una identidad segura si el almacenamiento falla', () => {
const identity = cargar({ getItem: () => { throw new Error('bloqueado'); } });
assert.equal(identity.token(), 'sin-token');
});
80 changes: 80 additions & 0 deletions game/js/config/options.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Configuración persistente del cliente. Este módulo es la única pieza que
// conoce la clave de localStorage y los valores por defecto.
(function () {
'use strict';

const STORAGE_KEY = 'backrooms-opts';
const GAMEPAD_DEFAULTS = Object.freeze({
interact: 0,
wait: 2,
light: 3,
handL: 4,
handR: 5,
backpack: 1,
menu: 9,
map: 6,
log: 7,
codex: 8,
journal: 11,
chat: 12,
});

function esObjeto(value) {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}

function valoresIniciales() {
const tactil = !!window.matchMedia?.('(pointer: coarse)').matches;
return {
gamepadMap: { ...GAMEPAD_DEFAULTS },
cursorSpeed: 8,
dado: true,
mostrarFps: false,
camaraModo: 'libre',
camaraInvertir: tactil,
camaraSens: 100,
camaraSeguimiento: 8,
resolucion: 'auto16x9',
fpsMax: 'vsync',
menuMusica: 'menu1',
};
}

function cargar() {
const defaults = valoresIniciales();
try {
const stored = JSON.parse(window.localStorage.getItem(STORAGE_KEY));
if (!esObjeto(stored)) return defaults;
return {
...defaults,
...stored,
gamepadMap: {
...GAMEPAD_DEFAULTS,
...(esObjeto(stored.gamepadMap) ? stored.gamepadMap : {}),
},
};
} catch (e) {
return defaults;
}
}

const valores = cargar();

function guardar() {
try {
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(valores));
return true;
} catch (e) {
return false;
}
}

function restaurarMando() {
valores.gamepadMap = { ...GAMEPAD_DEFAULTS };
valores.cursorSpeed = 8;
guardar();
}

window.OPTS = valores; // compatibilidad con los módulos existentes
window.Options = { valores, guardar, restaurarMando };
})();
Loading