Estas páginas se generan a partir de la documentación interna del proyecto. Las referencias a ficheros de código y a offsets del binario se conservan como procedencia —para que se vea de dónde sale cada afirmación—, pero todavía no se pueden seguir: el motor aún no está publicado. Cuando lo esté, pasarán a ser enlaces.
1 · Mejoras de imagen
Todo lo que cambia lo que se ve: el suavizado, las dos pieles, la transparencia de sprites y tiles, la tipografía, y las líneas y contornos.
Etiquetas: 🎯 FIDELIDAD (el «antes» era un fallo nuestro) · ✨ AÑADIDO (el original no lo hacía) · ⚖️ DIVERGENCIA DECLARADA · 🗑️ RETIRADO. 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 ✨
- La regla que costó dos capturas del usuario: «nunca fondo de pared» ✨
- Tiles: el cadáver que era desierto 🎯
- Tipografía I — los iconos de equipo salían de la fuente equivocada 🎯
- Tipografía II — el cuerpo de los letreros: latín o runas ⚖️
- Tipografía III — la fuente del panel, dos maquetas descartadas
- Líneas y contornos I — los remates de la banda de scroll 🎯
- Líneas y contornos II — la flecha que era un rombo partido 🎯
- El zodíaco transpuesto 🎯
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.
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 · Código: game/src/skin/shader/xbr-gl.ts, upscaler.ts
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 (diferencia ponderada en YUV, pesos 48/7/6), 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 (lote-c-smooth-partido, 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.tsy quedó reportada al carril de publicación. - 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 caveat heredado en
docs/verdicts/contour-transp/README.md, 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 en diagnostico-smooth-portrait y resuelto en el lote C. 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 · Actas: docs/verdicts/sprites-transparencia/candidatos.md (censo), docs/verdicts/transp-wire/README.md (1ª ola), docs/verdicts/transp-wave2/README.md (2ª ola)
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.
El censo primero, el cableado después
Antes de tocar un píxel se hizo un censo de candidatos sobre los 529 tiles de TileData.json, con impacto / coherencia / coste y —la columna que decide— si la referencia original ya lo insinúa (DOS) o si es puro añadido estético (Licencia). La maqueta A/B se renderizó del atlas HD real antes de elegir:
![]()
El usuario eligió la columna B (alpha .55 del cuerpo), y eso es lo cableado.
Ahora — 1ª ola
| 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 en la 2ª ola, el boundary del shadowlord (112-127, como «bruma») y la llama azul (222).
🗑️ Lo que este apartado enseñaba, y por qué ya no
Aquí había una comparación A/B de la moongate translúcida. Se ha retirado, porque el efecto ya no existe.
La moongate se cableó en la 1ª ola y el usuario la mandó quitar el 22-07-2026 («El moongate no debe tener transparencia dentro», con dos capturas). Está en el código, en el sitio exacto donde antes se pintaba:
// MOONGATE: RETIRADA del censo de translucidez por VEREDICTO DEL USUARIO
La puerta se queda con el pintado opaco de la capa fiel. El tile 220 sigue en isCensusTerrainTile, pero sólo como exclusión de suelo-donante para los fields vecinos.
Cómo se cazó, que es lo reutilizable: capturando las dos ramas del A/B —por defecto y ?transp=off— y comparando el sha. Salieron byte-idénticas. Un kill-switch que no cambia ni un byte significa que la cosa que apaga ya no está. No hizo falta leer código para sospecharlo; sólo para confirmarlo.
Las capturas originales siguen en su carpeta de veredicto: son ciertas de su fecha, y son el acta de un carril. Lo que no puede hacerse es citarlas en presente.
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. Pero la decisión relevante no es esa, sino la de al lado —
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. Está escrito como riesgo transversal nº 4 del censo, 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 el censo 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) sí queda sin cubrir, pero su «vecino dominante» es más cascada o roca: el suelo sintetizado saldría mal. Queda como carril futuro, no como olvido. - 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 · Actas: docs/verdicts/contour-transp/README.md, docs/verdicts/contour-fix/README.md
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 problema de fondo, y su solución
La fuente la hornea la piel fiel sin un hook por tile: no hay dónde engancharse. La salida fue sintetizar el suelo de debajo 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.
La regresión que apareció, y por qué es instructiva
Al cablear el recorte, la fuente dejó de animarse. terrainWindow lleva el tile crudo del mapa (frame base fijo, 0xd8); la fiel anima la fuente al dibujar, con animatedFrame(tile, phase, groups). El recorte blitaba el tile crudo, así que tapaba la fuente animada de la capa mundo con un frame estático. Un fix visual que congela lo que estaba vivo.
El arreglo: el recorte deriva el frame vivo con la misma fase de la fiel (FaithfulSkin.animPhase). Los cuatro frames, ya ciclando con el recorte activo:

Y quedó una guarda de regresión medida (SAD entre frames distintos = 133 454, o sea ≠ 0) en game/tests/contour-transp.test.ts.
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. Hallazgo del propio carril: 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.
5 · La regla que costó dos capturas del usuario: «nunca fondo de pared»
✨ AÑADIDO (corrección de un añadido) · Código: game/src/render/floor-underlay.ts · Acta: docs/verdicts/transp-wave2/README.md
El «vecino dominante» de §4 tenía un defecto que sólo se ve mirando: si los vecinos de la celda eran paredes, sintetizaba pared como suelo, y el recorte pintaba ladrillo donde el jugador ve piedra.
Dos reglas, las dos con un testigo visual del usuario detrás:
- 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 pintaba ladrillo detrás. Se dejan 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) se excluyeron por el mismo principio: el fondo debe ser lo que realmente hay detrás.
Verificación — interior del keep «Calma»
?contourTransp=off | con el fix |
|---|---|
![]() | ![]() |
El brasero se funde con el ladrillo (no con la piedra blanca del borde) y los apliques quedan idénticos entre las dos capturas, porque están excluidos del recorte.
Por qué importa fuera de su carril
dominantFloorNeighbor es un helper compartido por fuente, braseros, fields, moongate y boundary. Arreglarlo aquí corrigió también lo ya aterrizado del carril anterior. Un bug en una función compartida no tiene un dueño: los tiene a todos.
Contrapartida: la mejora queda deliberadamente DESIGUAL
El precio de la regla 2 es que dos fuegos contiguos se ven distintos. En una sala con un brasero de suelo y un aplique de pared, el brasero se funde con el ladrillo y el aplique conserva su cuadro horneado. A ojos de quien no conoce la razón, parece que el efecto «falla a veces».
Se aceptó a sabiendas, y es la elección correcta: la alternativa —recortar también los fuegos de pared— era peor y además falsa, porque pintaba ladrillo donde el jugador ve piedra. Entre un efecto uniforme que miente y uno desigual que dice la verdad sobre lo que hay detrás, se eligió el segundo. Cuando el fondo real es la pared, la respuesta correcta es no recortar.
6 · Tiles: el cadáver que era desierto
🎯 FIDELIDAD · Código: game/src/skin/coreview.ts (arenaLoot) · Acta: docs/verdicts/corpse-tile/README.md
Antes
En la celda de un troll recién muerto sobre un puente no salía el cadáver, sino «una mezcla de hierba con transparencia»: motas verdes y amarillas sobre los tablones.
La causa
COMBAT:0x1574 guarda en la tabla de objetos de la arena el byte bajo del resto: cadáver 0x1E, sangre 0x1F, cofre 0x01. El render los blitea desde el banco alto, con +0x100. arenaLoot pintaba el byte crudo, sin ese offset — así que salían sus gemelos del banco bajo:
| byte | banco bajo (el bug) | banco alto (lo fiel) |
|---|---|---|
| 0x1E | LeftDesert2 (desierto) | 0x11E DeadBody (cadáver) |
| 0x1F | RightDesert2 (desierto) | 0x11F Splat (sangre) |
| 0x01 | Water1 (agua) | 0x101 Chest (cofre) |
Tiles de desierto —motas verdes sobre negro— pintados como entidad con fondo transparente sobre tablones. Exactamente lo que el usuario describió.
Antes / después

Fila superior, el banco bajo (el bug); fila inferior, el banco alto.
Por qué importa
Es el género de defecto que no parece un defecto: parece un efecto raro. El testigo no dijo «falta el cadáver», dijo «sale un desierto con transparencia» — y ese es precisamente el síntoma que produce un offset de 0x100 perdido. Sin la descripción literal del usuario, la búsqueda habría empezado en el sitio equivocado.
Contrapartida: el arreglo es de tres bytes, no de la familia
lootTiles() conserva su contrato de bytes bajos porque lo consumen los tests y open_chest: el +0x100 se añade sólo en el punto de pintado. Es lo correcto —cambiar el contrato habría roto a los otros consumidores— pero deja la asimetría viva: hay dos convenciones de banco en juego y hay que saber en cuál se está.
Y el arreglo no cubre la familia entera: el gargoyle (0x4C) sigue diferido, porque es otra capa (terreno del mapa, no objeto de la tabla de la arena) y no se soluciona con el mismo offset. Está anotado como diferido en el acta, no como resuelto.
7 · Tipografía I — los iconos de equipo salían de la fuente equivocada
🎯 FIDELIDAD · Código: game/src/skin/fiel/skin.ts (drawReadyPicker) · Acta: docs/verdicts/font-glyphs/verdict.md
El reporte
«los iconos de fuente de la plantilla NO son los del juego, hay que revisar eso bien, tenemos algún error»
Lo que NO estaba roto (descartado byte a byte)
Antes de tocar nada se verificó lo obvio, y lo obvio estaba bien:
font-ibm.png==IBM.CH: comparación píxel a píxel de los 128 glifos, incluido todo el rango de control 0x00–0x1F ⇒ 0 píxeles de diferencia.font-runes.png==RUNES.CH: ídem ⇒ 0 píxeles.READY_GLYPH_TABLE==DATA.OVL: correcta. (El comentario citabaDS 0x1ae8; el offset real es0x1af8por la regla DGROUPfileoff = DS + 0x10. En0x1ae8vive otra tabla — valores de armadura.)
El bug real
El original imprime el glifo de clase de cada ítem equipado como carácter de atributo desde la fuente rúnica, no desde IBM. La banda de control de RUNES.CH lleva dingbats de equipo (casco, escudo, coraza, armas, anillo); la de IBM.CH lleva triángulos y piezas de caja. El port escribía el código en una celda que la piel rasteriza con IBM ⇒ los 48/48 ítems renderizaban un glifo distinto del que muestra el DOS.
El corolario lo confirma: 0x09 (Thrwng Axe) es un glifo vacío en IBM.CH —de ahí el «BLANK!» que había desconcertado a un carril anterior— pero tiene glifo real en RUNES.CH. Ningún código de la tabla es vacío en la fuente correcta.
Y lo que sigue en IBM, verificado
Las flechas de scroll del picker (0x18/0x19/0x12) sí son IBM en el DOS real: en la referencia es un ↓ limpio = IBM.CH[0x19]. El marco del pergamino y el texto de la fila también. El fix usa el highlightCols que layoutReadyPicker ya calculaba y que se ignoraba, para dibujar sólo las celdas del glifo de clase desde el atlas rúnico.
Contrapartida: bajo la piel smooth, estos glifos no se suavizan
La piel shader hereda el fix por el blit nearest de la base fiel, no repintándolo. O sea: los iconos de equipo salen ya correctos, pero sin vectorizar — al contrario que las flechas de scroll de §11, que sí se repintan en vectorial. En la misma pantalla conviven un elemento suave y otro de píxel duro.
Es la elección barata y es defendible (un dingbat de 8×8 vectorizado a mano por 30 códigos es mucho trabajo para un icono diminuto), pero es una desigualdad real de la piel smooth, no una casualidad.
🔴 Nota de estado del material
El veredicto citaba una imagen comparativa.png (columna roja = IBM, verde = RUNES) que nunca llegó a commitearse: la carpeta contiene sólo verdict.md. La adjudicación no depende de ella —se sostiene en el careo byte a byte de arriba—, pero la referencia estaba rota. Queda anotada en el propio veredicto (04-08-2026) para que nadie vuelva a buscarla.
8 · Tipografía II — el cuerpo de los letreros: latín o runas
🎯 FIDELIDAD · Código: game/src/skin/fiel/sign-box.ts (layoutSignBox, runicBodyCells)
| Antes (latín) | Ahora, por defecto (runas) |
|---|---|
![]() | ![]() |
El DOS real pinta el cuerpo de los carteles en runas, con los dígrafos colapsados (TH=0x5b, EA=0x5e, ST=0x5f, NG=0x5d, EE=0x5c) — por eso «NORTH», «EAST» y «TRINSIC» salen más compactos que en latín. El port lo hacía en latín; ahora lo hace en runas.
Se decidió a favor de la fidelidad (veredicto #25): layoutSignBox lleva const runicBody = opts?.runicBody !== false — o sea rúnico salvo que te opongas, y el latín sobrevive únicamente para los tests de la variante legible.
Lo que salva la legibilidad
El log de consola conserva el latín. La caja va fiel en runas y el texto sigue leyéndose en el historial. Fidelidad máxima donde se mira, legibilidad donde se lee — y por eso la decisión no costó nada que doliera.
Detalle de método que conviene no perder: la caja se dimensiona al cuerpo ya compuesto y se capa al ancho del panel (interior 14, derivado de SIGNS.DAT). Como la runa colapsa dígrafos, el mismo texto ocupa menos: «PRIVATE ISLAND» llena el panel justo, sin aire.
🔴 Esta sección decía lo contrario hasta el 04-08-2026. Afirmaba que el cuerpo rúnico era una «maqueta no aterrizada» tras un flag
?signRunic=1, y que el port «se queda en latín por defecto». Las dos cosas eran falsas: el flag no existe en el árbol y el rúnico es el default. No era una cifra caducada — era el resultado invertido de una decisión que sí se tomó. Lo destapó el censo de este documento contra el código.
9 · Tipografía III — la fuente del panel, dos maquetas descartadas
Maquetas de la rama fiel/mockups-verdicts, sin aterrizar · Acta: docs/verdicts/fuente-transparente/README.md
La idea de «fuente transparente» del panel bajo la piel shader admitía dos lecturas, y se maquetaron las dos como flags reversibles antes de decidir:
| Estado de hoy | ?mockFont=nobg | ?mockFont=alpha |
|---|---|---|
![]() | ![]() | ![]() |
- A — hoy: glifos HD blancos opacos sobre panel negro plano.
- B — el glifo pierde su fondo de celda y «flota» sobre un panel tematizado. Ojo al matiz del acta: sobre el panel negro plano de hoy, quitar el fondo de celda no se vería; por eso la maqueta va acompañada de un degradado azul. Lo que se está enseñando no es sólo «quitar el fondo», es «quitar el fondo y dar color al panel».
- C — glifos a alfa 0,55 con bordes suaves sobre el panel negro actual.
Apagados, el render es byte-idéntico al de hoy (no-op verificado). El acta anota que en C la banda de vientos («Calm Winds») se pinta por otra ruta y queda opaca en la maqueta: si esta vía se eligiera, habría que unificarla al cablear.
10 · Líneas y contornos I — los remates de la banda de scroll
🎯 FIDELIDAD · Actas: docs/verdicts/chevrones/README.md (shader), docs/verdicts/ready-arrows/README.md (fiel)
Cuando una lista del picker de Ready desborda sus 7 filas visibles, el original pinta una banda ► flecha ◄ bajo el pergamino. En el port, los remates quedaban separados de la banda azul por una línea negra de 1 píxel lógico.
| antes | después |
|---|---|
![]() | ![]() |
Y la prueba a ×8 de la junta, que es donde de verdad se adjudica:

Dos causas distintas para el mismo síntoma
Esto es lo que hace la entrada interesante: el mismo defecto tenía una causa distinta en cada piel, y arreglar una no arregló la otra.
- Shader (vectorial): el dorso del chevron caía 1 px lógico dentro de la celda. Fix: desplazar cada remate 1 px hacia su banda (
nudge = s). - Fiel (bitmap):
drawReadyPickerennegrecía las tres celdas del indicador antes de trazar los remates, borrando las dos columnas base —el azul que aporta el chrome—. Fix: ennegrecer sólo la celda central, la de la flecha. CadadrawBandBracketya ennegrece su propio hueco interior y respeta la columna base.
El resultado de la piel fiel es byte-idéntico al recorte de referencia del DOS (original-ORIG_3-band.png).
11 · Líneas y contornos II — la flecha que era un rombo partido
🎯 FIDELIDAD · Código: game/src/skin/shader/skyband.ts · Acta: docs/verdicts/ready-arrows/README.md
La piel shader repinta el chrome en vectorial, y su flecha de scroll dibujaba triángulos rellenos ▲/▼. Para el estado «hay más arriba y abajo» (↕) pintaba dos triángulos con un hueco entre ellos, que leía como un rombo partido.
| antes / después, piel fiel | antes / después, piel shader |
|---|---|
![]() | ![]() |
El glifo del DOS no es un triángulo: es una flecha fina con asta (↑ 0x18 / ↓ 0x19 / ↕ 0x12 de IBM.CH). El fix vectoriza esa forma midiendo el bitmap real de font-ibm.png — cabeza de semiancho 3 (cols 1..7), asta de semiancho 1 (cols 3..5) — y el ↕ pasa a ser una silueta continua de doble punta unida por el asta, no dos piezas.
La guarda quedó escrita en el término que importa: el test shader-skyband.test.ts afirma que el ↕ es 1 fill + 1 stroke, no 2+2. Un test que cuenta operaciones de dibujo, porque el defecto era precisamente que había dos piezas donde debía haber una.
El censo que vino detrás
El mismo indicador faltaba en las listas de Ztats, y ahí el hallazgo fue de método (ui-scroll-audit): la banda no la dibuja cada lista, es un kernel único (0x6c0a) sin coordenadas —o sea, banda fija en pantalla— que varios comandos invocan. Las derivaciones se habían hecho por comando, en vertical; nadie barrió el consumidor compartido. Ready lo implementó cuando le tocó y ahí se quedó.
De las tres bandas de scroll de lista del binario, el port cubre dos. La tercera es la lista de mercancías del herrero, y no aplica: el port sirve las tiendas como flujo de consola, no como el pergamino enmarcado del original. Eso es una divergencia de presentación anotada, no una flecha que falte.
🔴 Y hay un cuarto sitio donde esa banda aparece sin deber aparecer
En el port, el picker de (R)eady tiene un cliente más: el selector de reactivos de (M)ix, que reusa drawReadyPicker con variant:"mix". Así que el arreglo de arriba le llegó gratis. Y con él, algo que no debía llegarle:

7 reactivos y una ↓. Los del juego son 8: falta Mandrake Root.
En el binario, el panel de reactivos de Mix ni siquiera es el picker de Ready: es una rutina propia de otro overlay, CMDS.OVL @0x18be, y no pagina nada.
- Abre una ventana de texto de 9 filas —
set_text_window(1, 0x18, 1, 0x26, 9)en0x1900— para 8 ítems como máximo. Nunca desborda, así que nunca necesita paginar. - Su bucle de impresión (
0x1932-0x196d) cierra concmp si, word ptr [bp - 0x14], y[bp-0x14]es el nº de reactivos POSEÍDOS, contado en0x18ca-0x18dd. No haycmp si,7en ninguna parte. - Y en todo
CMDS.OVLno hay una sola llamada que resuelva al kernel de la banda (0x6c0a). Cero. El original no dibuja aquí ninguna flecha de scroll.
El 7 del port sale de skin/fiel/ready.ts:56 — READY_VISIBLE_ROWS, derivado de la ventana de Ready (draw_list_frame(8) @ZSTATS 0x12ee) y aplicado a Mix por el solo hecho de compartir renderizador.
La misma banda es el ARREGLO en Ztats y Ready, y el DEFECTO en Mix. Y el mecanismo no es «se heredó una constante»: es que el port hizo de Mix un cliente del picker de Ready, y el binario los tiene separados en dos overlays distintos. Al reutilizar el renderizador viajó con él su geometría —la cota de página, y la banda que anuncia esa cota— hasta un caso que no la tiene. Un widget compartido no comparte sólo el dibujo: comparte sus supuestos, y nadie decide si valen también para el cliente nuevo.
Sigue abierto: la ventana de Mix son 9 filas, así que con 8 de contenido hay que desplazarla, y el renderizador es el de Ready — radio de explosión. Detalle en docs/verdicts/mix-flow/README.md y mix-flow-acta §6b.
Corolario, y matiza la frase de arriba: de las tres bandas de lista del binario el port cubre dos y la tercera no aplica. Pero «cubierto» no es «fiel» — y hay un cuarto sitio, Mix, donde el port pinta una banda que el binario no pinta. El censo contó las bandas que FALTABAN; nadie contó las que SOBRAN.
⚠️ Discrepancia anotada, para adjudicar fuera de este documento:
re/notes/ui-scroll-audit.md:34lista el «selector de reactivos de (M)ix» como consumidor de la banda de ZSTATSitem_page_controller@0x0f2e, en una tabla cuyo sujeto declarado es el BINARIO. Esa rutina no puede servir a Mix: despacha sobre[bp+4]y sólo tiene dos ramas —0x52('R', tabla de equipo, 48 entradas) y el caso contrario (otra tabla, 38 entradas)—, ninguna es la lista de 8 reactivos, y el único llamador que le pasa argumento pasa0x52. Lo que esa fila describe es la implementación del PORT. Corresponde al carril del censo, no a este documento.
| estado | captura |
|---|---|
| 7 ítems (cabe justo) — sin flecha | ![]() |
| 8 ítems, tope — sólo ▼ | ![]() |
| 8 ítems, scrolleado — ▲ | ![]() |
| piel shader — vectorial | ![]() |
12 · El zodíaco transpuesto
🎯 FIDELIDAD · Capturas: docs/verdicts/zodiaco-70/
La lámina del zodíaco (el cielo estrellado con los signos) salía con las estrellas en las posiciones equivocadas:
| antes | después |
|---|---|
![]() | ![]() |
Y el detalle a zoom de un signo, que es donde se ve la diferencia de forma:
| antes | después |
|---|---|
![]() | ![]() |
La carpeta no tiene acta, así que se midió
zodiaco-70/ no lleva README. Lo único que había era el nombre de un fichero afirmando «transpuesto», y un nombre de fichero no es una derivación. En vez de repetirlo como si lo fuera, se contrastó la hipótesis contra las dos imágenes (04-08-2026).
Método: las dos láminas son 528×528 y ~98 % negro, así que comparar píxel a píxel da 99 % de acuerdo con cualquier transformación — el fondo domina. Se comparan sólo los píxeles encendidos, por intersección sobre unión (IoU), contra las ocho simetrías del cuadrado:
Hipótesis: DESPUÉS = … de ANTES | IoU |
|---|---|
| transpuesta (aᵀ) | 0,4897 |
| identidad (ningún cambio) | 0,0242 |
| rotación 90° | 0,0160 |
| rotación 180° | 0,0059 |
| espejo vertical | 0,0040 |
| rotación 270° | 0,0020 |
| espejo horizontal | 0,0000 |
La transposición gana por 20× sobre la siguiente candidata. La hipótesis del nombre de fichero queda corroborada: lo que estaba mal era el intercambio de los dos índices al leer la lámina.
Lo que esta medición NO establece
El IoU es 0,49, no 1,0, y hay que decirlo: la transposición explica el grueso de la diferencia, pero no toda. DESPUÉS tiene más píxeles encendidos que ANTES (2 394 frente a 2 178), así que el arreglo no fue una transposición pura — hubo algo más, o los sprites de estrella no son simétricos y transponerlos no los mapea exactamente.
Cuál de las dos cosas, no lo sé y no lo invento: haría falta el código del carril. Lo que aquí queda es una hipótesis medida con su residuo declarado, que es bastante más de lo que había.
Las cuatro capturas estaban sin commitear hasta hoy; entraron en la rama de este documento junto con las otras ocho sueltas de
feat-anim/ygem-glyphs/.
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; su rearquitectura vive en la memoria
fog-premul-rearquitectura. - El agua xBRZ pre-horneada (
water-xbrz.png) — mencionada de pasada en el censo de transparencia como capa ya existente; no tiene carpeta de veredicto propia con A/B. - Las mazmorras 3D y su decorado — carril propio, material en
docs/verdicts/dungeon3d-audit2/,doom-,gem-.






















