Las referencias a ficheros de código y a offsets del binario que verás aquí son la procedencia de cada afirmación: dicen de dónde sale. El motor está publicado bajo GPL-3.0-or-later en github.com/kokoima/openu5, así que se pueden ir a mirar una a una.
1 · Mejoras de imagen
Todo lo que cambia lo que se ve: el suavizado, las dos pieles y la transparencia de sprites y tiles.
Las cuatro entradas de este documento son ✨ AÑADIDO: cosas que el original no hacía, cada una con su interruptor. Ver README.
Índice
- Las dos pieles: «1988» y «smooth» ✨
- El suavizado: xBR en la GPU ✨
- Transparencia I — el cuerpo a alpha .55 ✨
- Transparencia II — el recorte de contorno ✨
Qué puedes tocar tú, y dónde
Casi todo lo que cuenta este documento se enciende y se apaga en marcha, sin recargar:
| Atajo | Qué hace |
|---|---|
| F9 | Cambia de piel: alterna «1988» (píxeles exactos) y «smooth» (con el suavizado xBR). Es lo de §1 y §2. |
| F10 | Abre el menú SISTEMA: partidas, vídeo, audio y teclas — la misma puerta que el botón ⚙. |
| ⚙ (en la barra) | Ajustes: idioma, piel, sonido y el resto de preferencias, sin teclado. |
?transp=off | Añadido a la URL, deja los cuerpos opacos: es el interruptor de §3 y §4. |
La piel arranca en «smooth». La preferencia se guarda en tu navegador, así que la próxima visita respeta lo último que pulsaste.
1 · Las dos pieles: «1988» y «smooth»
✨ AÑADIDO · Código: game/src/skin/fiel/skin.ts, game/src/skin/shader/skin.ts, game/src/skin/manager.ts · Cambio en vivo con F9 o desde ⚙ → Vídeo.
Dos presentaciones intercambiables en caliente: la fiel de 1988 y una «smooth» que la aloja y sólo filtra el visor.
Antes
El original tiene una sola presentación: EGA de 16 colores, 320×200, píxeles gordos. No hay nada que elegir.
Ahora
Hay dos pieles intercambiables en caliente, y una es un calco de la otra:
- «1988 (fiel)» — el frontend de 1988 reconstruido pieza a pieza desde el binario: chrome EGA con coordenadas literales (
paint_screen_frame@0x637e), visor 11×11 con el pack de tiles EGA, roster y consola con la fuenteIBM.CH. - «smooth» (shader) — la misma piel fiel con UN cambio de pipeline, no una reimplementación. Instancia una
FaithfulSkinque pinta a un canvas 320×200 oculto, y compone cada frame en dos pasos: el frame fiel entero se escala por vecino (chrome y texto byte-idénticos a la fiel), y sólo el rectángulo del visor (176×176) pasa por el filtro de bordes.
Por qué importa
Que la shader aloje a la fiel en vez de duplicarla es la decisión que sostiene todo lo demás: el reloj de animación, los overlays de combate, la moongate, el terremoto, el modal de Ztats y el audio siguen siendo los de la fiel. Cuando se arregla un defecto en la fiel, la shader lo hereda sin tocar una línea. El calco de 1988 no se bifurca nunca.
Y la elección de filtrar sólo el visor no es pereza: el texto y el chrome viven fuera de ese rectángulo, así que el filtro no puede tocarlos. Un upscaler pasado sobre la fuente 8×8 la habría convertido en papilla.
Contrapartida
- La cinemática de intro (ui/faithful-intro.ts) es un subsistema aparte que corre y termina antes de montar ninguna piel: sus láminas no pasan por el filtro. Está declarado como frontera en el docstring de skin/shader/skin.ts.
- La gema de overworld usa un panel DOM aparte y tampoco se filtra; la gema de mazmorra, que la fiel sí pinta dentro del visor, se filtra sola. Dos gemas, dos comportamientos.
2 · El suavizado: xBR en la GPU
✨ AÑADIDO · Con la piel smooth (F9) · Código: game/src/skin/shader/xbr-gl.ts, upscaler.ts
El visor pasa por un filtro de bordes xBR ejecutado en la GPU; el chrome y el texto siguen siendo los de 1988.
Antes
Vecino más cercano. Un píxel del original es un cuadrado de N×N píxeles de pantalla. Es lo que hace la piel «1988», y es lo correcto para ella.
Ahora
El visor pasa por xBR nivel 1 con la regla de detección de bordes de Hyllian (que compara los píxeles por luminosidad, no por color crudo), portada a GLSL y ejecutada en WebGL1. El pipeline encadena dos pases de 2× (= 4× interno) y deja el estirón 4→6 al blit bilineal.
Por qué importa
Un filtro de bordes no «desenfoca»: reconstruye las diagonales que la retícula de 16 píxeles no podía representar. La medición del carril smooth-partido-c sobre el layout partido dio 12,8× más colores distintos en el visor que la piel fiel (medido el 28-07-2026) — el filtro está trabajando, no adornando.
Contrapartidas, y son varias
- Licencia: el kernel es un port propio del extractor/src/upscale/xbr.ts del proyecto, no el
.glslGPL de RetroArch que recomendaba la spec. La desviación está declarada en la cabecera dexbr-gl.ts. - Sin WebGL no hay filtro: existe un
NearestUpscalerde degradado que sube a 6× por vecino. La piel shader sigue montada y conmutable, pero se ve idéntica a la fiel. Es un degradado silencioso: nada avisa al jugador. - Lo recortado pierde el suavizado. Los sprites que se recomponen desde el atlas EGA (§3 y §4) se blitean con vecino, así que el sprite recortado no lleva xBR. Está anotado como limitación conocida, no como regresión.
- Backbuffer colapsado si el anfitrión mide 0×0. Alojada dentro de otro layout sin tamaño, la shader calcula
ceil(320/320) = 1y se queda en 320×200: cero suavizado, sin error. Diagnosticado y resuelto. Es el modo de fallo más traicionero de todos, porque el juego funciona perfectamente y sólo está mal la única cosa por la que se eligió esta piel.
3 · Transparencia I — el cuerpo a alpha .55
✨ AÑADIDO · Piel shader · Kill-switch ?transp=off
Los cuerpos de los sprites elegidos se pintan a alpha .55 en la piel smooth y dejan ver el suelo de debajo.
Antes
Nada en el port aplicaba alpha < 1 al cuerpo de un tile o de un actor. Los actores ya tenían el fondo negro recortado, pero el cuerpo era sólido: un fantasma tapaba el suelo igual que un troll.
Ahora
Una lista blanca de familias, elegida sobre una maqueta A/B renderizada del atlas real (A opaco, B a alpha .55, C con tinte de cristal — se cableó la B):
![]()
Las tres columnas de la maqueta: A opaco, B a alpha .55, C con tinte de cristal. Se cableó la B.
| Familia | Tiles | Vía |
|---|---|---|
| Force fields (Poison/Magic/Fire/Electric) | 488-491 | terreno: suelo sintetizado + cuerpo a .55 |
| Fantasmas | 412-415 | actor: ActorTransparency.bodyAlpha |
| Wisps | 468-471 | actor |
| Shadowlords | 508-511 | actor |
Y además, el boundary del shadowlord (112-127, como «bruma») y la llama azul (222).
Por qué importa
Es el uso canónico de la translucidez: un fantasma que se ve sólido es un fantasma que no da miedo.
Contrapartidas y descartes
- Lista blanca por tileId, nunca global. Un alpha aplicado a todos los actores volvería translúcido a un orco y rompería la lectura del combate, y el cableado lo respeta.
- Antes de la niebla, no después. Fields y llamas viven en interiores donde el
fogCanvasya modula; un alpha aplicado después se multiplicaría con la niebla y el sprite desaparecería de noche. Todo el pase va antes. - Las ventanas se descartaron. Se probó alpha .55 sobre los tiles de ventana (74/75) y el resultado sólo atenúa la piedra: no revela nada (el fondo es negro) ni produce el «cristal luminoso» que se buscaba. Lo que se pedía —brillo interior de noche— es un glow, o sea diseño nuevo. El scaffolding se retiró.
- El agua no se trata, a propósito.
WaterStream(96-111) yCornerWithWater(228-231) ya los compone la capawaterfn32; añadir alpha encima sería doble tratamiento y, peor, revelaría el vacío detrás (un río no tiene suelo debajo). La cascada (212-215) queda fuera del tratamiento por la misma razón: su «vecino dominante» es más cascada o roca, así que el suelo sintetizado saldría mal. - Suelo de 16 colores: por debajo de alpha ~.45 sobre terreno oscuro el tile se vuelve ilegible. El .55 elegido está justo encima de ese piso.
4 · Transparencia II — el recorte de contorno
✨ AÑADIDO · Piel shader · Kill-switch ?contourTransp=off · Código: game/src/render/floor-underlay.ts
El negro exterior del sprite se recorta y se sustituye por el suelo sintetizado; el negro interior se conserva, y una pared nunca se pinta como suelo.
Es el otro tratamiento, y no es el mismo que el anterior. Aquí no se baja el alpha del cuerpo: se sustituyen por transparencia los píxeles negros exteriores del sprite (los conectados al borde de la celda, por flood-fill), y los negros internos se conservan. El sprite sigue opaco; lo que desaparece es su cuadrado de fondo.
Antes / después — la fuente del castillo de Lord British
?contourTransp=off | por defecto |
|---|---|
![]() | ![]() |
A la izquierda, el suelo de rejilla se corta en negro en las esquinas del tile. A la derecha, la rejilla continúa por las esquinas y sólo queda el agua y la basa.
El suelo sintetizado
La fuente la hornea la piel fiel sin un hook por tile: no hay dónde engancharse. El suelo de debajo se sintetiza con dominantFloorNeighbor — de los cuatro vecinos ortogonales, el tile de suelo que más aparece, copiando sus píxeles ya renderizados (xBR-exactos) antes de blitear el recorte. Y los tiles animados (la fuente cicla cuatro frames) se recortan con la misma fase de animación de la fiel (FaithfulSkin.animPhase): el fuego se mueve y el borde no parpadea, con una guarda en game/tests/contour-transp.test.ts que exige que los cuatro frames sigan distintos.
La regla «nunca fondo de pared»
El «vecino dominante» sólo elige entre suelos de verdad, y hay dos reglas que lo garantizan:
- Las paredes no son candidatas a suelo.
isFloorUnderlayCandidate: un candidato es suelo si es pisable a pie o navegable (hierba, ladrillo, madera, alfombra, puente, escaleras, lava, agua). Muros, piedra seca, puertas y ventanas quedan fuera. Con tres paredes y un suelo alrededor, gana el suelo: las paredes ni cuentan. Todo pared ⇒null⇒ celda intacta. Jamás pared de fondo. - Los fuegos montados en pared no se recortan. Los apliques izquierdo/derecho (0xb0/0xb1) y el hogar (0xbc) ya llevan la pared en su gráfico; recortarlos pintaría ladrillo detrás. Se quedan con su celda entera horneada, y el titileo lo sigue poniendo la fiel. Los fuegos de suelo (brasero 0xb2, hoguera 0xb3, farola 0xbd, llama azul 0xde) sí se recortan. Los ambiguos (0xbe vela sobre mesa, 0xbf cocina) quedan fuera por el mismo principio: el fondo debe ser lo que realmente hay detrás.
Extensión a los fuegos, que no se animan igual
Braseros (0xb2), antorchas (0xb0/0xb1), hogueras (0xb3), hogar (0xbc–0xbf) y llama azul (0xde) no ciclan su id: su titileo es ruido procedural fn32 sobre un canvas vivo. El recorte los trata distinto — sintetiza el suelo, y luego recorta el canvas vivo de la llama por la silueta estática del tile (destination-in), de modo que el parpadeo sobrevive y el fondo se corta.
?contourTransp=off | por defecto |
|---|---|
![]() | ![]() |
Lo que NO se toca: su aportación de luz (lightLevel / visMask). Sólo el pintado. Un fix de presentación que hubiera movido la iluminación habría cambiado el juego.
Contrapartidas
- Sólo es legible sobre suelo denso (ladrillo, rejilla). Sobre hierba o desierto EGA —que son mayormente negros— el recorte es invisible: el efecto existe en todos lados y se ve en pocos.
- El recorte se blitea desde el atlas EGA por vecino ⇒ el sprite recortado pierde el suavizado xBR. Ver §2, contrapartida 3.
- La mejora es deliberadamente DESIGUAL. Por la regla 2, dos fuegos contiguos pueden verse distintos: el brasero de suelo se funde con el ladrillo y el aplique de pared conserva su cuadro horneado. Es la elección correcta — recortar también los fuegos de pared pintaría ladrillo donde el jugador ve piedra. Cuando el fondo real es la pared, la respuesta correcta es no recortar.
Lo que este documento no cubre
- La niebla, la oclusión nocturna y las sombras de ciudad — las pinta la piel fiel y son calco, no mejora.
- El agua xBRZ pre-horneada (
water-xbrz.png) — capa ya existente, sin un antes/después que enseñar. - Las mazmorras 3D y su decorado — tienen documento propio.
- Los arreglos de fidelidad (tiles, tipografía, líneas y contornos, el zodíaco) — eran defectos nuestros y están corregidos: son historial del port, no una oferta sobre 1988, así que ya no se listan aquí como «mejoras».



