Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034 2035 2036 2037 2038 2039 2040 2041 2042 2043 2044 2045 2046 2047 2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058 2059 2060 2061 2062 2063 2064 2065 2066 2067 2068 2069 2070 2071 2072 2073 2074 2075 2076 2077 2078 2079 2080 2081 2082 2083 2084 2085 2086 2087 2088 2089 2090 2091 2092 2093 2094 2095 2096 2097 2098 2099 2100 2101 2102 2103 2104 2105 2106 2107 2108 2109 2110 2111 2112 2113 2114 2115 2116 2117 2118 2119 2120 2121 2122 2123 2124 2125 2126 2127 2128 2129 2130 2131 2132 2133 2134 2135 2136 2137 2138 2139 2140 2141 2142 2143 2144 2145 2146 2147 2148 2149 2150 2151 2152 2153 2154 2155 2156 2157 2158 2159 2160 2161 2162 2163 2164 2165 2166 2167 2168 2169 2170 2171 2172 2173 2174 2175 2176 2177 2178 2179 2180 2181 2182 2183 2184 2185 2186 2187 2188 2189 2190 2191 2192 2193 2194 2195 2196 2197 2198 2199 2200 2201 2202 2203 2204 2205 2206 2207 2208 2209 2210 2211 2212 2213 2214 2215 2216 2217 2218 2219 2220 2221 2222 2223 2224 2225 2226 2227 2228 2229 2230 2231 2232 2233 2234 2235 2236 2237 2238 2239 2240 2241 2242 2243 2244 2245 2246 2247 2248 2249 2250 2251 2252 2253 2254 2255 2256 2257 2258 2259 2260 2261 2262 2263 2264 2265 2266 2267 2268 2269 2270 2271 2272 2273 2274 2275 2276 2277 2278 2279 2280 2281 2282 2283 2284 2285 2286 2287 2288 2289 2290 2291 2292 2293 2294 2295 2296 2297 2298 2299 2300 2301 2302 2303 2304 2305 2306 2307 2308 2309 2310 2311 2312 2313 2314 2315 2316 2317 2318 2319 2320 2321 2322 2323 2324 2325 2326 2327 2328 2329 2330 2331 2332 2333 2334 2335 2336 2337 2338 2339 2340 2341 2342 2343 2344 2345 2346 2347 2348 2349 2350 2351 2352 2353 2354 2355 2356 2357 2358 2359 2360 2361 2362 2363 2364 2365 2366 2367 2368 2369 2370 2371 2372 2373 2374 2375 2376 2377 2378 2379 2380 2381 2382 2383 2384 2385 2386 2387 2388 2389 2390 2391 2392 2393 2394 2395 2396 2397 2398 2399 2400 2401 2402 2403 2404 2405 2406 2407 2408 2409 2410 2411 2412 2413 2414 2415 2416 2417 2418 2419 2420 2421 2422 2423 2424 2425 2426 2427 2428 2429 2430 2431 2432 2433 2434 2435 2436 2437 2438 2439 2440 2441 2442 2443 2444 2445 2446 2447 2448 2449 2450 2451 2452 2453 2454 2455 2456 2457 2458 2459 2460 2461 2462 2463 2464 2465 2466 2467 2468 2469 2470 2471 2472 2473 2474 2475 2476 2477 2478 2479 2480 2481 2482 2483 2484 2485 2486 2487 2488 2489 2490 2491 2492 2493 2494 2495 2496 2497 2498 2499 2500 2501 2502 2503 2504 2505 2506 2507 2508 2509 2510 2511 2512 2513 2514 2515 2516 2517 2518 2519 2520 2521 2522 2523 2524 2525 2526 2527 2528 2529 2530 2531 2532 2533 2534 2535 2536 2537 2538 2539 2540 2541 2542 2543 2544 2545 2546 2547 2548 2549 2550 2551 2552 2553 2554 2555 2556 2557 2558 2559 2560 2561 2562 2563 2564 2565 2566 2567 2568 2569 2570 2571 2572 2573 2574 2575 2576 2577 2578 2579 2580 2581 2582 2583 2584 2585 2586 2587 2588 2589 2590 2591 2592 2593 2594 2595 2596 2597 2598 2599 2600 2601 2602 2603 2604 2605 2606 2607 2608 2609 2610 2611 2612 2613 2614 2615 2616 2617 2618 2619 2620 2621 2622 2623 2624 2625 2626 2627 2628 2629 2630 2631 2632 2633 2634 2635 2636 2637 2638 2639 2640 2641 2642 2643 2644 2645 2646 2647 2648 2649 2650 2651 2652 2653 2654 2655 2656 2657 2658 2659 2660 2661 2662 2663 2664 2665 2666 2667 2668 2669 2670 2671 2672 2673 2674 2675 2676 2677 2678 2679 2680 2681 2682 2683 2684 2685 2686 2687 2688 2689 2690 2691 2692 2693 2694 2695 2696 2697 2698 2699 2700 2701 2702 2703 2704 2705 2706 2707 2708 2709 2710 2711 2712 2713 2714 2715 2716 2717 2718 2719 2720 2721 2722 2723 2724 2725 2726 2727 2728 2729 2730 2731 2732 2733 2734 2735 2736 2737 2738 2739 2740 2741 2742 2743 2744 2745 2746 2747 2748 2749 2750 2751 2752 2753 2754 2755 2756 2757 2758 2759 2760 2761 2762 2763 2764 2765 2766 2767 2768 2769 2770 2771 2772 2773 2774 2775 2776 2777 2778 2779 2780 2781 2782 2783 2784 2785 2786 2787 2788 2789 2790 2791 2792 2793 2794 2795 2796 2797 2798 2799 2800 2801 2802 2803 2804 2805 2806 2807 2808 2809 2810 2811 2812 2813 2814 2815 2816 2817 2818 2819 2820 2821 2822 2823 2824 2825 2826 2827 2828 2829 2830 2831 2832 2833 2834 2835 2836 2837 2838 2839 2840 2841 2842 2843 2844 2845 2846 2847 2848 2849 2850 2851 2852 2853 2854 2855 2856 2857 2858 2859 2860 2861 2862 2863 2864 2865 2866 2867 2868 2869 2870 2871 2872 2873 2874 2875 2876 2877 2878 2879 2880 2881 2882 2883 2884 2885 2886 2887 2888 2889 2890 2891 2892 2893 2894 2895 2896 2897 2898 2899 2900 2901 2902 2903 2904 2905 2906 2907 2908 2909 2910 2911 2912 2913 2914 2915 2916 2917 2918 2919 2920 2921 2922 2923 2924 2925 2926 2927 2928 2929 2930 2931 2932 2933 2934 2935 2936 2937 2938 2939 2940 2941 2942 2943 2944 2945 2946 2947 2948 2949 2950 2951 2952 2953 2954 2955 2956 2957 2958 2959 2960 2961 2962 2963 2964 2965 2966 2967 2968 2969 2970 2971 2972 2973 2974 2975 2976 2977 2978 2979 2980 2981 2982 2983 2984 2985 2986 2987 2988 2989 2990 2991 2992 2993 2994 2995 2996 2997 2998 2999 3000 3001 3002 3003 3004 3005 3006 3007 3008 3009 3010 3011 3012 3013 3014 3015 3016 3017 3018 3019 3020 3021 3022 3023 3024 3025 3026 3027 3028 3029 3030 3031 3032 3033 3034 3035 3036 3037 3038 3039 3040 3041 3042 3043 3044 3045 3046 3047 3048 3049 3050 3051 3052 3053 3054 3055 3056 3057 3058 3059 3060 3061 3062 3063 3064 3065 3066 3067 3068 3069 3070 3071 3072 3073 3074 3075 3076 3077 3078 3079 3080 3081 3082 3083 3084 3085 3086 3087 3088 3089 3090 3091 3092 3093 3094 3095 3096 3097 3098 3099 3100 3101 3102 3103 3104 3105 3106 3107 3108 3109 3110 3111 3112 3113 3114 3115 3116 3117 3118 3119 3120 3121 3122 3123 3124 3125 3126 3127 3128 3129 3130 3131 3132 3133 3134 3135 3136 3137 3138 3139 3140 3141 3142 3143 3144 3145 3146 3147 3148 3149 3150 3151 3152 3153 3154 3155 3156 3157 3158 3159 3160 3161 3162 3163 3164 3165 3166 3167 3168 3169 3170 3171 3172 3173 3174 3175 3176 3177 3178 3179 3180 3181 3182 3183 3184 3185 3186 3187 3188 3189 3190 3191 3192 3193 3194 3195 3196 3197 3198 3199 3200 3201 3202 3203 3204 3205 3206 3207 3208 3209 3210 3211 3212 3213 3214 3215 3216 3217 3218 3219 3220 3221 3222 3223 3224 3225 3226 3227 3228 3229 3230 3231 3232 3233 3234 3235 3236 3237 3238 3239 3240 3241 3242 3243 3244 3245 3246 3247 3248 3249 3250 3251 3252 3253 3254 3255 3256 3257 3258 3259 3260 3261 3262 | 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 2184175x 2184175x 2184175x 73x 73x 73x 371085x 371085x 371085x 371085x 371085x 450163x 450163x 450163x 108354x 108354x 108354x 108354x 108354x 103232x 103232x 450163x 450163x 111x 111x 111x 111x 111x 426235x 426235x 426124x 426235x 73x 73x 70104x 70104x 70104x 70104x 70104x 70104x 70104x 38x 38x 38x 38x 38x 38x 38x 38x 38x 70104x 26x 26x 26x 70040x 70040x 70040x 70104x 70104x 70104x 70104x 70104x 70104x 70104x 70104x 70104x 70104x 70104x 70104x 70104x 11070x 11070x 70104x 624x 624x 360x 623x 134x 264x 130x 130x 624x 624x 70104x 70104x 70104x 70104x 3x 3x 3x 3x 3x 3x 3x 3x 70104x 70104x 70104x 70104x 70104x 70104x 70104x 70104x 70104x 70104x 70104x 70104x 70104x 70104x 70104x 70104x 70040x 70040x 70040x 70040x 70040x 70040x 70104x 73x 73x 73x 73x 73x 724804x 724804x 724804x 362402x 362402x 362402x 362402x 362402x 226839x 226839x 226839x 226839x 69856x 69856x 150123x 226839x 76944x 76944x 76944x 5491x 5491x 5491x 5491x 5491x 5491x 71453x 71453x 71453x 71453x 71453x 71453x 71453x 16659x 16659x 16659x 16659x 16659x 16659x 16659x 4227x 16659x 206x 206x 206x 206x 206x 206x 206x 60x 40x 54x 20x 20x 206x 18x 146x 12x 128x 20x 114x 20x 20x 206x 206x 73x 556080x 556080x 556080x 556080x 556080x 879x 879x 556080x 22035x 22035x 556080x 48539x 48539x 556080x 1573x 1573x 556080x 1603x 1603x 556080x 1151x 1151x 556080x 1164x 1164x 556080x 4042x 4042x 556080x 4279x 4279x 556080x 4263x 4263x 556080x 4075x 4075x 556080x 206x 206x 556080x 462271x 462271x 556080x 556080x 93809x 93809x 556080x 73x 331x 26480x 556080x 556080x 26480x 331x 109096x 109096x 109096x 109096x 109096x 109096x 16043x 16043x 1409x 16043x 13130x 16043x 1504x 16043x 16043x 109096x 109096x 109096x 109096x 109096x 109096x 109096x 109096x 109096x 109096x 109096x 109096x 109096x 109096x 109096x 109096x 109096x 1291x 109096x 27582x 27582x 27582x 27582x 27582x 27582x 109096x 60884x 60884x 60884x 60884x 60884x 60884x 109096x 3971x 109096x 3974x 109096x 4192x 109096x 3906x 109096x 109096x 109096x 109096x 3060x 3060x 3060x 3060x 746x 746x 746x 3060x 812x 812x 812x 3060x 625x 625x 625x 3060x 877x 877x 3060x 3060x 3060x 3060x 1230x 1142x 953x 591x 1230x 3060x 468x 107x 468x 152x 361x 209x 209x 468x 3060x 598x 54x 598x 240x 544x 304x 304x 304x 598x 3060x 764x 91x 762x 248x 673x 425x 425x 425x 764x 3060x 3060x 3060x 109096x 234x 234x 58x 54x 54x 54x 58x 58x 58x 58x 30x 234x 234x 234x 234x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 234x 110x 104x 104x 33x 234x 63x 63x 63x 23x 234x 234x 109096x 2x 109096x 109096x 73x 73x 73x 73x 73x 73x 73x 73x 10328527x 10328527x 10328527x 71x 10328527x 1491x 1491x 119280x 119280x 1491x 71x 71x 71x 71x 71x 71x 71x 71x 71x 71x 10328527x 73x 10328527x 10328527x 10328527x 30745x 30745x 10328527x 10328527x 10328527x 10328527x 10328527x 10328527x 10328527x 10328527x 10328527x 10328527x 10328527x 10328527x 10328527x 10328527x 10328527x 10328527x 10328527x 10328527x 10328527x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 1836850x 1836850x 1836850x 1836850x 1836850x 1836850x 1771348x 1836850x 1836850x 1836850x 1836850x 1836850x 1836850x 1836850x 1836850x 1836850x 73x 73x 73x 73x 1716x 1716x 1716x 1716x 1716x 1716x 73x 36477449x 36477449x 36477449x 27649372x 27649372x 8828077x 8828077x 8828077x 36477449x 36477449x 36477449x 36477449x 36477449x 36477449x 1013x 1013x 8828077x 8827064x 8827064x 3223x 3223x 3223x 8827064x 3043x 3043x 3043x 3043x 8823841x 8820798x 7332x 7332x 7332x 8820798x 8813466x 8813466x 8813466x 8724683x 8724683x 8724683x 8724683x 5x 5x 5x 5x 8724683x 72x 72x 72x 72x 72x 72x 72x 72x 72x 72x 72x 8724678x 20611x 20611x 20611x 20611x 20611x 20611x 20611x 8724606x 468192x 468192x 468192x 468192x 8703995x 1944x 1944x 1944x 1944x 1944x 1944x 1944x 1768x 1747x 1736x 40x 1944x 8235803x 4252292x 4252292x 4252292x 4252292x 8233859x 3981567x 3981567x 3981567x 3981567x 1837600x 1837600x 1837600x 1837600x 1837600x 1837600x 1837600x 1837600x 1837600x 3981567x 2143967x 2143967x 2143967x 8813466x 55403x 55403x 55403x 55403x 88783x 33380x 33380x 1651x 1651x 1651x 33380x 31729x 2x 2x 2x 31729x 107x 107x 31727x 2086x 2086x 2086x 31620x 4452x 4452x 4452x 29534x 2419x 2419x 2419x 25082x 22663x 22663x 22663x 22663x 8828077x 8828077x 36477449x 36477449x 8828077x 8828077x 36477449x 73x 73x 73x 73x 9952x 9952x 9952x 73x 3906x 3906x 73x 6015x 6015x 73x 31x 31x 73x 73x 98646x 98646x 98646x 98646x 98646x 98646x 98646x 98646x 98646x 98646x 98646x 73x 73x 73x 3x 3x 3x 3x 73x 73x 367241x 367241x 367241x 367241x 367241x 367241x 367241x 367241x 367241x 367241x 367241x 1990x 1990x 1990x 367241x 575x 575x 364676x 367241x 98026x 98026x 266650x 367241x 367241x 367241x 367241x 367241x 10503x 10503x 10503x 10486x 6213x 6213x 367241x 3x 3x 3x 3x 3x 3x 367241x 367241x 367241x 367241x 367241x 367241x 367241x 367241x 367241x 367241x 367241x 367241x 367241x 367241x 367241x 367241x 367241x 367241x 73x 73x 356195x 356195x 356195x 356195x 356195x 356195x 356195x 345150x 345150x 356195x 73x 12996x 12996x 12996x 12996x 12996x 12996x 12996x 12996x 73x 73x 73x 73x 73x 356289x 356289x 356289x 73x 711x 711x 711x 711x 711x 711x 711x 711x 692x 692x 711x 73x 313x 313x 73x 73x 73x 73x 73x 73x 73x 73x 6272x 6272x 6272x 6272x 6272x 5569x 5569x 5569x 6272x 73x 73x 73x 73x 73x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 73x 73x 73x 73x 73x 199x 199x 199x 73x 1454x 1454x 1454x 1454x 1454x 1454x 1454x 1454x 1447x 1447x 1454x 73x 73x 11772x 11772x 11772x 11772x 11772x 11772x 11274x 230371x 230371x 703x 703x 703x 703x 230371x 10571x 11069x 11191x 10571x 10571x 19x 19x 19x 19x 10571x 11050x 11772x 9814x 9814x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 9814x 11043x 11043x 11043x 11043x 11772x 73x 362342x 362342x 362342x 362342x 362342x 362342x 362342x 362342x 362342x 330970x 7176166x 7176166x 6154x 6154x 6154x 5451x 5451x 5451x 5451x 5451x 6154x 6154x 6154x 7176166x 324816x 356188x 358646x 324816x 324816x 398x 398x 398x 324816x 355790x 362342x 194043x 194043x 1454x 1454x 1454x 194043x 354336x 354336x 362342x 343293x 343293x 362342x 73x 73x 73x 73x 41x 41x 41x 41x 41x 41x 41x 41x 41x 41x 41x 41x 41x 41x 41x 1x 1x 41x 41x 41x 41x 1260x 1260x 41x 41x 73x 73x 73x 73x 19727x 19727x 19727x 19727x 19727x 19727x 19727x 19727x 1x 19727x 19726x 19726x 19726x 19726x 19726x 19727x 19727x 19727x 73x 73x 73x 11913x 11913x 11913x 11913x 11913x 11913x 11913x 17x 17x 17x 17x 17x 17x 17x 17x 11896x 11896x 11896x 11913x 11913x 11913x 11913x 11913x 11913x 11913x 11913x 11913x 11913x 6538x 11879x 5358x 2x 5358x 5356x 5356x 11896x 11896x 11913x 73x 73x 734476x 734476x 734476x 734476x 734175x 734476x 734175x 734476x 734175x 734175x 734476x 734175x 734175x 734476x 362219x 362219x 362219x 362219x 362219x 362219x 362219x 362219x 362219x 362219x 362219x 362219x 19585x 19585x 19585x 19585x 19585x 362219x 342634x 342634x 342634x 342634x 342634x 11772x 11772x 11772x 11772x 11772x 11772x 11772x 11772x 342634x 60x 330862x 330802x 330802x 330802x 342634x 734476x 371956x 371956x 371956x 142x 142x 142x 142x 371956x 371814x 371814x 371814x 38700x 371814x 371814x 141x 141x 141x 371814x 588x 371673x 371085x 371085x 371085x 371085x 371085x 371085x 371085x 371085x 371085x 371085x 371085x 371085x 357x 357x 357x 371085x 370728x 21x 21x 370728x 12974x 12974x 12974x 370728x 371085x 371814x 371956x 734476x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 11x 11x 1461x 1461x 1461x 28x 1461x 1461x 1461x 1461x 1461x 73x 73x 10020x 10020x 10020x 10020x 335288x 49258x 49258x 105571x 105571x 105571x 49258x 4983x 49258x 44275x 44275x 335288x 286030x 286030x 286030x 335288x 10020x 10020x 73x 73x 73x 73x 73x 105210x 105210x 105210x 105210x 105210x 105210x 105210x 105210x 105210x 8311590x 8311590x 8311590x 1685214x 1685214x 1685214x 8311590x 105210x 73721x 73721x 73721x 73721x 73721x 73721x 73721x 73721x 73721x 92979x 55605x 105210x 17636x 17636x 105210x 1873150x 1873150x 1873150x 1873150x 1873150x 1873150x 1873150x 1873150x 1873150x 1873150x 1873150x 1873150x 1873150x 1x 1x 1873150x 426x 426x 426x 426x 426x 426x 426x 426x 234x 234x 426x 426x 1873150x 1873150x 1873150x 187935x 187935x 1462393x 1462393x 1274458x 1274458x 187935x 187935x 187935x 187935x 105745x 105745x 105745x 105745x 2419x 2419x 2419x 105745x 3x 3x 3x 3x 105745x 105745x 36508x 36508x 36508x 105745x 105745x 105745x 105745x 187935x 1873150x 249x 126x 126x 248x 123x 123x 123x 123x 249x 1767405x 1767405x 1767405x 1767405x 1767405x 1873150x 1873150x 290210x 290210x 290210x 1873150x 264467x 264467x 264467x 1767405x 1767405x 73721x 105210x 3946x 3946x 105210x 103493x 54753x 54753x 73721x 105210x 73x 73x 73x 73x 8396x 8396x 8396x 8396x 8396x 8396x 8396x 11766x 11766x 11766x 11766x 11766x 11766x 11766x 11766x 8396x 8396x 8396x 8396x 8396x 8396x 8396x 8396x 8396x 8396x 8396x 8396x 8396x 8396x 8396x 8396x 8396x 8396x 8396x 8396x 8396x 8396x 8396x 127x 127x 127x 8396x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 985x 985x 985x 985x 985x 985x 985x 73x 73x 985x 4948x 985x 985x 985x 4948x 985x 73x 73x 73x 73x 73x 72x 72x 72x 72x 72x 72x 72x 72x 72x 72x 72x 72x 73x 73x 73x 73x 73x 17x 17x 12x 17x 5x 5x 17x 17x 73x 73x 73x 73x 73x 4112x 4112x 4112x 4112x 4112x 4112x 209213x 209213x 209213x 209213x 209213x 209213x 209213x 17997x 17997x 209213x 209213x 4112x 73x 73x 73x 73x 408x 408x 408x 408x 408x 408x 408x 408x 4528x 4528x 4528x 256611x 256611x 4528x 4528x 4528x 4528x 4112x 4112x 4528x 408x 408x 408x 10x 10x 10x 408x 73x 73x 73x 655x 655x 655x 655x 655x 655x 73x 73x 73x 127x 127x 68x 68x 127x 127x 127x 127x 73x 73x 73x 73x 652x 652x 652x 652x 652x 652x 360x 360x 4432x 248931x 248931x 4432x 652x 292x 6349x 6349x 292x 652x 652x 652x 652x 601x 8543x 8543x 8543x 674897x 674897x 674897x 92204x 92204x 674897x 81237x 81237x 674897x 674897x 8543x 601x 652x 652x 73x 14634x 14634x 14634x 14634x 13186x 13186x 13186x 13186x 13186x 3949x 3949x 3949x 3949x 3949x 3949x 3949x 3949x 3949x 3949x 3949x 3949x 3949x 3949x 3949x 3949x 3949x 3949x 13186x 13186x 14634x 618x 618x 618x 14634x 830x 830x 14634x 14634x 830x 830x 212x 212x 830x 14634x 14634x 73x 73x 73x 73x 73x 73x 837x 837x 837x 837x 15771x 1245909x 1245909x 1245909x 1245909x 1245909x 1245909x 15771x 837x 73x 73x 73x 73x 73x 618x 618x 618x 618x 618x 618x 618x 618x 618x 618x 618x 618x 618x 618x 618x 618x 618x 618x 618x 618x 618x 618x 618x 618x 618x 618x 73x 73x 73x 1135x 1135x 1135x 730x 730x 129x 129x 129x 726x 601x 601x 601x 730x 1135x 405x 405x 10x 10x 405x 405x 405x 405x 405x 405x 405x 405x 405x 405x 405x 405x 405x 405x 405x 219x 219x 219x 219x 219x 219x 219x 219x 219x 219x 219x 219x 219x 219x 219x 219x 219x 219x 219x 219x 219x 405x 186x 186x 186x 405x 405x 405x 1135x 1135x 73x 141x 141x 141x 141x 141x 141x 141x 141x 73x 73x 73x 73x 73x 73x 406x 406x 406x 406x 406x 406x 73x 73x 2413x 2413x 2413x 73x 73x 73x 4074x 4074x 4074x 73x 487x 487x 487x 73x 73x 73x 73x 5349x 5349x 5349x 5349x 5349x 5349x 5349x 339x 339x 142x 339x 74x 74x 74x 74x 74x 74x 74x 74x 339x 339x 339x 339x 339x 339x 5010x 5010x 5010x 5010x 5010x 5010x 5010x 5010x 5010x 5010x 5010x 5010x 5316x 105210x 8311590x 8311590x 8311590x 8311590x 8311590x 8311590x 105210x 5010x 5010x 5010x 5010x 5010x 5010x 5010x 5010x 5349x 5349x 4925x 4925x 103425x 103425x 8170575x 8170575x 8170575x 8170575x 8170575x 8170575x 8170575x 8170575x 8170575x 8170575x 8170575x 8170575x 103425x 4925x 5010x 5010x 5010x 5010x 5010x 5010x 5010x 5010x 5010x 5316x 4925x 4925x 562529x 562529x 562529x 562529x 4925x 5010x 5010x 5010x 5010x 5010x 5010x 5010x 5349x 85x 85x 5010x 5010x 5010x 5010x 5010x 5010x 5349x 5349x 5349x 1244x 1244x 1244x 1244x 1244x 1244x 1244x 1244x 5010x 5010x 5010x 5316x 105210x 105210x 105210x 5010x 5010x 5010x 5010x 5010x 5010x 5010x 5010x 5010x 5010x 5349x 5010x 5010x 5349x 5010x 5010x 5010x 5010x 5010x 7x 7x 7x 5010x 5003x 5003x 5003x 5003x 5003x 5003x 5010x 5349x 73x 4100x 4100x 4100x 4100x 4100x 4100x 4100x 4100x 4100x 4100x 98400x 98400x 98400x 98400x 98400x 2571679x 2571679x 377808x 377808x 377808x 377808x 377808x 377808x 136392x 377808x 241416x 241416x 241416x 241324x 241324x 241324x 241232x 241232x 241232x 241232x 210832x 112702x 112702x 112702x 112702x 112702x 112702x 112702x 112702x 112702x 112702x 112702x 98130x 241416x 241416x 2571679x 146174x 2193871x 146174x 2047697x 1901523x 1901523x 1901523x 1901523x 2571679x 98400x 4100x 369156x 369156x 369156x 369156x 17840138x 17840138x 220378x 220378x 220378x 17840138x 369156x 369156x 4739102x 4739102x 220378x 220378x 220378x 4739102x 369156x 220378x 220378x 220378x 220378x 220378x 220378x 220378x 220378x 369156x 369156x 174049x 42833x 174049x 369156x 369156x 369156x 369156x 369156x 369156x 369156x 40752x 40752x 40752x 40752x 40752x 40752x 40752x 38978x 1639x 1639x 1639x 40752x 40752x 369156x 5916204x 5916204x 5916204x 5916204x 5916204x 5916204x 5916204x 5916204x 5916204x 5916204x 5916204x 5239902x 5239902x 676302x 676302x 676302x 5909722x 5916204x 5916204x 2117x 2089x 2117x 28x 28x 28x 28x 2117x 2117x 2117x 5909702x 5916204x 5916204x 5916204x 356x 356x 356x 356x 356x 356x 356x 356x 673829x 5916204x 5916204x 257415x 343x 343x 343x 343x 343x 257415x 257072x 257072x 257072x 257072x 257415x 5909702x 416414x 416414x 416414x 416414x 416414x 416414x 416414x 416414x 673829x 673829x 673829x 673829x 5916204x 369156x 5916204x 5916204x 5916204x 5916204x 5916204x 5916204x 1052x 1052x 414x 414x 414x 414x 1052x 196x 196x 1052x 5916204x 5916204x 5916204x 508989x 508989x 1986675x 1986675x 469x 469x 469x 469x 1x 1x 469x 1986675x 1986675x 1986675x 1986675x 1986675x 508882x 508882x 1477792x 1477792x 508989x 123063x 123063x 123063x 123063x 123063x 2x 2x 2x 123063x 49568x 49568x 49568x 123063x 508989x 385926x 385926x 385926x 385926x 385926x 19x 19x 19x 385926x 32981x 32981x 32981x 385926x 385926x 508989x 508989x 508989x 5407215x 5407215x 5407215x 5407215x 5407215x 5439716x 5916204x 5916204x 5916204x 5916204x 5916204x 6015x 6015x 6015x 6015x 6015x 6015x 5916204x 149x 149x 5407215x 5439716x 5916204x 5916204x 5916204x 5916204x 5916204x 302x 302x 5407215x 5916204x 5916204x 5916204x 302x 302x 5916204x 494x 494x 494x 494x 5407215x 5407215x 5916204x 2125x 2125x 2125x 2125x 2125x 2125x 2125x 2125x 2125x 397x 2125x 7x 7x 2125x 5407215x 5916204x 5916204x 5916204x 5916204x 5916204x 5916204x 5916204x 363424x 363424x 5916204x 146661x 146661x 146661x 5916204x 5916204x 5916204x 369156x 23261x 2044x 23261x 21217x 21217x 23261x 369156x 220378x 369156x 73x 15454x 15454x 15454x 15454x 15454x 370896x 16780728x 16780728x 370896x 15454x 369156x 369156x 369156x 220378x 220378x 220378x 369156x 369156x 15454x 15454x 73x 73x 73x 73x 73x 73x 15979x 15979x 15979x 15979x 15979x 335559x 335559x 335559x 335559x 335559x 5959625x 5959625x 5935625x 5935625x 5959625x 5959625x 5934958x 5934958x 888927x 888927x 5934958x 5934958x 5935625x 5935625x 80675x 15979x 15979x 15979x 15979x 15979x 15979x 15979x 15979x 15979x 15979x 15979x 15979x 15979x 15979x 15979x 335559x 26509161x 26509161x 26509161x 26509161x 4756962x 26509161x 26509161x 26509161x 6x 26509161x 26509161x 26509161x 26509161x 335559x 15979x 15979x 73x 15979x 15979x 15979x 15979x 335559x 335559x 335559x 15979x 73x 73x 14442x 14442x 14442x 378x 378x 14442x 14229x 14442x 2101x 14438x 12x 12x 14229x 14229x 14229x 14442x 11256x 11256x 11256x 14442x 73x 73x 73x 73x 73x 73x 73x 935x 935x 935x 935x 935x 73x 73x 73x 73x 73x 73x 73x 73x 73x 732x 732x 732x 732x 732x 732x 732x 732x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 4x 4x 4x 4x 4x 4x 84x 84x 4x 4x 73x 73x 73x 591x 591x 591x 591x 591x 591x 591x 73x 73x 73x 130x 130x 130x 130x 130x 130x 130x 73x 73x 73x 73x 73x 60984x 60984x 60984x 60984x 60984x 60984x 60984x 73x 73x 21774x 21774x 21774x 21774x 21774x 21774x 73x 73x 27950x 27950x 27950x 27950x 73x 73x 91058x 91058x 84619x 6815x 6815x 91058x 91058x 73x 73x 62190x 62190x 62190x 62190x 62190x 73x 73x 27158x 27158x 73x 73x 648x 648x 648x 648x 648x 648x 648x 73x 60672x 60672x 60672x 60672x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 73x 73x 73x 73x 73x 175x 175x 175x 175x 175x 175x 29x 29x 29x 29x 29x 29x 29x 175x 175x 146x 175x 146x 175x 175x 11x 11x 11x 175x 26x 16x 16x 75x 75x 26x 9x 9x 26x 26x 26x 175x 175x 81x 81x 77x 81x 77x 175x 28x 28x 11x 11x 11x 28x 28x 20x 20x 97x 97x 175x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 1792x 1792x 1792x 1792x 1792x 1792x 1792x 1792x 1792x 1792x 1792x 73x 73x 73x 73x 73x 73x 73x 73x 829x 829x 829x 829x 829x 829x 829x 73x 73x 73x 73x 73x 73x 73x 66x 66x 66x 66x 66x 66x 66x 73x 73x 73x 73x 73x 73x 5x 5x 5x 73x 72x 72x 72x 72x 72x 72x 72x 72x 72x 72x 72x | // display.js — Display system (port of display.c, botl.c)
// Produces ANSI terminal output matching C's tty interface.
//
// The C harness records raw terminal output: cursor movement,
// ANSI color codes, and printable characters. For screen parity,
// this module must produce the exact same byte stream.
//
// Key concepts:
// - Whitespace compression: runs of spaces become \x1b[NC (cursor forward)
// - Colors use ANSI SGR codes (30-37 standard, 90-97 bright)
// - No DEC graphics mode — uses ASCII wall/floor characters
// - Screen is 24 rows: row 0=message, rows 1-21=map, rows 22-23=status
import { game } from './gstate.js';
import { ACURR } from './attrib.js';
import { A_CHA, A_CON, A_DEX, A_INT, A_WIS } from './const.js';
import { observe_object } from './invent.js';
import { more } from './input.js';
import { m_at as map_m_at } from './map_access.js';
import { getModalOwner } from './modal_guard.js';
import { mons } from './monsters.js';
import { Blind, Blind_telepat, Detect_monsters, Hallucination, In_mines, In_sokoban, In_hell, Infravision, Is_waterlevel, Protection_from_shape_changers, See_invisible, Unblind_telepat, Underwater, Upolyd, Warning } from './macros.js';
import { COLNO, ROWNO, STONE, VWALL, HWALL, TLCORNER, TRCORNER, BLCORNER, BRCORNER, CROSSWALL, TUWALL, TDWALL, TLWALL, TRWALL, SDOOR, SCORR, DOOR, CORR, ROOM, STAIRS, LADDER, POOL, MOAT, WATER, LAVAPOOL, LAVAWALL, CLOUD, IRONBARS, FOUNTAIN, THRONE, SINK, GRAVE, ALTAR, ICE, TREE, D_NODOOR, D_BROKEN, D_ISOPEN, D_CLOSED, D_LOCKED, IS_WALL, IS_STWALL, IS_SDOOR, isok, SV0, SV1, SV2, SV3, SV4, SV5, SV6, SV7, WM_MASK, WM_C_OUTER, WM_C_INNER, WM_W_LEFT, WM_W_RIGHT, WM_W_TOP, WM_W_BOTTOM, WM_T_LONG, WM_T_BL, WM_T_BR, WM_X_TL, WM_X_TR, WM_X_BL, WM_X_BR, WM_X_TLBR, WM_X_BLTR, HI_DOMESTIC, INTRINSIC, Is_knox_level, PROTECTION, ROOMOFFSET, WIN_STOP, WIN_NOSTOP, DISP_BEAM, DISP_ALL, DISP_TETHER, DISP_FLASH, DISP_ALWAYS, DISP_CHANGE, DISP_END, DISP_FREEMEM, BACKTRACK, M_AP_NOTHING, M_AP_FURNITURE, M_AP_OBJECT, M_AP_MONSTER, TER_MAP, TER_MON, TER_OBJ, TER_TRP, AM_MASK, AM_NONE, AM_CHAOTIC, AM_NEUTRAL, AM_LAWFUL } from './const.js';
import { def_monsyms, def_oc_syms, defsyms, S_altar, S_arrow_trap, S_bars, S_blcorn, S_brcorn, S_brdnstair, S_brupstair, S_cloud, S_corr, S_crwall, S_darkroom, S_digbeam, S_dnladder, S_dnstair, S_engrcorr, S_engroom, S_expl_tl, S_fountain, S_grave, S_hcdoor, S_hodoor, S_hwall, S_ice, S_lava, S_lavawall, S_litcorr, S_ndoor, S_pool, S_room, S_sink, S_stone, S_sw_bc, S_sw_bl, S_sw_br, S_sw_ml, S_sw_mr, S_sw_tc, S_sw_tl, S_sw_tr, S_tdwall, S_throne, S_tlcorn, S_tlwall, S_trcorn, S_tree, S_trwall, S_tuwall, S_upladder, S_upstair, S_vbeam, S_vcdoor, S_vodoor, S_vwall, S_water } from './symbols.js';
import { MAXPCHARS, SYM_OFF_O, SYM_OFF_M, SYM_OFF_W, SYM_OFF_X, gs, pchar_sym, showsym, init_symbols, load_symset } from './drawing.js';
import { pushAnimFrameEvent, pushRngLogEntry, rn2_on_display_rng } from './rng.js';
import { AMULET_OF_GUARDING, BOULDER, CLASS_SYMBOLS, COIN_CLASS, CORPSE, FIRST_REAL_GEM, FIRST_SPELL, LAST_GLASS_GEM, LAST_SPELL, POTION_CLASS, RIN_PROTECTION, STATUE, objectData } from './objects.js';
import { roles as _roles_cache } from './roles.js';
import { ATR_BOLD, ATR_INVERSE, ATR_UNDERLINE, CLR_BLACK, CLR_BLUE, CLR_BRIGHT_BLUE, CLR_BRIGHT_CYAN, CLR_BRIGHT_GREEN, CLR_BRIGHT_MAGENTA, CLR_BROWN, CLR_CYAN, CLR_GRAY, CLR_GREEN, CLR_MAGENTA, CLR_ORANGE, CLR_RED, CLR_WHITE, CLR_YELLOW, NO_COLOR, DEC_TO_UNICODE } from './terminal.js';
import { setScreenAnsiLines } from './screen_capture.js';
import { cansee, couldsee, vision_reset } from './vision.js';
import { random_monster as _random_monster, random_object as _random_object } from './glyph.js';
import { bot, timebot, status_initialize, do_statusline1, do_statusline2, rank_of, get_strength_str } from './botl.js';
import { cmap_to_glyph as _cmap_to_glyph_int, trap_to_glyph as _trap_to_glyph_int, objnum_to_glyph as _objnum_to_glyph_int, body_to_glyph as _body_to_glyph_int, statuenum_to_glyph as _statuenum_to_glyph_int, monnum_to_glyph as _monnum_to_glyph_int, petnum_to_glyph as _petnum_to_glyph_int, detected_monnum_to_glyph as _detected_monnum_to_glyph_int, ridden_monnum_to_glyph as _ridden_monnum_to_glyph_int, warning_to_glyph as _warning_to_glyph_int, glyph_is_cmap as _glyph_is_cmap, glyph_to_cmap as _glyph_to_cmap, glyph_is_monster as _glyph_is_monster, glyph_to_mon as _glyph_to_mon, glyph_is_body as _glyph_is_body, glyph_is_body_piletop as _glyph_is_body_piletop, glyph_to_body_corpsenm as _glyph_to_body_corpsenm, glyph_is_statue as _glyph_is_statue, glyph_to_statue_corpsenm as _glyph_to_statue_corpsenm, glyph_is_object as _glyph_is_object, glyph_is_trap as _glyph_is_trap, glyph_is_warning as _glyph_is_warning, glyph_to_warning as _glyph_to_warning, glyph_is_normal_piletop_obj as _glyph_is_normal_piletop_obj, glyph_is_piletop_generic_obj as _glyph_is_piletop_generic_obj, glyph_is_generic_object as _glyph_is_generic_object, obj_to_glyph as _obj_to_glyph_full, mon_to_glyph as _mon_to_glyph_full, pet_to_glyph as _pet_to_glyph_full, ridden_mon_to_glyph as _ridden_mon_to_glyph_full, detected_mon_to_glyph as _detected_mon_to_glyph_full, GLYPH_INVISIBLE, GLYPH_UNEXPLORED, GLYPH_NOTHING, GLYPH_TRAP_OFF, GLYPH_ZAP_OFF, GLYPH_OBJ_OFF, GLYPH_OBJ_PILETOP_OFF, GLYPH_BODY_OFF, GLYPH_BODY_PILETOP_OFF, GLYPH_STATUE_MALE_OFF, GLYPH_STATUE_FEM_OFF, GLYPH_STATUE_MALE_PILETOP_OFF, GLYPH_STATUE_FEM_PILETOP_OFF, GLYPH_MON_MALE_OFF, GLYPH_MON_FEM_OFF, GLYPH_PET_MALE_OFF, GLYPH_PET_FEM_OFF, GLYPH_RIDDEN_MALE_OFF, GLYPH_RIDDEN_FEM_OFF, GLYPH_DETECT_MALE_OFF, GLYPH_DETECT_FEM_OFF, GLYPH_INVIS_OFF, GLYPH_WARNING_OFF, GLYPH_ALTAR_OFF, GLYPH_CMAP_STONE_OFF, GLYPH_CMAP_MAIN_OFF, GLYPH_CMAP_MINES_OFF, GLYPH_CMAP_GEH_OFF, GLYPH_CMAP_KNOX_OFF, GLYPH_CMAP_SOKO_OFF, GLYPH_CMAP_A_OFF, GLYPH_CMAP_B_OFF, GLYPH_CMAP_C_OFF, GLYPH_SWALLOW_OFF, GLYPH_EXPLODE_OFF, GLYPH_EXPLODE_FROSTY_OFF, GLYPH_UNEXPLORED_OFF, GLYPH_NOTHING_OFF, FEMALE, MALE } from './glyph.js';
// Re-exports for backward compatibility with callers importing status-line
// functions from display.js; canonical source is botl.js (matches C's botl.c).
export { bot, timebot, status_initialize, do_statusline1, do_statusline2 };
const TOPLINE_EMPTY = 0;
const TOPLINE_NEED_MORE = 1;
const TOPLINE_NON_EMPTY = 2;
// ── CLR_* to ANSI SGR color code mapping ──
// NetHack CLR_* constants (0-15) to ANSI terminal color codes
const CLR_TO_ANSI = {
[CLR_BLACK]: 90, // bright black / dark gray (matches tty capture)
[CLR_RED]: 31, // red
[CLR_GREEN]: 32, // green
[CLR_BROWN]: 33, // brown/yellow (low intensity)
[CLR_BLUE]: 34, // blue
[CLR_MAGENTA]: 35, // magenta
[CLR_CYAN]: 36, // cyan
[CLR_GRAY]: 37, // gray (default white)
[NO_COLOR]: 39, // default
[CLR_ORANGE]: 91, // bright red (closest to orange)
[CLR_BRIGHT_GREEN]: 92, // bright green
[CLR_YELLOW]: 93, // bright yellow
[CLR_BRIGHT_BLUE]: 94, // bright blue
[CLR_BRIGHT_MAGENTA]: 95, // bright magenta
[CLR_BRIGHT_CYAN]: 96, // bright cyan
[CLR_WHITE]: 97, // bright white
};
const UNICODE_TO_DEC = Object.fromEntries(
Object.entries(DEC_TO_UNICODE).map(([decChar, unicodeChar]) => [unicodeChar, decChar])
);
// ── Terrain type to defsyms index mapping ──
// Maps rm.typ values to S_* symbol indices in the defsyms table
import { WARNCOUNT, MAXEXPCHARS, def_warnsyms, SYM_OFF_P, SYM_NOTHING, SYM_UNEXPLORED, SYM_BOULDER, SYM_INVISIBLE } from './const.js';
// ── Terminal output state ──
// current ANSI foreground color (39=default)
export function resetDisplayState() {
game.curColor = 39;
for (let i = 0; i < MAXWIN; i++) game.wins[i] = null;
game.WIN_MESSAGE = -1;
game.WIN_STATUS = -1;
game.WIN_MAP = -1;
game.WIN_BASE = -1;
game.WIN_INVEN = -1;
game.erasing_tty_screen = 0;
// gbuf-cleanup Phase 3: render-buffer grid mirroring C gg.gbuf.
// Lazy-allocated by show_glyph_cell; cleared to null here so a fresh
// session starts with a clean buffer.
game.gbuf = null;
game._gbuf_start = null;
game._gbuf_stop = null;
}
function ansiColor(clr) {
return CLR_TO_ANSI[clr] ?? 39;
}
// ── Visibility ──
// Visibility check — can the hero currently SEE this tile?
function is_visible(x, y) {
const g = game;
const ux = g.u?.ux ?? 0, uy = g.u?.uy ?? 0;
const loc = g.level?.at(x, y) || null;
// Blindness suppresses visual line-of-sight; keep this helper aligned
// with C visibility semantics for display-only tint decisions.
if (Blind()) return false;
// Always visible: player's position
if (x === ux && y === uy) return true;
// Adjacent tiles are always visible
if (Math.abs(x - ux) <= 1 && Math.abs(y - uy) <= 1) return true;
// Real vision buffer matters even with the simplified display path:
// lit tiles just outside the room-bounds shortcut are still in sight.
// C vision has a narrower rule for doors/walls: only show them if the
// adjacent square toward the hero is lit too.
if (loc?.lit && couldsee(x, y)) {
const doorMask = loc?.doormask ?? loc?.flags ?? 0;
const needsLitTowardHero = loc.typ === SDOOR
|| IS_WALL(loc.typ)
|| (loc.typ === DOOR
&& (doorMask & (D_BROKEN | D_ISOPEN | D_CLOSED | D_LOCKED)) !== D_NODOOR);
if (needsLitTowardHero) {
const dx = Math.sign(ux - x);
const dy = Math.sign(uy - y);
const flev = g.level?.at(x + dx, y + dy) || null;
if (flev?.lit) return true;
} else {
return true;
}
}
// Find the room the player is in
const playerRoom = find_room_for(ux, uy);
if (playerRoom) {
// Show the full room including walls.
if (x >= playerRoom.lx - 1 && x <= playerRoom.hx + 1
&& y >= playerRoom.ly - 1 && y <= playerRoom.hy + 1) {
const isPerimeterOpening = loc?.typ === DOOR
&& (loc.flags & (D_BROKEN | D_ISOPEN | D_CLOSED | D_LOCKED)) === D_NODOOR
&& ((x === playerRoom.lx - 1 || x === playerRoom.hx + 1
|| y === playerRoom.ly - 1 || y === playerRoom.hy + 1));
if (isPerimeterOpening) {
return couldsee(x, y);
}
return true;
}
}
return false;
}
// Has this tile been seen before (in terrain memory)?
function was_seen(x, y) {
const g = game;
const loc = g.level?.at(x, y);
return !!(loc && loc.seenv);
}
function find_room_for(x, y) {
const g = game;
const rooms = g.level.rooms || [];
const nroom = g.level.nroom || 0;
if (!rooms) return null;
for (let i = 0; i < nroom; i++) {
const r = rooms[i];
if (r && x >= r.lx && x <= r.hx && y >= r.ly && y <= r.hy)
return r;
}
return null;
}
function cmap_sym_color(symIdx) {
// C ref: display.c reset_glyphmap() wall_color(main/mines/gehennom/knox/sokoban).
if (symIdx >= S_vwall && symIdx <= S_trwall) {
const uz = game.u?.uz;
if (In_sokoban(uz)) return CLR_BLUE;
if (Is_knox_level(uz)) return CLR_GRAY;
if (game.dungeons?.[uz?.dnum]?.flags?.hellish) return CLR_RED;
if (In_mines(uz)) return CLR_BROWN;
return CLR_GRAY;
}
return defsyms[symIdx]?.color ?? NO_COLOR;
}
function altar_sym_color(loc) {
const mask = (loc?.altarmask ?? 0) & AM_MASK;
if (mask === AM_NONE) return CLR_RED;
if (mask === AM_CHAOTIC || mask === AM_NEUTRAL || mask === AM_LAWFUL) return CLR_GRAY;
return CLR_BRIGHT_MAGENTA;
}
function cmap_sym_color_for_loc(symIdx, loc) {
if (symIdx === S_altar) return altar_sym_color(loc);
return cmap_sym_color(symIdx);
}
// ── Get terrain symbol and color for a location ──
export function terrain_sym_color(loc, x, y) {
const g = game;
const typ = loc.typ;
// C ref: display.c back_to_glyph() — stairs/ladder glyphs are driven
// by the level stair coordinates, not solely by rm.typ.
const up = g.level?.upstair;
const dn = g.level?.dnstair;
if (up && x === up.x && y === up.y) {
const isBranch = (g.u?.uz?.dlevel === 1 && g.u?.uz?.dnum === 0);
const sym = defsyms[isBranch ? S_brupstair : S_upstair];
return {
ch: sym?.ch ?? '<',
color: sym?.color ?? NO_COLOR,
decgfx: false,
symidx: isBranch ? S_brupstair : S_upstair,
};
}
if (dn && x === dn.x && y === dn.y) {
const sym = defsyms[S_dnstair];
return { ch: sym?.ch ?? '>', color: sym?.color ?? NO_COLOR, decgfx: false, symidx: S_dnstair };
}
// Map terrain type to defsyms index
let symIdx;
switch (typ) {
case STONE: symIdx = S_stone; break;
case SDOOR:
case VWALL:
case HWALL:
case TLCORNER:
case TRCORNER:
case BLCORNER:
case BRCORNER:
case CROSSWALL:
case TUWALL:
case TDWALL:
case TLWALL:
case TRWALL:
symIdx = wall_sym_idx(loc);
break;
case DOOR: {
const doorState = loc.flags & (D_BROKEN | D_ISOPEN | D_CLOSED | D_LOCKED);
if (doorState === D_NODOOR || (doorState & D_BROKEN)) {
symIdx = S_ndoor;
} else if (doorState & D_ISOPEN) {
symIdx = loc.horizontal ? S_hodoor : S_vodoor;
} else {
symIdx = loc.horizontal ? S_hcdoor : S_vcdoor;
}
break;
}
case SCORR: symIdx = S_stone; break; // secret corridor looks like stone
case CORR: symIdx = loc.lit ? S_litcorr : S_corr; break;
case ROOM: symIdx = S_room; break;
case STAIRS: {
// Determine up vs down, and whether it's a branch stair
if (up && x === up.x && y === up.y) {
// C ref: display.c back_to_glyph() — branch stairs are yellow
// Level 1 upstair is always a branch stair (leads out of dungeon)
const isBranch = (g.u?.uz?.dlevel === 1 && g.u?.uz?.dnum === 0);
symIdx = isBranch ? S_brupstair : S_upstair;
} else if (dn && x === dn.x && y === dn.y) {
symIdx = S_dnstair;
} else {
symIdx = S_upstair; // fallback
}
break;
}
case LADDER: symIdx = S_upladder; break;
case POOL: symIdx = S_pool; break;
case MOAT: symIdx = S_pool; break;
case WATER: symIdx = S_water; break;
case LAVAPOOL: symIdx = S_lava; break;
case CLOUD: symIdx = S_cloud; break;
case IRONBARS: symIdx = S_bars; break;
case TREE: symIdx = S_tree; break;
case FOUNTAIN: symIdx = S_fountain; break;
case THRONE: symIdx = S_throne; break;
case SINK: symIdx = S_sink; break;
case GRAVE: symIdx = S_grave; break;
case ALTAR: symIdx = S_altar; break;
case ICE: symIdx = S_ice; break;
default: symIdx = S_stone; break;
}
// Look up display symbol via the symset system (drawing.js).
// pchar_sym handles DECgraphics, IBMgraphics, etc. based on the
// active symset loaded by init_symbols() + load_symset().
const { ch: symCh, decgfx } = pchar_sym(symIdx);
const color = cmap_sym_color_for_loc(symIdx, loc);
return { ch: symCh, color, decgfx, symidx: symIdx };
}
const seenvMatrix = [
[SV2, SV1, SV0],
[SV3, 0, SV7],
[SV4, SV5, SV6],
];
function sign(z) {
return z < 0 ? -1 : (z !== 0 ? 1 : 0);
}
function set_seenv(loc, x0, y0, x, y) {
const dx = x - x0;
const dy = y0 - y;
loc.seenv |= seenvMatrix[sign(dy) + 1][sign(dx) + 1];
}
function check_pos(x, y, which) {
if (!isok(x, y)) return which;
const type = game.level?.at(x, y)?.typ ?? STONE;
if (IS_STWALL(type) || type === CORR || type === SCORR || IS_SDOOR(type)) {
return which;
}
return 0;
}
function more_than_one(a, b, c) {
return ((a && (b || c)) || (b && (a || c)) || (c && (a || b)));
}
function set_twall(x1, y1, x2, y2, x3, y3) {
const is_1 = check_pos(x1, y1, WM_T_LONG);
const is_2 = check_pos(x2, y2, WM_T_BL);
const is_3 = check_pos(x3, y3, WM_T_BR);
return more_than_one(is_1, is_2, is_3) ? 0 : (is_1 + is_2 + is_3);
}
function set_wall(x, y, horiz) {
const is_1 = horiz ? check_pos(x, y - 1, WM_W_TOP)
: check_pos(x - 1, y, WM_W_LEFT);
const is_2 = horiz ? check_pos(x, y + 1, WM_W_BOTTOM)
: check_pos(x + 1, y, WM_W_RIGHT);
return more_than_one(is_1, is_2, 0) ? 0 : (is_1 + is_2);
}
function set_corn(x1, y1, x2, y2, x3, y3, x4, y4) {
const is_1 = check_pos(x1, y1, 1);
const is_2 = check_pos(x2, y2, 1);
const is_3 = check_pos(x3, y3, 1);
const is_4 = check_pos(x4, y4, 1);
if (is_4) return WM_C_INNER;
if (is_1 && is_2 && is_3) return WM_C_OUTER;
return 0;
}
function set_crosswall(x, y) {
const is_1 = check_pos(x - 1, y - 1, 1);
const is_2 = check_pos(x + 1, y - 1, 1);
const is_3 = check_pos(x + 1, y + 1, 1);
const is_4 = check_pos(x - 1, y + 1, 1);
let wmode = is_1 + is_2 + is_3 + is_4;
if (wmode > 1) {
if (is_1 && is_3 && (is_2 + is_4 === 0)) {
wmode = WM_X_TLBR;
} else if (is_2 && is_4 && (is_1 + is_3 === 0)) {
wmode = WM_X_BLTR;
} else {
wmode = 0;
}
} else if (is_1) {
wmode = WM_X_TL;
} else if (is_2) {
wmode = WM_X_TR;
} else if (is_3) {
wmode = WM_X_BR;
} else if (is_4) {
wmode = WM_X_BL;
}
return wmode;
}
export function xy_set_wall_state(x, y) {
const lev = game.level?.at(x, y);
if (!lev) return;
let wmode;
switch (lev.typ) {
case SDOOR:
wmode = set_wall(x, y, lev.horizontal);
break;
case VWALL:
wmode = set_wall(x, y, 0);
break;
case HWALL:
wmode = set_wall(x, y, 1);
break;
case TDWALL:
wmode = set_twall(x, y - 1, x - 1, y + 1, x + 1, y + 1);
break;
case TUWALL:
wmode = set_twall(x, y + 1, x + 1, y - 1, x - 1, y - 1);
break;
case TLWALL:
wmode = set_twall(x + 1, y, x - 1, y - 1, x - 1, y + 1);
break;
case TRWALL:
wmode = set_twall(x - 1, y, x + 1, y + 1, x + 1, y - 1);
break;
case TLCORNER:
wmode = set_corn(x - 1, y - 1, x, y - 1, x - 1, y, x + 1, y + 1);
break;
case TRCORNER:
wmode = set_corn(x, y - 1, x + 1, y - 1, x + 1, y, x - 1, y + 1);
break;
case BLCORNER:
wmode = set_corn(x, y + 1, x - 1, y + 1, x - 1, y, x + 1, y - 1);
break;
case BRCORNER:
wmode = set_corn(x + 1, y, x + 1, y + 1, x, y + 1, x - 1, y - 1);
break;
case CROSSWALL:
wmode = set_crosswall(x, y);
break;
default:
wmode = -1;
break;
}
if (wmode >= 0) {
lev.wall_info = (lev.wall_info & ~WM_MASK) | wmode;
}
}
export function set_wall_state() {
for (let x = 0; x < COLNO; x++) {
for (let y = 0; y < ROWNO; y++) {
xy_set_wall_state(x, y);
}
}
}
function wall_sym_idx(loc) {
const seenv = loc.seenv & 0xff;
const mode = loc.wall_info & WM_MASK;
if (!seenv) return S_stone;
const only = (sv, bits) => ((sv & bits) && !(sv & ~bits));
const setCorner = (which, outer, inner) => {
switch (mode) {
case 0:
return which;
case WM_C_OUTER:
return (seenv & outer) ? which : S_stone;
case WM_C_INNER:
return (seenv & ~inner) ? which : S_stone;
default:
return S_stone;
}
};
const wallMatrix = [
[S_stone, S_tlcorn, S_trcorn, S_hwall, S_tdwall], // tdwall
[S_stone, S_trcorn, S_brcorn, S_vwall, S_tlwall], // tlwall
[S_stone, S_brcorn, S_blcorn, S_hwall, S_tuwall], // tuwall
[S_stone, S_blcorn, S_tlcorn, S_vwall, S_trwall], // trwall
];
const crossMatrix = [
[S_brcorn, S_blcorn, S_tlcorn, S_tuwall, S_trwall, S_crwall],
[S_blcorn, S_tlcorn, S_trcorn, S_trwall, S_tdwall, S_crwall],
[S_tlcorn, S_trcorn, S_brcorn, S_tdwall, S_tlwall, S_crwall],
[S_trcorn, S_brcorn, S_blcorn, S_tlwall, S_tuwall, S_crwall],
];
const T_stone = 0, T_tlcorn = 1, T_trcorn = 2, T_hwall = 3, T_tdwall = 4;
const C_trcorn = 0, C_brcorn = 1, C_blcorn = 2, C_tlwall = 3, C_tuwall = 4, C_crwall = 5;
switch (loc.typ) {
case SDOOR:
return loc.horizontal ? S_hwall : S_vwall;
case VWALL:
switch (mode) {
case 0: return seenv ? S_vwall : S_stone;
case 1: return (seenv & (SV1 | SV2 | SV3 | SV4 | SV5)) ? S_vwall : S_stone;
case 2: return (seenv & (SV0 | SV1 | SV5 | SV6 | SV7)) ? S_vwall : S_stone;
default: return S_stone;
}
case HWALL:
switch (mode) {
case 0: return seenv ? S_hwall : S_stone;
case 1: return (seenv & (SV3 | SV4 | SV5 | SV6 | SV7)) ? S_hwall : S_stone;
case 2: return (seenv & (SV0 | SV1 | SV2 | SV3 | SV7)) ? S_hwall : S_stone;
default: return S_stone;
}
case TLCORNER:
return setCorner(S_tlcorn, SV3 | SV4 | SV5, SV4);
case TRCORNER:
return setCorner(S_trcorn, SV5 | SV6 | SV7, SV6);
case BLCORNER:
return setCorner(S_blcorn, SV1 | SV2 | SV3, SV2);
case BRCORNER:
return setCorner(S_brcorn, SV7 | SV0 | SV1, SV0);
case TUWALL:
case TLWALL:
case TRWALL:
case TDWALL: {
let row;
let rotated = seenv;
switch (loc.typ) {
case TUWALL:
row = wallMatrix[2];
rotated = ((seenv >> 4) | (seenv << 4)) & 0xff;
break;
case TLWALL:
row = wallMatrix[1];
rotated = ((seenv >> 2) | (seenv << 6)) & 0xff;
break;
case TRWALL:
row = wallMatrix[3];
rotated = ((seenv >> 6) | (seenv << 2)) & 0xff;
break;
default:
row = wallMatrix[0];
break;
}
let col;
switch (mode) {
case 0:
if (rotated === SV4) col = T_tlcorn;
else if (rotated === SV6) col = T_trcorn;
else if (rotated & (SV3 | SV5 | SV7) || ((rotated & SV4) && (rotated & SV6))) col = T_tdwall;
else if (rotated & (SV0 | SV1 | SV2)) col = (rotated & (SV4 | SV6)) ? T_tdwall : T_hwall;
else col = T_stone;
return row[col];
case WM_T_LONG:
if ((rotated & (SV3 | SV4)) && !(rotated & (SV5 | SV6 | SV7))) {
col = T_tlcorn;
} else if ((rotated & (SV6 | SV7)) && !(rotated & (SV3 | SV4 | SV5))) {
col = T_trcorn;
} else if ((rotated & SV5) || ((rotated & (SV3 | SV4)) && (rotated & (SV6 | SV7)))) {
col = T_tdwall;
} else {
col = T_stone;
}
return row[col];
case WM_T_BL:
if (only(rotated, SV4 | SV5)) {
col = T_tlcorn;
} else if ((rotated & (SV0 | SV1 | SV2 | SV7)) && !(rotated & (SV3 | SV4 | SV5))) {
col = T_hwall;
} else if (only(rotated, SV6)) {
col = T_stone;
} else {
col = T_tdwall;
}
return row[col];
case WM_T_BR:
if (only(rotated, SV5 | SV6)) {
col = T_trcorn;
} else if ((rotated & (SV0 | SV1 | SV2 | SV3)) && !(rotated & (SV5 | SV6 | SV7))) {
col = T_hwall;
} else if (only(rotated, SV4)) {
col = T_stone;
} else {
col = T_tdwall;
}
return row[col];
default:
return S_stone;
}
}
case CROSSWALL:
switch (mode) {
case 0:
if (seenv === SV0) return S_brcorn;
if (seenv === SV2) return S_blcorn;
if (seenv === SV4) return S_tlcorn;
if (seenv === SV6) return S_trcorn;
if (!(seenv & ~(SV0 | SV1 | SV2)) && ((seenv & SV1) || seenv === (SV0 | SV2))) return S_tuwall;
if (!(seenv & ~(SV2 | SV3 | SV4)) && ((seenv & SV3) || seenv === (SV2 | SV4))) return S_trwall;
if (!(seenv & ~(SV4 | SV5 | SV6)) && ((seenv & SV5) || seenv === (SV4 | SV6))) return S_tdwall;
if (!(seenv & ~(SV0 | SV6 | SV7)) && ((seenv & SV7) || seenv === (SV0 | SV6))) return S_tlwall;
return S_crwall;
case WM_X_TL:
case WM_X_TR:
case WM_X_BL:
case WM_X_BR: {
let row;
let rotated = seenv;
switch (mode) {
case WM_X_TL:
row = crossMatrix[1];
rotated = ((seenv >> 4) | (seenv << 4)) & 0xff;
break;
case WM_X_TR:
row = crossMatrix[2];
rotated = ((seenv >> 6) | (seenv << 2)) & 0xff;
break;
case WM_X_BL:
row = crossMatrix[0];
rotated = ((seenv >> 2) | (seenv << 6)) & 0xff;
break;
default:
row = crossMatrix[3];
break;
}
if (rotated === SV4) return S_stone;
rotated &= ~SV4;
let col;
if (rotated === SV0) col = C_brcorn;
else if (rotated & (SV2 | SV3)) {
if (rotated & (SV5 | SV6 | SV7)) col = C_crwall;
else if (rotated & (SV0 | SV1)) col = C_tuwall;
else col = C_blcorn;
} else if (rotated & (SV5 | SV6)) {
if (rotated & (SV1 | SV2 | SV3)) col = C_crwall;
else if (rotated & (SV0 | SV7)) col = C_tlwall;
else col = C_trcorn;
} else if (rotated & SV1) {
col = (rotated & SV7) ? C_crwall : C_tuwall;
} else if (rotated & SV7) {
col = (rotated & SV1) ? C_crwall : C_tlwall;
} else {
col = C_crwall;
}
return row[col];
}
case WM_X_TLBR:
if (only(seenv, SV1 | SV2 | SV3)) return S_blcorn;
if (only(seenv, SV5 | SV6 | SV7)) return S_trcorn;
if (only(seenv, SV0 | SV4)) return S_stone;
return S_crwall;
case WM_X_BLTR:
if (only(seenv, SV0 | SV1 | SV7)) return S_brcorn;
if (only(seenv, SV3 | SV4 | SV5)) return S_tlcorn;
if (only(seenv, SV2 | SV6)) return S_stone;
return S_crwall;
default:
return S_stone;
}
default:
return S_stone;
}
}
// ── newsym system: per-cell display state ──
// C ref: display.c — newsym, show_glyph, _map_location, map_background,
// map_trap, map_object, display_monster, display_self, back_to_glyph
// C ref: display.c:1866 show_glyph() — store display result on map cell.
// gbuf-cleanup Phase 3: also populate game.gbuf (the canonical render
// buffer mirroring C's gg.gbuf[y][x]) as a dual-write. Readers migrate
// off of loc.disp_* and onto game.gbuf in Phase 7; until then both
// representations carry the same data.
function _ensure_gbuf() {
const g = game;
if (g.gbuf) return g.gbuf;
const rows = new Array(ROWNO);
for (let y = 0; y < ROWNO; y++) {
rows[y] = new Array(COLNO);
for (let x = 0; x < COLNO; x++) {
rows[y][x] = { gnew: 0, glyph: 0, ch: ' ', color: NO_COLOR, decgfx: false, attr: 0 };
}
}
g.gbuf = {
rows,
start: new Int32Array(ROWNO).fill(COLNO - 1),
stop: new Int32Array(ROWNO).fill(0),
};
// Keep legacy game._gbuf_start/_gbuf_stop as aliases of the same buffers
// so Phase 3 can land without touching the readers yet.
g._gbuf_start = g.gbuf.start;
g._gbuf_stop = g.gbuf.stop;
return g.gbuf;
}
export function show_glyph_cell(x, y, ch, color, decgfx, attr = 0, glyphHint = null) {
const loc = game.level?.at(x, y);
if (!loc) return;
if (game.level?.flags?.is_rogue) {
color = NO_COLOR;
}
loc.gnew = 1;
const gbuf = _ensure_gbuf();
// Mirror of C's gg.gbuf[y][x] write in show_glyph(). glyphHint is an
// explicit canonical int supplied by callers that paint a non-terrain
// layer (monster, hero, invisible marker, warning). Map-layer writers
// and ad-hoc terrain paints leave glyphHint null; we fall back to
// loc.glyph (written by the map_* functions in Phase 2), which is the
// underlying memory glyph — acceptable since those callsites really
// are painting the memory layer.
const entry = gbuf.rows[y][x];
entry.gnew = 1;
entry.glyph = (glyphHint != null) ? glyphHint : (loc.glyph ?? entry.glyph);
entry.ch = ch;
entry.color = color;
entry.decgfx = !!decgfx;
entry.attr = attr | 0;
if (gbuf.start[y] > x) gbuf.start[y] = x;
if (gbuf.stop[y] < x) gbuf.stop[y] = x;
}
// C ref: display.c:2528 glyph_at — returns the currently-rendered glyph
// (the third-screen value from gg.gbuf[y][x].glyphinfo.glyph), NOT the
// memory glyph in levl[x][y].glyph. Callers that want the underlying
// terrain/memory should use loc.glyph directly.
// gbuf-cleanup Phase 6b: map_glyphinfo(glyph, loc=null) — faithful port of
// C's reset_glyphmap + map_glyphinfo chain (display.c:2644, 2789).
// Given a canonical int glyph, compute sym.symidx (an index into showsyms)
// and color the same way C does per glyph range, then resolve ch/decgfx
// via showsym(symidx). Hallucination remap is NOT applied here — it's
// already baked into the glyph int by obj_to_glyph / mon_to_glyph at the
// call site, matching C's single-call contract.
//
// The `loc` parameter is used for the altar cmap range: JS cmap_to_glyph
// collapses all altars into one glyph, so we recover the alignment from
// loc.flags when available. C's altar_to_glyph encodes alignment in the
// glyph int directly; this is a minor JS-specific accommodation until
// cmap_to_glyph grows an altar-alignment-aware variant.
const _ALTAR_COLORS = [
CLR_RED, // altar_unaligned
CLR_GRAY, // altar_chaotic
CLR_GRAY, // altar_neutral
CLR_GRAY, // altar_lawful
CLR_BRIGHT_MAGENTA, // altar_other
];
// C ref: display.c explode_colors[] — 7 explosion types.
const _EXPLODE_COLORS = [
CLR_GRAY, // EXPL_DARK
CLR_GREEN, // EXPL_NOXIOUS
CLR_BROWN, // EXPL_MUDDY
CLR_BLUE, // EXPL_WET
CLR_MAGENTA, // EXPL_MAGICAL
CLR_ORANGE, // EXPL_FIERY
CLR_WHITE, // EXPL_FROSTY
];
// C ref: zap.js zapcolors[] (mirrored here to avoid display→zap import).
// HI_ZAP = CLR_BRIGHT_BLUE per const.js.
const _ZAP_COLORS = [
CLR_BRIGHT_BLUE, // missile (HI_ZAP)
CLR_ORANGE, // fire
CLR_WHITE, // frost
CLR_BRIGHT_BLUE, // sleep (HI_ZAP)
CLR_BLACK, // death
CLR_WHITE, // lightning
CLR_GREEN, // poison gas
CLR_YELLOW, // acid
];
const _WALL_COLORS = [
CLR_GRAY, CLR_GRAY, CLR_GRAY, CLR_GRAY, CLR_GRAY,
// main, mines, gehennom, knox, sokoban — all CLR_GRAY per wallcolors[].
];
// C ref: dat/symbols — DECgraphics / IBMgraphics symsets attach per-branch
// color overrides via G_hwall_mines: /brown and friends. These overrides
// live in C's customization_details (see glyphs.c) and apply only when
// the relevant symset is loaded. Until JS ports the full G_* customization
// loader, hardcode the symset-uniform per-branch color table here.
const _SYMSET_WALL_COLORS = {
// [main, mines, gehennom, knox, sokoban]
'DECgraphics': [CLR_GRAY, CLR_BROWN, CLR_RED, CLR_YELLOW, CLR_BLUE],
'IBMgraphics': [CLR_GRAY, CLR_BROWN, CLR_RED, CLR_YELLOW, CLR_BLUE],
'IBMGraphics_1':[CLR_GRAY, CLR_BROWN, CLR_RED, CLR_YELLOW, CLR_BLUE],
'IBMGraphics_2':[CLR_GRAY, CLR_BROWN, CLR_RED, CLR_YELLOW, CLR_BLUE],
};
// Current level's wall-branch index matching wallcolors[] enum order
// (main=0, mines=1, gehennom=2, knox=3, sokoban=4). C ref: display.h
// enum level_walls and cmap_walls_to_glyph.
function _current_wall_branch_idx() {
if (In_mines()) return 1;
if (In_hell()) return 2;
const uz = game?.u?.uz;
if (uz && game?.knox_dnum && uz.dnum === game.knox_dnum) return 3;
if (In_sokoban()) return 4;
return 0;
}
function _wall_color_for_current_branch() {
const branch = _current_wall_branch_idx();
const symset = game?.nhDisplay?.gs?.symset?.[0]?.name
?? gs?.symset?.[0]?.name
?? null;
const table = symset && _SYMSET_WALL_COLORS[symset];
return table ? table[branch] : _WALL_COLORS[branch];
}
// C ref: display.c:2748 cmap_to_roguecolor. Active when has_rogue_color
// (which requires HAS_ROGUE_IBM_GRAPHICS + symset without nocolor). In JS
// we approximate by keying off game.level.flags.is_rogue — the Rogue
// level always uses the rogue color palette.
function _cmap_to_roguecolor(cmap) {
if (cmap >= S_vwall && cmap <= S_hcdoor) return CLR_BROWN;
if (cmap >= S_arrow_trap && cmap <= /* S_polymorph_trap */ 70) return CLR_MAGENTA;
if (cmap === S_corr || cmap === S_litcorr) return CLR_GRAY;
if (cmap >= S_room && cmap <= S_water && cmap !== S_darkroom) return CLR_GREEN;
return NO_COLOR;
}
export function map_glyphinfo(glyph, loc = null) {
// Validate: unset / non-integer glyphs should not paint. Callers
// should not hand us undefined, but guard for safety.
if (!Number.isInteger(glyph) || glyph < 0) {
return { ch: ' ', color: NO_COLOR, decgfx: false };
}
// C ref: has_rogue_color = HAS_ROGUE_IBM_GRAPHICS + symset.nocolor==0.
// JS approximation: key off the current level's is_rogue flag, which
// matches when cmap colors should go through cmap_to_roguecolor.
const hasRogueColor = !!game?.level?.flags?.is_rogue;
let symidx = 0;
let color = NO_COLOR;
// C ref: reset_glyphmap order — higher offsets checked first.
if (glyph >= GLYPH_NOTHING_OFF) {
symidx = SYM_NOTHING + SYM_OFF_X;
color = NO_COLOR;
} else if (glyph >= GLYPH_UNEXPLORED_OFF) {
symidx = SYM_UNEXPLORED + SYM_OFF_X;
color = NO_COLOR;
} else if (glyph >= GLYPH_STATUE_FEM_PILETOP_OFF) {
const offset = glyph - GLYPH_STATUE_FEM_PILETOP_OFF;
symidx = (mons[offset]?.mlet ?? 0) + SYM_OFF_M;
color = objectData[STATUE]?.oc_color ?? NO_COLOR;
} else if (glyph >= GLYPH_STATUE_MALE_PILETOP_OFF) {
const offset = glyph - GLYPH_STATUE_MALE_PILETOP_OFF;
symidx = (mons[offset]?.mlet ?? 0) + SYM_OFF_M;
color = objectData[STATUE]?.oc_color ?? NO_COLOR;
} else if (glyph >= GLYPH_BODY_PILETOP_OFF) {
const offset = glyph - GLYPH_BODY_PILETOP_OFF;
symidx = (objectData[CORPSE]?.oc_class ?? 7) + SYM_OFF_O;
color = mons[offset]?.mcolor ?? NO_COLOR;
} else if (glyph >= GLYPH_OBJ_PILETOP_OFF) {
const offset = glyph - GLYPH_OBJ_PILETOP_OFF;
symidx = (objectData[offset]?.oc_class ?? 1) + SYM_OFF_O;
if (offset === BOULDER) symidx = SYM_BOULDER + SYM_OFF_X;
color = (game.objects?.[offset]?.oc_color ?? objectData[offset]?.oc_color) ?? NO_COLOR;
} else if (glyph >= GLYPH_STATUE_FEM_OFF) {
const offset = glyph - GLYPH_STATUE_FEM_OFF;
symidx = (mons[offset]?.mlet ?? 0) + SYM_OFF_M;
color = objectData[STATUE]?.oc_color ?? NO_COLOR;
} else if (glyph >= GLYPH_STATUE_MALE_OFF) {
const offset = glyph - GLYPH_STATUE_MALE_OFF;
symidx = (mons[offset]?.mlet ?? 0) + SYM_OFF_M;
color = objectData[STATUE]?.oc_color ?? NO_COLOR;
} else if (glyph >= GLYPH_WARNING_OFF) {
const offset = glyph - GLYPH_WARNING_OFF;
symidx = offset + SYM_OFF_W;
color = def_warnsyms[offset]?.color ?? NO_COLOR;
} else if (glyph >= GLYPH_EXPLODE_OFF) {
// C ref: display.c:2960 reset_glyphmap EXPLODE case.
// 7 explosion types × MAXEXPCHARS (9) positions. Position picks the
// cmap symbol (S_expl_tl..S_expl_br); type picks the color.
const offset = glyph - GLYPH_EXPLODE_OFF;
const expl_pos = offset % MAXEXPCHARS;
const expl_type = (offset / MAXEXPCHARS) | 0;
const cmap = S_expl_tl + expl_pos;
symidx = cmap + SYM_OFF_P;
color = _EXPLODE_COLORS[expl_type] ?? NO_COLOR;
} else if (glyph >= GLYPH_SWALLOW_OFF) {
// C ref: display.c:2938 reset_glyphmap SWALLOW case.
// Encoding: ((mnum << 3) | loc_off) + GLYPH_SWALLOW_OFF.
// loc_off picks the stomach-wall sym (S_sw_tl..S_sw_br);
// mnum picks the swallower's color.
const offset = glyph - GLYPH_SWALLOW_OFF;
const mnum = offset >> 3;
const loc_off = offset & 0x7;
const cmap = S_sw_tl + loc_off;
symidx = cmap + SYM_OFF_P;
color = mons[mnum]?.mcolor ?? NO_COLOR;
} else if (_glyph_is_cmap(glyph)) {
// Cmap ranges: CMAP_C, ZAP, CMAP_B, ALTAR, CMAP_A, CMAP_SOKO,
// CMAP_KNOX, CMAP_GEH, CMAP_MINES, CMAP_MAIN, CMAP_STONE.
// C checks these in descending order; do the same.
if (glyph >= GLYPH_CMAP_C_OFF) {
const offset = glyph - GLYPH_CMAP_C_OFF;
const cmap = S_digbeam + offset;
symidx = cmap + SYM_OFF_P;
color = hasRogueColor ? _cmap_to_roguecolor(cmap) : (defsyms[cmap]?.color ?? NO_COLOR);
} else if (glyph >= GLYPH_ZAP_OFF) {
// C ref: display.c:2941 reset_glyphmap ZAP case.
// Encoding: ((zap_type << 2) | dir) + GLYPH_ZAP_OFF.
// dir picks the cmap symbol (S_vbeam..S_rslant); zap_type picks
// the color via zapcolors[type]. Color often gets overridden
// by zapdir_to_glyph callers passing {sym, color: zapcolor}.
const offset = glyph - GLYPH_ZAP_OFF;
const dir = offset & 0x3;
const zap_type = (offset >> 2) & 0x7;
const cmap = S_vbeam + dir;
symidx = cmap + SYM_OFF_P;
color = _ZAP_COLORS[zap_type] ?? NO_COLOR;
} else if (glyph >= GLYPH_TRAP_OFF) {
// C ref: display.c:2820 reset_glyphmap TRAP case.
// Encoding: (trap_defsym - S_arrow_trap) + GLYPH_TRAP_OFF.
// Resolves the trap's cmap sym + defsyms color.
const offset = glyph - GLYPH_TRAP_OFF;
const cmap = S_arrow_trap + offset;
symidx = cmap + SYM_OFF_P;
color = defsyms[cmap]?.color ?? NO_COLOR;
} else if (glyph >= GLYPH_CMAP_B_OFF) {
const offset = glyph - GLYPH_CMAP_B_OFF;
const cmap = S_grave + offset;
symidx = cmap + SYM_OFF_P;
color = defsyms[cmap]?.color ?? NO_COLOR;
} else if (glyph >= GLYPH_ALTAR_OFF) {
// C uses altar_to_glyph to encode alignment in the glyph int.
// JS's cmap_to_glyph collapses altars; recover alignment from loc.
symidx = S_altar + SYM_OFF_P;
if (hasRogueColor) {
color = _cmap_to_roguecolor(S_altar);
} else {
const amsk = loc?.flags ?? AM_NONE;
if ((amsk & 0x08 /* AM_SANCTUM */) === 0x08) color = _ALTAR_COLORS[4];
else if ((amsk & AM_MASK) === AM_LAWFUL) color = _ALTAR_COLORS[3];
else if ((amsk & AM_MASK) === AM_NEUTRAL) color = _ALTAR_COLORS[2];
else if ((amsk & AM_MASK) === AM_CHAOTIC) color = _ALTAR_COLORS[1];
else color = _ALTAR_COLORS[0];
}
} else if (glyph >= GLYPH_CMAP_A_OFF) {
const offset = glyph - GLYPH_CMAP_A_OFF;
const cmap = S_ndoor + offset;
symidx = cmap + SYM_OFF_P;
color = hasRogueColor ? _cmap_to_roguecolor(cmap) : (defsyms[cmap]?.color ?? NO_COLOR);
} else if (glyph >= GLYPH_CMAP_SOKO_OFF) {
const cmap = S_vwall + (glyph - GLYPH_CMAP_SOKO_OFF);
symidx = cmap + SYM_OFF_P;
color = hasRogueColor ? _cmap_to_roguecolor(cmap) : _WALL_COLORS[4];
} else if (glyph >= GLYPH_CMAP_KNOX_OFF) {
const cmap = S_vwall + (glyph - GLYPH_CMAP_KNOX_OFF);
symidx = cmap + SYM_OFF_P;
color = hasRogueColor ? _cmap_to_roguecolor(cmap) : _WALL_COLORS[3];
} else if (glyph >= GLYPH_CMAP_GEH_OFF) {
const cmap = S_vwall + (glyph - GLYPH_CMAP_GEH_OFF);
symidx = cmap + SYM_OFF_P;
color = hasRogueColor ? _cmap_to_roguecolor(cmap) : _WALL_COLORS[2];
} else if (glyph >= GLYPH_CMAP_MINES_OFF) {
const cmap = S_vwall + (glyph - GLYPH_CMAP_MINES_OFF);
symidx = cmap + SYM_OFF_P;
color = hasRogueColor ? _cmap_to_roguecolor(cmap) : _WALL_COLORS[1];
} else if (glyph >= GLYPH_CMAP_MAIN_OFF) {
const cmap = S_vwall + (glyph - GLYPH_CMAP_MAIN_OFF);
symidx = cmap + SYM_OFF_P;
// JS cmap_to_glyph collapses all wall glyphs into the MAIN
// range, so we distinguish branches by reading the hero's
// current level here. Matches C's effective behavior where
// cmap_walls_to_glyph would have picked MINES/GEHENNOM/etc.
// offsets but map_glyphinfo would have applied the same
// wallcolors[] entry.
color = hasRogueColor ? _cmap_to_roguecolor(cmap) : _wall_color_for_current_branch();
} else { // GLYPH_CMAP_STONE_OFF
symidx = S_stone + SYM_OFF_P;
color = defsyms[S_stone]?.color ?? NO_COLOR;
}
} else if (glyph >= GLYPH_OBJ_OFF) {
const offset = glyph - GLYPH_OBJ_OFF;
symidx = (objectData[offset]?.oc_class ?? 1) + SYM_OFF_O;
if (offset === BOULDER) symidx = SYM_BOULDER + SYM_OFF_X;
color = (game.objects?.[offset]?.oc_color ?? objectData[offset]?.oc_color) ?? NO_COLOR;
} else if (glyph >= GLYPH_RIDDEN_FEM_OFF) {
const offset = glyph - GLYPH_RIDDEN_FEM_OFF;
symidx = (mons[offset]?.mlet ?? 0) + SYM_OFF_M;
color = mons[offset]?.mcolor ?? NO_COLOR;
} else if (glyph >= GLYPH_RIDDEN_MALE_OFF) {
const offset = glyph - GLYPH_RIDDEN_MALE_OFF;
symidx = (mons[offset]?.mlet ?? 0) + SYM_OFF_M;
color = mons[offset]?.mcolor ?? NO_COLOR;
} else if (glyph >= GLYPH_BODY_OFF) {
const offset = glyph - GLYPH_BODY_OFF;
symidx = (objectData[CORPSE]?.oc_class ?? 7) + SYM_OFF_O;
color = mons[offset]?.mcolor ?? NO_COLOR;
} else if (glyph >= GLYPH_DETECT_FEM_OFF) {
const offset = glyph - GLYPH_DETECT_FEM_OFF;
symidx = (mons[offset]?.mlet ?? 0) + SYM_OFF_M;
color = mons[offset]?.mcolor ?? NO_COLOR;
} else if (glyph >= GLYPH_DETECT_MALE_OFF) {
const offset = glyph - GLYPH_DETECT_MALE_OFF;
symidx = (mons[offset]?.mlet ?? 0) + SYM_OFF_M;
color = mons[offset]?.mcolor ?? NO_COLOR;
} else if (glyph >= GLYPH_INVIS_OFF) {
symidx = SYM_INVISIBLE + SYM_OFF_X;
color = NO_COLOR;
} else if (glyph >= GLYPH_PET_FEM_OFF) {
const offset = glyph - GLYPH_PET_FEM_OFF;
symidx = (mons[offset]?.mlet ?? 0) + SYM_OFF_M;
color = mons[offset]?.mcolor ?? NO_COLOR;
} else if (glyph >= GLYPH_PET_MALE_OFF) {
const offset = glyph - GLYPH_PET_MALE_OFF;
symidx = (mons[offset]?.mlet ?? 0) + SYM_OFF_M;
color = mons[offset]?.mcolor ?? NO_COLOR;
} else if (glyph >= GLYPH_MON_FEM_OFF) {
const offset = glyph - GLYPH_MON_FEM_OFF;
symidx = (mons[offset]?.mlet ?? 0) + SYM_OFF_M;
color = mons[offset]?.mcolor ?? NO_COLOR;
} else { // GLYPH_MON_MALE_OFF == 0
const offset = glyph - GLYPH_MON_MALE_OFF;
symidx = (mons[offset]?.mlet ?? 0) + SYM_OFF_M;
color = mons[offset]?.mcolor ?? NO_COLOR;
}
// C ref: final color filter (has_color / use_color). JS's use_color
// is effectively always true during replay (iflags.use_color is set).
const gameColorOn = game?.iflags?.use_color !== false;
if (color < 0 || color >= 16 /* NO_COLOR */ || !gameColorOn) color = NO_COLOR;
const { ch, decgfx } = showsym(symidx);
return { ch, color, decgfx };
}
// gbuf-cleanup Phase 7 accessors: external-friendly reads of the render
// buffer. Prefer these over reaching into loc.disp_*/game.gbuf directly.
export function rendered_cell(x, y) {
if (!isok(x, y)) return null;
return game.gbuf?.rows?.[y]?.[x] ?? null;
}
export function rendered_ch(x, y) {
return rendered_cell(x, y)?.ch ?? ' ';
}
export function rendered_color(x, y) {
return rendered_cell(x, y)?.color ?? NO_COLOR;
}
export function rendered_decgfx(x, y) {
return !!rendered_cell(x, y)?.decgfx;
}
export function glyph_at(x, y) {
if (!isok(x, y)) return _cmap_to_glyph_int(S_stone);
const gbuf = game.gbuf;
if (gbuf) {
const entry = gbuf.rows[y][x];
// entry.glyph may be 0 if show_glyph_cell has never painted here;
// in that case fall through to the memory glyph.
if (entry.gnew || entry.glyph) return entry.glyph || _cmap_to_glyph_int(S_stone);
}
const loc = game.level?.at(x, y);
return loc?.glyph ?? _cmap_to_glyph_int(S_stone);
}
// C ref: display.c:2275 back_to_glyph() — canonical glyph for the cell's
// background terrain. Wraps back_to_glyph_idx + cmap_to_glyph.
export function back_to_glyph(x, y) {
const loc = game.level?.at(x, y);
if (!loc) return _cmap_to_glyph_int(S_stone);
return _cmap_to_glyph_int(back_to_glyph_idx(loc, x, y));
}
// C ref: display.c:2275 back_to_glyph() — terrain type to S_* index
// Refactored from terrain_sym_color(): returns just the index.
function back_to_glyph_idx(loc, x, y) {
const g = game;
// C ref: back_to_glyph case STAIRS uses ptr->ladder & LA_DOWN.
// Also check stair coordinates for cells where loc.typ is not STAIRS
// but the cell is at a stair position (e.g., branch stairs on level 1).
const up = g.level?.upstair;
const dn = g.level?.dnstair;
// C ref: back_to_glyph uses known_branch_stairs(stairway_at()).
// Simplified: level 1 upstair always leads to surface (different dnum).
// TODO: port full known_branch_stairs with u_traversed tracking.
if (up && x === up.x && y === up.y) {
const isBranch = (g.u?.uz?.dlevel === 1 && g.u?.uz?.dnum === 0);
return isBranch ? S_brupstair : S_upstair;
}
if (dn && x === dn.x && y === dn.y) {
return S_dnstair;
}
// Walls: use wall_sym_idx (handles seenv-based connectivity)
if (IS_WALL(loc.typ) || loc.typ === SDOOR) {
return loc.seenv ? wall_sym_idx(loc) : S_stone;
}
switch (loc.typ) {
case STONE: return S_stone;
case SCORR: return S_stone;
case ROOM: return S_room;
case CORR: return (loc.waslit || game.flags?.lit_corridor) ? S_litcorr : S_corr;
case DOOR: {
const dm = loc.doormask ?? loc.flags ?? 0;
if (dm & D_BROKEN) return S_ndoor;
if (dm & D_ISOPEN) return loc.horizontal ? S_hodoor : S_vodoor;
if (dm & (D_CLOSED | D_LOCKED)) return loc.horizontal ? S_hcdoor : S_vcdoor;
return S_ndoor; // D_NODOOR
}
case STAIRS: {
// C ref: back_to_glyph uses ptr->ladder & LA_DOWN + known_branch_stairs
// TODO: known_branch_stairs not ported — always use normal stair symbols
const LA_DOWN = 2;
if (loc.ladder & LA_DOWN) return S_dnstair;
return S_upstair;
}
case LADDER: return S_upladder; // TODO: up vs down ladder
case POOL:
case MOAT: return S_pool;
case WATER: return S_water;
case LAVAPOOL: return S_lava;
case LAVAWALL: return S_lavawall;
case CLOUD: return S_cloud;
case IRONBARS: return S_bars;
case TREE: return S_tree;
case FOUNTAIN: return S_fountain;
case THRONE: return S_throne;
case SINK: return S_sink;
case GRAVE: return S_grave;
case ALTAR: return S_altar;
case ICE: return S_ice;
default: return S_stone;
}
}
// C ref: display.c:279 map_background() — terrain to display cell
export function map_background(x, y, show) {
const loc = game.level?.at(x, y);
if (!loc) return;
const symIdx = back_to_glyph_idx(loc, x, y);
loc.glyph = _cmap_to_glyph_int(symIdx);
const { ch, decgfx } = pchar_sym(symIdx);
const color = cmap_sym_color_for_loc(symIdx, loc);
if (show) {
show_glyph_cell(x, y, ch, color, decgfx);
}
}
// Render a specific S_* index (for dark room/corridor fixups in not-visible branch)
function map_background_with_idx(x, y, symIdx) {
const loc = game.level?.at(x, y);
if (!loc) return;
loc.glyph = _cmap_to_glyph_int(symIdx);
const { ch, decgfx } = pchar_sym(symIdx);
const color = cmap_sym_color(symIdx);
show_glyph_cell(x, y, ch, color, decgfx);
}
// gbuf-cleanup Phase 6b: remember_current_display_glyph is now a no-op.
// loc.glyph tracks memory; callers previously called this to capture the
// post-paint disp_* into remembered_glyph, but nothing reads that field
// anymore. Keep the call sites for now (they're harmless); delete in a
// follow-up mechanical cleanup once we audit for ordering dependencies.
function remember_current_display_glyph(loc, x = null, y = null) {
// Intentionally empty — see comment above.
}
// C ref: display.c:296 map_trap() — trap to display cell
function map_trap_cell(trap, show) {
const symIdx = S_arrow_trap + trap.ttyp - 1;
const loc = game.level?.at(trap.tx, trap.ty);
if (!loc) return;
loc.glyph = _trap_to_glyph_int(symIdx);
const { ch, decgfx } = pchar_sym(symIdx);
const color = defsyms[symIdx]?.color ?? NO_COLOR;
if (show) {
show_glyph_cell(trap.tx, trap.ty, ch, color, decgfx);
}
}
export function map_trap(trap, show) {
map_trap_cell(trap, show);
}
// C ref: display.c:333 map_object() — object to display cell.
// gbuf-cleanup Phase 6b: loc.glyph carries the C-faithful hallucination-
// aware glyph int (matches C's levl[x][y].glyph). The display output
// path still uses object_display_glyph to preserve bit-exact parity
// with recorded sessions until map_glyphinfo is fully aligned across
// the tail of edge cases (statue oc_color for rock class, appearance-
// shuffled colors, dim-room transitions); tracked in #412.
export function map_object(obj, show) {
const loc = game.level?.at(obj.ox, obj.oy);
if (!loc) return;
const glyph = _obj_to_glyph_full(obj, rn2_on_display_rng, Hallucination(), game.level?.objects);
loc.glyph = glyph;
if (show) {
const gi = map_glyphinfo(glyph, loc);
show_glyph_cell(obj.ox, obj.oy, gi.ch, gi.color, gi.decgfx, 0, glyph);
}
}
// gbuf-cleanup Phase 6b post-cleanup: _obj_to_int_glyph deleted — callers
// migrated to _obj_to_glyph_full (from js/glyph.js) which is the canonical
// C-faithful hallucination-aware port of display.h obj_to_glyph(obj, rng).
// C: unmap_object(x, y) — remove object from map display
export function unmap_object(x, y) {
// C ref: display.c unmap_object() — clear remembered object/invisible marker
// to trap/background/engraving (never to another remembered object).
if (!game.level?.flags?.hero_memory) return;
const loc = game.level?.at?.(x, y);
if (!loc) return;
const covers = (loc.typ === POOL || loc.typ === MOAT || loc.typ === WATER
|| loc.typ === LAVAPOOL);
const trap = !covers
? game.level.traps?.find(t => t.tx === x && t.ty === y && t.tseen)
: null;
if (trap) {
map_trap_cell(trap, false);
return;
}
if (loc.seenv) {
const ep = (!covers && (loc.typ === ROOM || loc.typ === CORR))
? game.level.engravingAt?.(x, y)
: null;
if (ep && ep.erevealed) {
if (cansee(x, y)) ep.erevealed = 1;
map_engraving_cell(loc, x, y, false);
} else {
map_background(x, y, false);
}
// C ref: unmap_object() — remembered dark room squares should stay dark.
if (loc.typ === ROOM && !loc.waslit && loc.glyph === _cmap_to_glyph_int(S_room)) {
const darkSym = game.level?.flags?.is_rogue ? S_stone : S_darkroom;
loc.glyph = _cmap_to_glyph_int(darkSym);
}
return;
}
// C ref: unseen squares default to stone.
loc.glyph = _cmap_to_glyph_int(S_stone);
}
// C ref: display.c:1098 unmap_invisible — clear an invisible-monster
// marker from a cell, returning whether one was actually cleared. Used
// by movement / kick / apply paths where the player learns that the
// previously-detected invisible monster is no longer there.
export function unmap_invisible(x, y) {
if (!isok(x, y)) return false;
if (glyph_at(x, y) !== GLYPH_INVISIBLE) return false;
unmap_object(x, y);
newsym(x, y);
return true;
}
// C ref: map_engraving (display.c) — engraving to display cell
function map_engraving_cell(loc, x, y, show) {
const result = engraving_sym_color(loc);
if (!result) return;
const engSym = (loc.typ === ROOM) ? S_engroom : S_engrcorr;
loc.glyph = _cmap_to_glyph_int(engSym);
// gbuf-cleanup Phase 6b: memory derived from loc.glyph + map_glyphinfo;
// no remembered_glyph write needed.
if (show) {
show_glyph_cell(x, y, result.ch, result.color, result.decgfx);
}
}
// Compute what _map_location would display without rendering to screen.
// Returns {ch, color, decgfx, glyph} for the remembered glyph.
function _compute_map_glyph(x, y, loc) {
const g = game;
const covers = (loc.typ === POOL || loc.typ === MOAT || loc.typ === WATER
|| loc.typ === LAVAPOOL);
// Objects
if (!covers && g.level.objects) {
for (let i = g.level.objects.length - 1; i >= 0; i--) {
const obj = g.level.objects[i];
if (obj.ox === x && obj.oy === y) {
const glyph = _obj_to_glyph_full(obj, rn2_on_display_rng, Hallucination(), g.level?.objects);
const gi = map_glyphinfo(glyph, loc);
return { ch: gi.ch, color: gi.color, decgfx: gi.decgfx, glyph };
}
}
}
// Traps
if (!covers) {
const trap = g.level.traps?.find(t => t.tx === x && t.ty === y && t.tseen);
if (trap) {
const symIdx = S_arrow_trap + trap.ttyp - 1;
const { ch, decgfx } = pchar_sym(symIdx);
return { ch, color: defsyms[symIdx]?.color ?? NO_COLOR, decgfx, glyph: _trap_to_glyph_int(symIdx) };
}
}
// Engravings
if (!covers && (loc.typ === ROOM || loc.typ === CORR)) {
const ep = g.level.engravingAt?.(x, y);
if (ep?.erevealed) {
const result = engraving_sym_color(loc);
if (result) {
const engSym = (loc.typ === ROOM) ? S_engroom : S_engrcorr;
return {
ch: result.ch,
color: result.color,
decgfx: result.decgfx,
glyph: _cmap_to_glyph_int(engSym),
};
}
}
}
// Terrain
const symIdx = back_to_glyph_idx(loc, x, y);
const { ch, decgfx } = pchar_sym(symIdx);
return { ch, color: cmap_sym_color(symIdx), decgfx, glyph: _cmap_to_glyph_int(symIdx) };
}
// C ref: display.c:448 _map_location() — objects > traps > engravings > terrain
function _map_location(x, y, show) {
const g = game;
const loc = g.level?.at(x, y);
if (!loc) return;
// C ref: covers_objects/covers_traps — pool/lava/water suppress objects/traps
const covers = (loc.typ === POOL || loc.typ === MOAT || loc.typ === WATER
|| loc.typ === LAVAPOOL);
// Objects (top of pile, newest first)
if (!covers && g.level.objects) {
for (let i = g.level.objects.length - 1; i >= 0; i--) {
const obj = g.level.objects[i];
if (obj.ox === x && obj.oy === y) {
// C ref: display.c:349 — observe_object when hero is nearby
// C checks distu(x,y) <= neardist where neardist = r*r*2-r, r=max(xray_range,2)
if (show && !Hallucination()) {
const r = Math.max(g.u?.xray_range ?? 0, 2);
const neardist = r * r * 2 - r;
const dx = x - (g.u?.ux ?? 0), dy = y - (g.u?.uy ?? 0);
if (dx * dx + dy * dy <= neardist) observe_object(obj);
}
map_object(obj, show);
return;
}
}
}
// Seen traps
if (!covers) {
const trap = g.level.traps?.find(t => t.tx === x && t.ty === y && t.tseen);
if (trap) {
map_trap_cell(trap, show);
return;
}
}
// Revealed engravings (C: spot_shows_engravings checks ROOM or CORR)
if (!covers && (loc.typ === ROOM || loc.typ === CORR)) {
const ep = g.level.engravingAt?.(x, y);
if (ep?.erevealed) {
map_engraving_cell(loc, x, y, show);
return;
}
}
// Terrain background
map_background(x, y, show);
if (show) {
remember_current_display_glyph(loc, x, y);
}
}
// C ref: display.c:751 feel_location() — tactile remap for nearby squares.
// This is intentionally narrower than newsym(): it maps the location and then
// applies dark-room/corridor memory fixups.
export function feel_location(x, y) {
const g = game;
if (!isok(x, y) || !g.level) return;
const loc = g.level.at(x, y);
if (!loc) return;
// C: underwater hero can't feel non-water tiles (except lava/ice).
if (Underwater() && loc.typ !== POOL && loc.typ !== MOAT
&& loc.typ !== WATER && loc.typ !== LAVAPOOL && loc.typ !== ICE) {
return;
}
set_seenv(loc, g.u?.ux ?? x, g.u?.uy ?? y, x, y);
// C: engravings that can be felt become revealed.
const ep = g.level.engravingAt?.(x, y);
if (ep) ep.erevealed = 1;
_map_location(x, y, true);
// C: floor/corridor darkening after tactile mapping.
if (loc.typ === ROOM && loc.glyph === _cmap_to_glyph_int(S_room)
&& (!loc.waslit || (g.flags?.dark_room && g.iflags?.wc_color !== false))) {
map_background_with_idx(x, y, g.flags?.dark_room ? S_darkroom : S_stone);
remember_current_display_glyph(loc, x, y);
} else if (loc.typ === CORR && loc.glyph === _cmap_to_glyph_int(S_litcorr) && !loc.waslit) {
map_background_with_idx(x, y, S_corr);
remember_current_display_glyph(loc, x, y);
}
// C: if a sensed monster is present, draw it on top.
if (!(g.u?.ux === x && g.u?.uy === y)) {
for (let mon = g.fmon; mon; mon = mon.nmon) {
if (!mon.dead && mon.mx === x && mon.my === y && sensemon(mon)) {
display_monster_cell(x, y, mon);
break;
}
}
}
}
// C ref: display.h:251 display_self() — show hero on map. Unified on
// map_glyphinfo: every branch computes the canonical glyph int first,
// then derives ch/color/decgfx from map_glyphinfo.
export function display_self() {
const g = game;
if (g.u?.ux == null || g.u?.uy == null) return;
const loc = g.level?.at(g.u.ux, g.u.uy);
const heroFem = g.u?.female ? FEMALE : MALE;
const apType = g.player?.m_ap_type ?? M_AP_NOTHING;
const ap = g.player?.mappearance ?? 0;
let glyph;
if (apType === M_AP_OBJECT) {
glyph = _objnum_to_glyph_int(ap);
} else if (apType === M_AP_FURNITURE) {
glyph = _cmap_to_glyph_int(ap);
} else if (apType === M_AP_MONSTER && ap >= 0 && mons?.[ap]) {
glyph = _monnum_to_glyph_int(ap, heroFem);
} else {
const pm = g.u?.umonnum ?? 0;
glyph = _monnum_to_glyph_int(pm, heroFem);
}
const gi = map_glyphinfo(glyph, loc);
show_glyph_cell(g.u.ux, g.u.uy, gi.ch, gi.color, gi.decgfx, 0, glyph);
}
// C ref: display.c:514 display_monster() — show monster on map.
// Unified on map_glyphinfo: compute the canonical glyph int once per
// branch, derive ch/color/decgfx from map_glyphinfo.
function display_monster_cell(x, y, mon) {
const loc = game.level?.at(x, y);
const monMimic = ((mon?.m_ap_type ?? M_AP_NOTHING) !== M_AP_NOTHING);
const sensed = !!(monMimic
&& (Protection_from_shape_changers() || sensemon(mon)));
const monFem = mon?.female ? FEMALE : MALE;
if (monMimic && !sensed) {
if (mon.m_ap_type === M_AP_OBJECT) {
const ap = mon.mappearance ?? 0;
const glyph = _objnum_to_glyph_int(ap);
const gi = map_glyphinfo(glyph, loc);
show_glyph_cell(x, y, gi.ch, gi.color, gi.decgfx, 0, glyph);
return;
}
if (mon.m_ap_type === M_AP_FURNITURE) {
const sym = Math.trunc(mon.mappearance ?? 0);
if (Number.isInteger(sym) && sym >= 0 && sym < defsyms.length) {
const glyph = _cmap_to_glyph_int(sym);
const gi = map_glyphinfo(glyph, loc);
show_glyph_cell(x, y, gi.ch, gi.color, gi.decgfx, 0, glyph);
return;
}
}
if (mon.m_ap_type === M_AP_MONSTER) {
const monIdx = Math.trunc(mon.mappearance ?? -1);
if (Number.isInteger(monIdx) && monIdx >= 0 && monIdx < mons.length) {
const glyph = _monnum_to_glyph_int(monIdx, monFem);
const gi = map_glyphinfo(glyph, loc);
show_glyph_cell(x, y, gi.ch, gi.color, gi.decgfx, 0, glyph);
return;
}
}
}
// Non-mimic or sensed-mimic: dispatch to pet/ridden/detected/normal
// via glyph.js mon_to_glyph family (each applies what_mon halluc remap
// on the index), then decode with map_glyphinfo.
const attr = (game.iflags?.wc_hilite_pet && mon?.mtame) ? ATR_INVERSE : 0;
// Select the appropriate monster-glyph region: pet / ridden / detected /
// normal. Mirrors the branches in display.c display_monster(). Detected
// via rayshade is not yet separately tracked in JS, so we use "detected"
// only when the monster is sensed without being visible (telepathy).
const mnum = mon?.data ? mons.indexOf(mon.data) : -1;
const hallu = Hallucination();
const monForGlyph = { mnum: (mnum < 0 ? 0 : mnum), female: mon?.female ?? false };
let g;
if (mon?.mtame) {
g = _pet_to_glyph_full(monForGlyph, rn2_on_display_rng, hallu);
} else if (mon === game.u?.usteed) {
g = _ridden_mon_to_glyph_full(monForGlyph, rn2_on_display_rng, hallu);
} else if (!mon_visible(mon) && sensemon(mon)) {
g = _detected_mon_to_glyph_full(monForGlyph, rn2_on_display_rng, hallu);
} else {
g = _mon_to_glyph_full(monForGlyph, rn2_on_display_rng, hallu);
}
const gi = map_glyphinfo(g, loc);
show_glyph_cell(x, y, gi.ch, gi.color, gi.decgfx, attr, g);
}
// C ref: display.c:918 newsym() — the central "what should this cell show?" function
export function newsym(x, y) {
const g = game;
if (!g.level) return;
// C ref: display.c _suppress_map_output() includes in_mklev.
if (g.in_mklev) return;
const loc = g.level.at(x, y);
if (!loc) return;
// C ref: display.c newsym() — when swallowed, only update hero tile.
if (g.u?.uswallow) {
if (g.u.ux === x && g.u.uy === y) {
display_self();
}
return;
}
// C ref: display.c newsym() — underwater (non-water level) only updates
// adjacent water/lava/ice tiles.
if (Underwater() && !Is_waterlevel(g.u?.uz)) {
const adjacent = Math.abs(x - (g.u?.ux ?? x)) <= 1
&& Math.abs(y - (g.u?.uy ?? y)) <= 1
&& !(x === g.u?.ux && y === g.u?.uy);
const fluidOrIce = (loc.typ === POOL || loc.typ === MOAT || loc.typ === WATER
|| loc.typ === LAVAPOOL || loc.typ === LAVAWALL || loc.typ === ICE);
if (!(fluidOrIce && adjacent)) return;
}
// C ref: newsym uses cansee(x,y) = couldsee && IN_SIGHT
const visible = cansee(x, y);
if (visible) {
// C ref: display.c:958 — remember lit condition
loc.waslit = !!loc.lit;
// C ref: display.c:960-961 — set seenv for wall rendering
set_seenv(loc, g.u?.ux ?? x, g.u?.uy ?? y, x, y);
// Mark as seen in terrain memory
if (!g._terrain_memory) g._terrain_memory = {};
g._terrain_memory[`${x},${y}`] = true;
// C ref: display.c:963-964 — reveal engravings when visible
const ep = g.level.engravingAt?.(x, y);
if (ep) ep.erevealed = 1;
// C ref: display.c:992-1028 — hero, monster, or terrain
if (g.u?.ux === x && g.u?.uy === y) {
// Hero's position: map terrain/objects underneath, then show hero
_map_location(x, y, true);
// display_self() shows hero but newsym MUST preserve the terrain
// in remembered_glyph. _map_location(..., true) already did that.
display_self();
} else {
const mon = map_m_at(x, y);
const wormTail = !!(mon && (x !== mon.mx || y !== mon.my));
const seeIt = !!(mon && (mon_visible(mon)
|| (!wormTail && (tp_sensemon(mon) || MATCH_WARN_OF_MON(mon)))));
if (mon && (seeIt || (!wormTail && Detect_monsters()))) {
// Map terrain/objects under monster (don't show them)
_map_location(x, y, false);
// newsym MUST update memory glyph with the underlying
// terrain/object even if we show a monster on top, so
// that memory renders correctly after the monster moves.
const cmg = _compute_map_glyph(x, y, loc);
loc.glyph = cmg.glyph;
display_monster_cell(x, y, mon);
} else if (mon && mon_warning(mon) && !wormTail) {
display_warning_cell(mon);
} else {
// Just terrain/objects/traps
_map_location(x, y, true);
}
}
} else {
// NOT VISIBLE
// C ref: display.c:1032-1089
if (g.u?.ux === x && g.u?.uy === y) {
// Hero at this position but can't see (blind, etc.)
set_seenv(loc, g.u.ux, g.u.uy, x, y);
_map_location(x, y, true);
display_self();
} else {
const mon = map_m_at(x, y);
const wormTail = !!(mon && (x !== mon.mx || y !== mon.my));
const seeIt = !!(mon && (tp_sensemon(mon)
|| MATCH_WARN_OF_MON(mon)
|| (see_with_infrared(mon) && mon_visible(mon))));
if (mon && (seeIt || (Detect_monsters() && !wormTail))) {
// Sensed monster out of physical sight.
// Show the monster, but DO NOT update remembered_glyph.
display_monster_cell(x, y, mon);
} else if (mon && mon_warning(mon) && !wormTail) {
display_warning_cell(mon);
} else if (was_seen(x, y) || loc.glyph) {
// C ref: display.c:1107-1128 — show remembered glyph.
// gbuf-cleanup Phase 6b: derive from loc.glyph via map_glyphinfo.
if (loc.glyph) {
const gi = map_glyphinfo(loc.glyph, loc);
show_glyph_cell(x, y, gi.ch, gi.color, gi.decgfx, 0, loc.glyph);
} else {
map_background(x, y, true);
}
// Then apply dark room/corridor fixups for light changes.
// C ref: display.c:1107-1122 — litcorr→corr, room→darkroom
const isRogue = !!g.level?.flags?.is_rogue;
const litcorrG = _cmap_to_glyph_int(S_litcorr);
const roomG = _cmap_to_glyph_int(S_room);
if (isRogue) {
if (loc.glyph === litcorrG && loc.typ === CORR) {
map_background_with_idx(x, y, S_corr);
remember_current_display_glyph(loc, x, y);
} else if (loc.glyph === roomG && loc.typ === ROOM
&& !loc.waslit) {
map_background_with_idx(x, y, S_stone);
remember_current_display_glyph(loc, x, y);
}
} else if (!loc.waslit || (game.flags?.dark_room && game.iflags?.wc_color !== false)) {
if (loc.glyph === litcorrG && loc.typ === CORR) {
map_background_with_idx(x, y, S_corr);
remember_current_display_glyph(loc, x, y);
} else if (loc.glyph === roomG && loc.typ === ROOM) {
map_background_with_idx(x, y, S_darkroom);
remember_current_display_glyph(loc, x, y);
}
}
} else {
const rendered = rendered_cell(x, y);
if (is_warning_display_char(rendered?.ch)) {
// C ref: display.c warning overlays don't persist after monster
// visibility/warning checks stop matching for that cell.
show_glyph_cell(x, y, ' ', rendered?.color ?? NO_COLOR, false);
}
}
}
}
}
// C ref: display.h — legacy glyph mapping helpers used by a handful of
// pre-Phase-6b callers (muse, detect, apply, mthrowu) that hand their
// result to show_glyph. show_glyph historically expected a showsyms
// index (SYM_OFF_M + pm), not a canonical glyph int, so these return
// the SYM_OFF_M-prefixed form to preserve the existing contract. Those
// consumers will migrate to canonical glyph ints + show_glyph_cell as
// part of the broader Phase 6b display-pipeline unification (#412).
// C display.h: pm_to_glyph / mon_to_glyph / obj_to_glyph — return canonical
// glyph ints. mon_to_glyph and obj_to_glyph consume one display-RNG call
// under Hallucination via what_mon_hallu / random_obj_to_glyph, matching
// C's macro expansion. Callers pass the result to show_glyph, which routes
// the int through map_glyphinfo at paint time (the C path).
export function pm_to_glyph(pm) {
return pm + GLYPH_MON_MALE_OFF;
}
export function mon_to_glyph(mon, rngfn) {
const mnum = (mon && Number.isInteger(mon.mnum))
? mon.mnum
: monsndx(mon?.data);
const monForGlyph = { mnum: Math.max(0, mnum), female: mon?.female ?? false };
return _mon_to_glyph_full(monForGlyph, rngfn || rn2_on_display_rng, Hallucination());
}
export function obj_to_glyph(obj, rngfn) {
return _obj_to_glyph_full(obj, rngfn || rn2_on_display_rng, Hallucination(), game.level?.objects);
}
// ── Get monster display character ──
function monster_sym_from_data(monData) {
const mlet = monData?.mlet;
if (mlet != null && mlet >= 0 && mlet < def_monsyms.length) {
const entry = def_monsyms[mlet];
if (entry && entry.sym && entry.sym !== '\\0') return entry.sym;
}
return '?';
}
// ── Get monster display color ──
function monster_color_from_data(mon, monData) {
// C ref: display.c:2677 — pet_color uses mons[n].mcolor, not CLR_WHITE.
// Tame pets use their natural monster color.
return monData?.mcolor ?? NO_COLOR;
}
// ── Get object display character ──
function object_sym(obj) {
// C ref: obj_to_glyph — statues use the depicted monster's display
// symbol, not the rock class backtick. Corpses already use '%' (FOOD_CLASS)
// which matches C, so only statues need special handling here.
if (obj.otyp === STATUE && obj.corpsenm != null) {
const mdata = mons?.[obj.corpsenm];
if (mdata && def_monsyms[mdata.mlet]?.sym) {
return def_monsyms[mdata.mlet].sym;
}
}
const oclass = obj.oclass;
if (oclass != null && CLASS_SYMBOLS[oclass]) {
return CLASS_SYMBOLS[oclass];
}
return ']'; // ILLOBJ
}
// ── Get object display color ──
// C ref: display.h:809 obj_is_generic — unexamined potions, gems, spellbooks
// display as their CLASS symbol without specific color (only class color).
function obj_is_generic(obj) {
return !obj.dknown
&& (obj.oclass === POTION_CLASS
|| (obj.otyp >= FIRST_REAL_GEM && obj.otyp <= LAST_GLASS_GEM)
|| (obj.otyp >= FIRST_SPELL && obj.otyp <= LAST_SPELL));
}
function object_color(obj) {
// C ref: display.c obj_to_glyph → objnam.c obj_color()
if (!obj) return NO_COLOR;
// C ref: generic objects use class color, not specific object color.
// This is why items look gray from a distance and get colored when examined.
if (obj_is_generic(obj)) {
const classSym = def_oc_syms[obj.oclass];
return classSym?.color ?? NO_COLOR;
}
// Corpses use the depicted monster's color.
if (obj.otyp === CORPSE && obj.corpsenm != null) {
const mdata = mons?.[obj.corpsenm];
if (mdata) return mdata.mcolor ?? NO_COLOR;
}
const od = game.objects[obj.otyp];
return od?.oc_color ?? NO_COLOR;
}
// monster_display_glyph / object_display_glyph deleted in the Phase 6b
// unification: all display paths now go through map_glyphinfo() applied
// to the canonical int glyph produced by obj_to_glyph / mon_to_glyph
// (both from js/glyph.js, both hallucination-aware with explicit rng).
// C ref: mapglyph(obj glyph) for temporary projectile rendering paths.
// Returns the canonical obj glyph int. tmp_at stores it on tglyph.glyph
// and show_glyph decodes via map_glyphinfo at paint time.
export function tmp_obj_glyph(obj) {
return _obj_to_glyph_full(obj, rn2_on_display_rng, Hallucination(), game.level?.objects);
}
function engraving_sym_color(loc) {
let symIdx = null;
if (loc?.typ === ROOM) symIdx = S_engroom;
else if (loc?.typ === CORR) symIdx = S_engrcorr;
if (symIdx == null) return null;
const { ch, decgfx } = pchar_sym(symIdx);
const color = defsyms[symIdx]?.color ?? NO_COLOR;
return { ch, color, decgfx };
}
// ── Compress spaces to cursor-forward sequences ──
// C ref: wintty.c compress_str() — replaces runs of spaces with \x1b[NC
function compress_spaces(line) {
let result = '';
let i = 0;
while (i < line.length) {
if (line[i] === ' ') {
let count = 0;
while (i < line.length && line[i] === ' ') {
count++;
i++;
}
if (count >= 5) {
result += `\x1b[${count}C`;
} else {
result += ' '.repeat(count);
}
} else {
result += line[i];
i++;
}
}
return result;
}
// ── Render a map row ──
// Returns the ANSI string for one map row (excluding trailing newline)
// render_map_row: reads from the gbuf render buffer populated by newsym
// via show_glyph_cell(). Only handles ANSI color/DEC emission — the
// "what to show" decision is made by newsym and stored in gbuf.rows[y][x].
function render_map_row(y, captureOverrides = null) {
const g = game;
if (!g.level) return '';
const gbufRow = g.gbuf?.rows?.[y];
// Find first and last columns to render.
// Use visibility and memory to determine bounds (matching C's gbuf
// start/stop which expands as newsym touches cells via show_glyph).
let firstCol = -1, lastCol = -1;
for (let x = 1; x < COLNO; x++) {
if (!g.level.at(x, y)) continue;
// Include if: visible, previously seen, or has non-space content.
if ((gbufRow?.[x]?.ch ?? ' ') !== ' ') {
if (firstCol < 0) firstCol = x;
lastCol = x;
}
}
if (firstCol < 0) return ''; // all empty
// Build the row content
let output = '';
let curAnsi = 39; // track color state within this row
let curDec = false;
let curInverse = false;
// C's tty uses literal spaces for small leading gaps; cursor-forward
// kicks in only once the gap is wider.
// Map column N renders at screen column N-1.
const gap = firstCol - 1;
if (gap > 4) {
output += `\x1b[${gap}C`;
} else if (gap > 0) {
output += ' '.repeat(gap);
}
for (let x = firstCol; x <= lastCol; x++) {
const loc = g.level.at(x, y);
const cell = gbufRow?.[x];
const override = captureOverrides?.get?.(`${x},${y}`) || null;
let ch = override?.ch ?? cell?.ch ?? ' ';
let color = g.level?.flags?.is_rogue
? NO_COLOR
: (override?.color ?? cell?.color ?? NO_COLOR);
const attr = override?.attr ?? cell?.attr ?? 0;
const inverse = !!(attr & ATR_INVERSE);
let dec = override?.decgfx ?? cell?.decgfx ?? false;
if (g.level?.flags?.is_rogue
&& ch === ' '
&& (loc?.typ === DOOR || loc?.typ === SDOOR)) {
ch = '+';
}
if (g.level?.flags?.is_rogue && dec) {
const rogueAscii = {
q: '-',
x: '|',
l: '-', k: '-', m: '-', j: '-',
n: '-', t: '-', u: '-', v: '-', w: '-',
};
ch = rogueAscii[ch] || ch;
if (ch === '~' || ch === 'a') {
ch = (loc?.typ === DOOR || loc?.typ === SDOOR) ? '+' : '.';
}
dec = false;
}
// C ref: tty uses cursor-forward for internal gaps (runs of spaces
// between content). Scan ahead to count consecutive space cells.
if (ch === ' ' && !dec) {
let spaceCount = 1;
while (x + spaceCount <= lastCol) {
const ncell = gbufRow?.[x + spaceCount];
if ((ncell?.ch ?? ' ') !== ' ' || (ncell?.decgfx ?? false)) break;
spaceCount++;
}
// If this run reaches lastCol, skip trailing spaces entirely
if (x + spaceCount > lastCol) break;
// Use cursor-forward for internal gaps (matches C's tty_curs)
if (spaceCount > 4) {
// C tty treats skipped gap cells as default-color background.
// Reset color before cursor-forward so later colored runs
// emit their own color SGR boundaries.
if (curAnsi !== 39) {
output += '\x1b[39m';
curAnsi = 39;
}
if (curInverse) {
output += '\x1b[0m';
curInverse = false;
curAnsi = 39;
}
// Exit DEC mode before cursor movement.
if (curDec) {
output += '\x0f';
curDec = false;
}
output += `\x1b[${spaceCount}C`;
x += spaceCount - 1; // -1 because loop increments
continue;
}
}
if (inverse !== curInverse) {
if (inverse) {
output += '\x1b[7m';
curInverse = true;
} else {
output += '\x1b[0m';
curInverse = false;
curAnsi = 39;
}
}
// Determine ANSI code for this color
const ansi = ansiColor(color);
// Emit color change if needed
// CLR_GRAY maps to 37 which is the default in C's tty — don't emit it
// NO_COLOR (8) maps to 39 (default)
const targetAnsi = (ansi === 37) ? 39 : ansi;
if (targetAnsi !== curAnsi) {
output += `\x1b[${targetAnsi}m`;
curAnsi = targetAnsi;
}
if (dec !== curDec) {
output += dec ? '\x0e' : '\x0f';
curDec = dec;
}
output += ch;
}
// Reset color at end of row if not default
if (curAnsi !== 39) {
output += `\x1b[39m`;
}
if (curInverse) {
output += '\x1b[0m';
}
if (curDec) {
output += '\x0f';
}
return output;
}
// do_statusline1 canonical in botl.js (re-exported below).
// C ref: do_wear.c find_ac() — compute armor class from worn equipment
// Equipment is stored on game.u (g.u.uarm, g.u.uarms, etc.)
export function find_ac() {
const g = game;
const u = g.u;
if (!u) return;
// Base AC: human = 10 (mons[PM_HUMAN].ac)
let uac = g.youmonst?.data?.ac ?? 10;
// ARM_BONUS(obj) = oc_oc1 + spe - min(erosion, oc_oc1)
function arm_bonus(obj) {
if (!obj) return 0;
const od = game.objects[obj.otyp];
if (!od) return 0;
const base = od.oc_oc1 || 0;
const spe = obj.spe || 0;
const erosion = Math.max(obj.oeroded || 0, obj.oeroded2 || 0);
return base + spe - Math.min(erosion, base);
}
if (g.u.uarm) uac -= arm_bonus(g.u.uarm);
if (g.u.uarmc) uac -= arm_bonus(g.u.uarmc);
if (g.u.uarmh) uac -= arm_bonus(g.u.uarmh);
if (g.u.uarmf) uac -= arm_bonus(g.u.uarmf);
if (g.u.uarms) uac -= arm_bonus(g.u.uarms);
if (g.u.uarmg) uac -= arm_bonus(g.u.uarmg);
if (g.u.uarmu) uac -= arm_bonus(g.u.uarmu);
// Ring of protection
if (g.u.uleft && g.u.uleft.otyp === RIN_PROTECTION)
uac -= g.u.uleft.spe || 0;
if (g.u.uright && g.u.uright.otyp === RIN_PROTECTION)
uac -= g.u.uright.spe || 0;
// Amulet of guarding
if (g.u.uamul && g.u.uamul.otyp === AMULET_OF_GUARDING)
uac -= 2;
// C ref: do_wear.c:2500 — only apply ublessed when HProtection is intrinsic
if ((g.u.uprops?.[PROTECTION]?.intrinsic || 0) & INTRINSIC)
uac -= u.ublessed || 0;
uac -= u.uspellprot || 0;
// Cap at +/- 99
if (Math.abs(uac) > 99) uac = Math.sign(uac) * 99;
// C ref: do_wear.c:2508 — only set botl when AC actually changes
if (uac !== u.uac) {
u.uac = uac;
setBotl('find_ac');
}
}
// do_statusline2 canonical in botl.js (re-exported below).
// ── TTY window management (port of wintty.c WinDesc system) ──
// C ref: wintty.h WinDesc, wintty.c window functions
// Window type constants (wintype.h)
const NHW_MESSAGE = 1;
const NHW_STATUS = 2;
const NHW_MAP = 3;
const NHW_MENU = 4;
const NHW_TEXT = 5;
const NHW_BASE = 6;
const MAXWIN = 20;
// C ref: game.wins[] array — window descriptors indexed by window ID
// WIN_MESSAGE, WIN_STATUS, WIN_MAP, WIN_BASE are assigned during init.
// wins moved to game.wins — initialized in init_display_globals
// Assigned window IDs (set during tty_init_nhwindows)
// C ref: wintty.c erasing_tty_screen guard
// Create a WinDesc matching C's struct
function makeWinDesc(type) {
return {
type, active: false, flags: 0,
offx: 0, offy: 0, rows: 0, cols: 0,
curx: 0, cury: 0, maxrow: 0, maxcol: 0,
};
}
// C ref: wintty.c tty_create_nhwindow
export function tty_create_nhwindow(type) {
for (let i = 0; i < MAXWIN; i++) {
if (!game.wins[i]) {
game.wins[i] = makeWinDesc(type);
return i;
}
}
return -1; // WIN_ERR
}
// Initialize the standard windows.
// C ref: tty_init_nhwindows creates BASE (slot 0).
// C ref: allmain.c newgame_display_rip creates MESSAGE (1), STATUS (2), MAP (3), INVEN (4).
// This matches C's allocation order so dynamic windows get the right IDs.
export function init_tty_windows() {
if (game.iflags) game.iflags.window_inited = 1;
game.WIN_BASE = tty_create_nhwindow(NHW_BASE); // slot 0
if (game.wins[game.WIN_BASE]) game.wins[game.WIN_BASE].active = true;
game.WIN_MESSAGE = tty_create_nhwindow(NHW_MESSAGE); // slot 1
if (game.wins[game.WIN_MESSAGE]) game.wins[game.WIN_MESSAGE].active = true;
game.WIN_STATUS = tty_create_nhwindow(NHW_STATUS); // slot 2
if (game.wins[game.WIN_STATUS]) game.wins[game.WIN_STATUS].active = true;
game.WIN_MAP = tty_create_nhwindow(NHW_MAP); // slot 3
if (game.wins[game.WIN_MAP]) game.wins[game.WIN_MAP].active = true;
game.WIN_INVEN = tty_create_nhwindow(NHW_MENU); // slot 4
if (game.wins[game.WIN_INVEN]) game.wins[game.WIN_INVEN].active = true;
}
// C ref: winprocs.h — #define display_nhwindow (*windowprocs.win_display_nhwindow)
// In C this is a macro dispatching through windowprocs to tty_display_nhwindow.
// In JS we have a single backend, so this just translates numeric window IDs
// to the string names that tty_display_nhwindow expects.
export async function display_nhwindow(win, blocking) {
let winName;
if (typeof win === 'string') {
winName = win;
} else if (win === game.WIN_MESSAGE) {
winName = 'message';
} else if (win === game.WIN_STATUS) {
winName = 'status';
} else if (win === game.WIN_MAP) {
winName = 'map';
} else if (win === game.WIN_BASE) {
winName = 'base';
} else {
// Unknown/null window ID — no-op (matches C behavior for uninitialized windows)
return;
}
await tty_display_nhwindow(winName, !!blocking);
}
// Export for menu.js and other callers
export { NHW_MESSAGE, NHW_STATUS, NHW_MAP, NHW_MENU, NHW_TEXT, NHW_BASE };
// ── TTY windowing primitives ──
// C ref: wintty.c tty_clear_nhwindow, docorner, tty_dismiss_nhwindow
// C ref: display.c:2189 row_refresh — repaint a row of map cells
function row_refresh(start, stop, y) {
if (!game.level) return;
const display = game?.nhDisplay || null;
if (!display?.setCell) return;
const gbufRow = game.gbuf?.rows?.[y];
for (let x = start; x <= stop && x < COLNO; x++) {
if (!game.level.at(x, y)) continue;
const row = y + 1;
const col = x - 1;
if (row < 0 || row >= display.rows || col < 0 || col >= display.cols) continue;
const cell = gbufRow?.[x];
let gridCh = cell?.ch || ' ';
if (cell?.decgfx && DEC_TO_UNICODE[gridCh]) {
gridCh = DEC_TO_UNICODE[gridCh];
}
display.setCell(col, row, gridCh, cell?.color ?? NO_COLOR, cell?.attr ?? 0, !!cell?.decgfx);
}
}
// C ref: wintty.c:3823 docorner — repaint map rows after clearing message/menu area
// C iterates rows ystart..ymax-1, calls row_refresh(xmin-offx, COLNO-1, y-offy).
// Then if ymax >= status row, calls SET_BOTLX + bot().
export async function docorner(xmin, ymax, ystart_between_menu_pages = 0) {
pushRngLogEntry(`^docorner[xmin=${xmin} ymax=${ymax} between=${ystart_between_menu_pages}]`);
const ystart = ystart_between_menu_pages || 0;
const display = game?.nhDisplay || null;
const mapOffy = 1; // map window starts one row below topline
const clearFrom = Math.max(0, xmin - 1);
// C: tty_curs + cl_end for each row, then row_refresh
// Clear display rows and repaint map content
for (let y = ystart; y < ymax; y++) {
// C: cl_end clears from current cursor column, not whole row.
if (!ystart_between_menu_pages && display && typeof display.setCell === 'function') {
for (let c = clearFrom; c < (display.cols || COLNO); c++) {
display.setCell(c, y, ' ');
}
}
// C row_refresh uses map coords: y - offy for map window.
const mapY = y - mapOffy;
if (mapY >= 0 && mapY < ROWNO) {
row_refresh(xmin, COLNO - 1, mapY);
}
}
// C ref: wintty.c:3898-3903 — if ymax reaches status row, SET_BOTLX + bot()
// Status row offset is ROWNO+1 typically (row 22 for 24-line terminal)
if (ymax >= ROWNO && !ystart_between_menu_pages) {
setBotlx('docorner');
await bot();
}
}
// C ref: wintty.c:2103 tty_dismiss_nhwindow — dismiss a window after display.
// Takes a window ID (from tty_create_nhwindow) and logs type/active from game.wins[].
export function tty_dismiss_nhwindow(winId) {
const cw = game.wins[winId];
const type = cw ? cw.type : -1;
const active = cw ? (cw.active ? 1 : 0) : 0;
pushRngLogEntry(`^tty_dismiss[win=${winId} type=${type} active=${active}]`);
if (cw) cw.active = false;
}
// C ref: wintty.c tty_destroy_nhwindow — free a window.
// In C, destroy calls dismiss if the window is still active.
export function tty_destroy_nhwindow(winId) {
const cw = game.wins[winId];
if (cw && cw.active) {
tty_dismiss_nhwindow(winId);
}
if (winId >= 0 && winId < MAXWIN) {
game.wins[winId] = null;
}
}
// C ref: wintty.c:1038 erase_menu_or_text — clear menu/text overlay from screen
// C clears the overlay area: for offx==0 (fullscreen) uses cl_eos or docrt;
// for offx!=0 (corner overlay) uses tty_curs + cl_end per row, then docorner.
export function erase_menu_or_text(winId, offx, maxrow) {
pushRngLogEntry(`^erase_menu_or_text[win=${winId} offx=${offx} maxrow=${maxrow}]`);
// C clears to end-of-line for corner overlays; fullscreen can clear rows.
// Then docrt/docorner repaints the map underneath.
const display = game?.nhDisplay || null;
if (display) {
if (offx > 0 && typeof display.setCell === 'function') {
const clearFrom = Math.max(0, offx - 1);
for (let y = 0; y <= maxrow; y++) {
for (let c = clearFrom; c < (display.cols || COLNO); c++) {
display.setCell(c, y, ' ');
}
}
} else if (typeof display.clearRow === 'function') {
for (let y = 0; y <= maxrow; y++) {
display.clearRow(y);
}
}
// Restore map content from the gbuf render buffer for cleared rows.
// C repaints via docrt/docorner after erasing; JS needs to
// restore the grid since gnew entries were already flushed.
if (game.level && display.grid) {
for (let gridRow = 1; gridRow <= Math.min(maxrow, ROWNO); gridRow++) {
const y = gridRow - 1; // grid row 1 → map y=0
const gbufRow = game.gbuf?.rows?.[y];
for (let x = 1; x < COLNO; x++) {
if (!game.level.at(x, y)) continue;
const cell = gbufRow?.[x];
if (!cell || !cell.ch || cell.ch === ' ') continue;
const col = x - 1;
let gridCh = cell.ch;
if (cell.decgfx && DEC_TO_UNICODE[gridCh]) {
gridCh = DEC_TO_UNICODE[gridCh];
}
display.setCell(col, gridRow, gridCh, cell.color ?? NO_COLOR, 0, !!cell.decgfx);
}
}
}
}
}
export async function tty_clear_nhwindow(win) {
// C ref: wintty.c:1124 tty_clear_nhwindow()
const display = game?.nhDisplay || null;
switch (win) {
case 'message': {
// C ref: wintty.c:1141-1155
const msgLen0 = (game._pending_message || '').length;
const cols0 = display?.cols || 80;
const cury0 = msgLen0 > cols0 ? 1 : 0;
if (display && display.toplin !== 0) {
// C: if (!erasing_tty_screen) { event_log("tty_clear_msg[...]"); ... }
if (!game.erasing_tty_screen) {
pushRngLogEntry(`^tty_clear_msg[cury=${cury0} toplin=${display.toplin}]`);
}
// C: home(); cl_end();
display.clearRow(0);
// C: if (cw->cury) docorner(1, cw->cury + 1, 0);
const msgLen = (game._pending_message || '').length;
const cols = display.cols || 80;
if (msgLen > cols) {
display.clearRow(1);
await docorner(1, cury0 + 1);
await bot();
}
// C: cw->curx = cw->cury = 0; toplin = TOPLINE_EMPTY;
// C ref: tty_clear_nhwindow resets the message state.
// Clear _pending_message so update_topl doesn't concatenate
// stale text with the next message.
game._pending_message = '';
display.toplin = 0;
pushRngLogEntry('^toplin[tty_clear_nhwindow=0]');
}
break;
}
case 'status':
// C: clear status rows, set botlx
setBotlx('tty_clear_nhwindow');
break;
case 'map':
// C: NHW_MAP sets botlx then falls through to NHW_BASE
setBotlx('tty_clear_nhwindow');
// FALLTHROUGH
case 'base':
// C: calls term_clear_screen (→ erase_tty_screen) unless guard is set
if (!game.erasing_tty_screen) {
await term_clear_screen();
}
break;
}
}
// After clearScreen(), re-mark all explored cells as dirty (gnew=1)
// so flush_glyph_buf repaints them from remembered_glyph. This matches
// C's docrt_flags show_glyph loop which renders ALL lev->glyph cells
// after cls(). Without this, explored cells lost by clearScreen are
// never repainted — their gnew was consumed by prior flush_glyph_buf
// calls and nothing sets it again.
function _repaint_remembered_glyphs() {
const g = game;
if (!g.level) return;
for (let y = 0; y < ROWNO; y++) {
for (let x = 1; x < COLNO; x++) {
const loc = g.level.at(x, y);
if (loc?.glyph) {
const gi = map_glyphinfo(loc.glyph, loc);
show_glyph_cell(x, y, gi.ch, gi.color, gi.decgfx, 0, loc.glyph);
}
}
}
}
// C ref: termcap.c:836 term_clear_screen → wintty.c erase_tty_screen
// Iterates all active windows calling tty_clear_nhwindow.
// Uses erasing_tty_screen guard to prevent recursion from MAP/BASE.
export async function term_clear_screen() {
if (game.erasing_tty_screen++) return;
// C: iterate game.wins[0..MAXWIN], call tty_clear_nhwindow for each active
// Standard windows: message, status, map, base
await tty_clear_nhwindow('message');
await tty_clear_nhwindow('status');
await tty_clear_nhwindow('map');
// base: physical screen clear
const display = game?.nhDisplay || null;
if (display && typeof display.clearScreen === 'function') {
display.clearScreen();
}
// C ref: erase_tty_screen clears the terminal but NOT gbuf (the glyph
// buffer). The gbuf entries must persist so erase_menu_or_text can
// restore the map after overlay dismissal. Do NOT clear gbuf here.
//
// After clearing the grid, mark ALL explored cells dirty (gnew=1) so
// the next flush_glyph_buf repaints them. C's terminal accumulates
// painted cells; JS's grid loses them on clearScreen. C compensates
// via docrt_flags' show_glyph(x,y,lev->glyph) loop that renders ALL
// cells after cls(). JS's docrt does the same from remembered_glyph,
// but only for cells explored at docrt time. This call catches cells
// explored after the last docrt but before this clearScreen.
_repaint_remembered_glyphs();
game._gridCleared = true;
game.erasing_tty_screen = 0;
}
// C ref: wintty.c:1980 tty_display_nhwindow()
// Handles display/refresh for each window type.
export async function tty_display_nhwindow(win, blocking, menuInfo) {
const display = game?.nhDisplay || null;
switch (win) {
case 'message':
// C ref: wintty.c:1873-1879 tty_display_nhwindow(WIN_MESSAGE)
if (display && display.toplin === TOPLINE_NEED_MORE) {
await more(display);
display.toplin = TOPLINE_NEED_MORE; // C: more resets this
await tty_clear_nhwindow('message');
} else if (display) {
display.toplin = TOPLINE_EMPTY;
pushRngLogEntry('^toplin[tty_display_nhwindow=0]');
}
break;
case 'menu': {
// C ref: wintty.c:2061 — if toplin==NEED_MORE, display WIN_MESSAGE first
if (display && display.toplin === TOPLINE_NEED_MORE) {
await tty_display_nhwindow('message', true);
}
// C ref: wintty.c:2054-2076 (H2344_BROKEN path enabled in patched C):
// offx = min(min(82, cols / 2), cols - maxcol - 1)
// fullscreen_menu = (maxrow >= rows || !menu_overlay)
{
const rows = display?.rows || 24;
const cols = display?.cols || 80;
const menuOverlay = (game.iflags?.menu_overlay !== false);
const maxcol = menuInfo?.maxcol || 0;
const maxrow = menuInfo?.maxrow || 0;
const offx = Math.min(
Math.min(82, Math.trunc(cols / 2)),
cols - maxcol - 1
);
const fullscreen = (maxrow >= rows || !menuOverlay);
if (fullscreen) {
// C ref: wintty.c:2080-2087 — fullscreen menu path.
// C directly sets toplin=EMPTY here (NOT via a separate
// WIN_MESSAGE display_nhwindow call). The event is logged
// as "toplin[tty_display_nhwindow=0]" from THIS function.
if (display) {
display.toplin = TOPLINE_EMPTY;
pushRngLogEntry('^toplin[tty_display_nhwindow=0]');
}
// C ref: tty_clear_nhwindow on MAP + STATUS in fullscreen
// menu clear path emits two SET_BOTLX marks.
setBotlx('tty_clear_nhwindow');
setBotlx('tty_clear_nhwindow');
// C ref: when menu_overlay is off (and offy==0), tty uses
// term_clear_screen(), which emits an additional MAP+STATUS
// clear pair before dismiss.
if (!menuOverlay) {
setBotlx('tty_clear_nhwindow');
setBotlx('tty_clear_nhwindow');
}
if (display && typeof display.clearScreen === 'function') {
display.clearScreen();
_repaint_remembered_glyphs();
game._gridCleared = true;
}
} else {
// C: overlay — only clear WIN_MESSAGE
await tty_clear_nhwindow('message');
}
}
break;
}
}
}
export async function cls() {
// C ref: display.c:2178 cls() calls:
// 1. display_nhwindow(WIN_MESSAGE, FALSE) — flushes messages / more()
// 2. disp.botlx = TRUE (SET_BOTLX in patched C)
// 3. clear_nhwindow(WIN_MAP) → await tty_clear_nhwindow(MAP) → fallthrough → term_clear_screen
await tty_display_nhwindow('message', false);
setBotlx('cls');
await tty_clear_nhwindow('map');
}
// C ref: display.c:1691 docrt → docrt_flags — full screen redraw.
// Called from goto_level after level transition. In C, docrt calls
// cls() which calls clear_nhwindow(WIN_MAP) → await tty_clear_nhwindow(MAP)
// → fallthrough to BASE → term_clear_screen(). We separate this into
// docrt_cls which does the full sequence including term_clear_screen.
export async function docrt_cls() {
await tty_display_nhwindow('message', false);
setBotlx('cls');
// C: NHW_MAP botlx + fallthrough to NHW_BASE → term_clear_screen
setBotlx('tty_clear_nhwindow');
await term_clear_screen();
}
// status_initialize canonical in botl.js (re-exported below).
export function setBotl(site) {
pushRngLogEntry(`^botl[${site}]`);
game.disp.botl = true;
}
// C ref: disp.botl = TRUE — no event logging, just flag set
export function SET_BOTL() { game.disp.botl = true; }
export function setBotlx(site) {
pushRngLogEntry(`^botlx[${site}]`);
game.disp.botlx = true;
}
export function setTimeBotl(site) {
pushRngLogEntry(`^time_botl[${site}]`);
game.disp.time_botl = true;
}
// ── docrt: full screen redraw ──
// C ref: display.c docrt()
// Produces a complete screen frame as an ANSI string
export async function docrt(forceStateRedraw = false, captureOnly = false) {
const g = game;
const display = game?.nhDisplay || null;
const modalOwner = getModalOwner();
const _docrt_early_exit = !g.level || (!forceStateRedraw && g._menuActive)
|| (!forceStateRedraw && (modalOwner === 'item-prompt' || modalOwner === 'dir-prompt'
|| modalOwner === 'yn' || modalOwner === 'more' || modalOwner === 'getlin') && display?.grid);
if (_docrt_early_exit) {
if (modalOwner === 'more' && display?.grid
&& (display.toplin === TOPLINE_NEED_MORE
|| display.toplin === TOPLINE_NON_EMPTY)
&& game._screen_output) {
const prevLines = String(game._screen_output).split('\n');
const currLines = serialize_terminal_grid(display).split('\n');
const top = String(currLines[0] || '');
const inventoryLikeMore = /^[A-Za-z$*?] - /.test(top);
if (prevLines.length >= 24
&& inventoryLikeMore
&& String(prevLines[0] || '').endsWith('--More--')) {
if (currLines.length >= 24
&& String(currLines[0] || '').endsWith('--More--')) {
const out = [];
out.push(currLines[0] || '');
for (let y = 1; y <= ROWNO; y++) {
out.push(prevLines[y] || '');
}
out.push(currLines[22] || '');
out.push(currLines[23] || '');
g._screen_output = out.join('\n');
return;
}
}
}
// Chargen/startup pager windows and active menu overlays render
// directly into the tty grid. Menu content is written to the grid
// by menu.js before nhgetch; serializing from grid preserves it.
if (display?.grid) g._screen_output = serialize_terminal_grid(display);
return;
}
// C ref: docrt_flags flow:
// 1. vision_recalc(2) — blank vision (clear old state)
// 2. cls() — clear screen
// 3. show_glyph loop — render stored lev->glyph for all cells
// 4. vision_recalc(0) — full vision recalc
// 5. see_monsters() — overlay monsters on map
//
// JS equivalent: blank vision, render from remembered glyphs,
// then do a full vision recalc + newsym to show the correct state.
vision_recalc(2);
// Step 3: render stored display state (C: show_glyph(x, y, lev->glyph)).
// gbuf-cleanup Phase 6b: derive from loc.glyph via map_glyphinfo.
for (let y = 0; y < ROWNO; y++) {
for (let x = 1; x < COLNO; x++) {
const loc = g.level?.at(x, y);
if (loc && loc.glyph) {
const gi = map_glyphinfo(loc.glyph, loc);
show_glyph_cell(x, y, gi.ch, gi.color, gi.decgfx, 0, loc.glyph);
}
}
}
// Capture-only parity guard:
// nhgetch-boundary capture should not rewrite out-of-sight remembered
// dark-room cells in the emitted frame. Snapshot existing dark-room room
// cells and, if capture redraw converts them to room, override only the
// rendered output for this frame (do not mutate game state).
let captureDarkroomSnapshot = null;
let captureDarkroomOverrides = null;
const captureAtInputBoundary = captureOnly
&& (game?.nhDisplay?.terminal?.inputQueueLength ?? 0) === 0;
if (captureOnly && g.level && !g._reveal_terrain_active) {
captureDarkroomSnapshot = [];
for (let y = 0; y < ROWNO; y++) {
const gbufRow = g.gbuf?.rows?.[y];
for (let x = 1; x < COLNO; x++) {
const loc = g.level.at(x, y);
if (!loc || loc.typ !== ROOM || loc.glyph !== _cmap_to_glyph_int(S_darkroom)) continue;
const cell = gbufRow?.[x];
captureDarkroomSnapshot.push({
x,
y,
ch: cell?.ch,
color: cell?.color,
decgfx: cell?.decgfx,
attr: cell?.attr ?? 0,
});
}
}
}
// C ref: docrt_flags steps 4+5:
// 4. vision_recalc(0) — the main update loop newsyms cells where
// IN_SIGHT/seenv changed (proper seenv accumulation).
// 5. see_monsters() — overlay visible monsters on map.
// C does NOT newsym every cell. Only vision_recalc's main loop
// newsyms changed cells. An all-cells newsym would accumulate
// extra seenv bits on walls from the hero's current angle.
vision_recalc(0);
see_monsters();
if (captureDarkroomSnapshot) {
captureDarkroomOverrides = new Map();
for (const snap of captureDarkroomSnapshot) {
const loc = g.level?.at(snap.x, snap.y);
if (!loc || loc.typ !== ROOM) continue;
if (captureAtInputBoundary && loc.glyph !== _cmap_to_glyph_int(S_darkroom)) {
captureDarkroomOverrides.set(`${snap.x},${snap.y}`, {
ch: snap.ch,
color: snap.color,
decgfx: snap.decgfx,
attr: snap.attr ?? 0,
});
}
}
}
// C ref: docrt_flags line 1810 — SET_BOTLX after newsym loop.
// During startup, cls+docrt run before the session captures step 0,
// so their botlx events don't appear in the session data.
// Only emit during gameplay (after level is generated).
// Skip when called for screen capture (captureOnly) — C doesn't call
// docrt at nhgetch boundaries, so the capture hook must not produce
// gameplay side-effects like setBotlx.
if (!captureOnly && game.level?.objects?.length > 0) {
setBotlx('docrt_flags');
}
let output = '';
// Row 0: message line
// C: gt.toplines stays visible while toplin is NEED_MORE or NON_EMPTY.
// toplin=0 (EMPTY): blank line
// toplin=1 (NEED_MORE): message visible, waiting to be acknowledged
// toplin=2 (NON_EMPTY): acknowledged message still visible
const toplin = display?.toplin ?? 0;
let msg = '';
if (toplin === TOPLINE_NEED_MORE || toplin === TOPLINE_NON_EMPTY) {
// C keeps two related message states:
// - visible row-0 text currently on the terminal
// - remembered gt.toplines used for future update_topl decisions
// After WIN_STOP suppression, those can diverge: the visible row
// stays old while remembered toplines keep advancing. Screen
// capture must serialize the visible row, not the remembered one.
msg = display?.topMessage ?? g._pending_message ?? '';
}
output += compress_spaces(msg);
output += '\n';
// Rows 1-21: map (y=0 to y=ROWNO-1)
for (let y = 0; y < ROWNO; y++) {
output += render_map_row(y, captureDarkroomOverrides);
output += '\n';
}
// Row 22: status line 1
output += compress_spaces(do_statusline1());
output += '\n';
// Row 23: status line 2
output += do_statusline2();
// No trailing newline on last line
g._screen_output = output;
// Render to DOM terminal grid for browser display.
// Only in browser mode (display.container exists) — headless tests
// use gnew tracking + flush_glyph_buf for grid updates.
if (display?.container) {
render_screen_output_to_display(display);
}
// C: gt.toplines persists until replaced by pline or cleared by more().
// Do NOT clear _pending_message here — it stays visible on screen.
if (display && g.u) {
if (toplin === TOPLINE_NEED_MORE && msg.startsWith('#')
&& display.cursorVisible && display.cursorRow === 0) {
const cols = Number.isInteger(display.cols) ? display.cols : COLNO;
display.setCursor(Math.min(display.cursorCol, cols - 1), 0);
} else if (toplin === TOPLINE_NEED_MORE && msg.endsWith('--More--')) {
display.setCursor(msg.length, 0);
} else if (Number.isInteger(g.getposx) && Number.isInteger(g.getposy)
&& g.getposx > 0 && g.getposy >= 0) {
// C ref: getpos.c sets gg.getposx/gg.getposy while targeting;
// cursor follows targeting location instead of hero position.
display.setCursor(g.getposx - 1, g.getposy + 1);
} else {
// C ref: display.c curs_on_u() → tty_curs(WIN_MAP, u.ux, u.uy)
// tty_curs does: curx = --x + offx, cury = y + offy
// MAP window: offx=0, offy=1. x is 1-based, --x makes it 0-based.
// So terminal cursor = (u.ux - 1, u.uy + 1).
display.setCursor(g.u.ux - 1, g.u.uy + 1);
}
}
}
export function render_screen_output_to_display(display) {
if (!display?.grid) return;
const output = game._screen_output || '';
if (!output) return; // nothing to render — don't clear the screen
const lines = output.split('\n');
// Clear the display, then write parsed ANSI content.
// Use display.setCell for each cell so BOTH the grid data AND the
// DOM spans (if present) are updated through a single code path.
display.clearScreen();
const src = Array.isArray(lines) ? lines : [];
for (let r = 0; r < display.rows && r < src.length; r++) {
const line = String(src[r] || '');
let i = 0, col = 0;
let fg = CLR_GRAY, attr = 0;
let decGraphics = false;
while (i < line.length && col < display.cols) {
const ch = line[i];
if (ch === '\x1b' && line[i + 1] === '[') {
// ANSI escape
i += 2;
let param = '';
while (i < line.length && !/[A-Za-z]/.test(line[i])) { param += line[i]; i++; }
const cmd = line[i] || ''; i++;
if (cmd === 'C') {
col += parseInt(param) || 1;
} else if (cmd === 'm') {
const codes = param ? param.split(';').map(Number) : [0];
for (const code of codes) {
if (code === 0) { fg = CLR_GRAY; attr = 0; }
else if (code === 1) attr |= 2;
else if (code === 4) attr |= 4;
else if (code === 7) attr |= 1;
else if (code === 22) attr &= ~2;
else if (code === 24) attr &= ~4;
else if (code === 27) attr &= ~1;
else if (code >= 30 && code <= 37) fg = code - 30;
else if (code >= 90 && code <= 97) {
switch (code) {
case 90: fg = CLR_BLACK; break;
case 91: fg = CLR_ORANGE; break;
case 92: fg = CLR_BRIGHT_GREEN; break;
case 93: fg = CLR_YELLOW; break;
case 94: fg = CLR_BRIGHT_BLUE; break;
case 95: fg = CLR_BRIGHT_MAGENTA; break;
case 96: fg = CLR_BRIGHT_CYAN; break;
case 97: fg = CLR_WHITE; break;
}
}
else if (code === 39) fg = CLR_GRAY;
}
}
} else if (ch === '\x0e') {
decGraphics = true; i++;
} else if (ch === '\x0f') {
decGraphics = false; i++;
} else {
const decoded = decGraphics ? (DEC_TO_UNICODE[ch] || ch) : ch;
display.setCell(col, r, decoded, fg, attr, decGraphics);
col++; i++;
}
}
}
}
function serialize_terminal_row(row, rowIndex = 0) {
let lastCol = -1;
let firstCol = -1;
for (let c = row.length - 1; c >= 0; c--) {
const cell = row[c];
if (cell.ch !== ' ' || (cell.attr ?? 0) || !!cell.decgfx) {
lastCol = c;
break;
}
}
if (lastCol < 0) return '';
for (let c = 0; c <= lastCol; c++) {
const cell = row[c];
if (cell.ch !== ' ' || (cell.attr ?? 0) || !!cell.decgfx) {
firstCol = c;
break;
}
}
if (firstCol < 0) return '';
let output = '';
let activeColor = CLR_GRAY;
let activeInverse = false;
let activeBold = false;
let activeUnderline = false;
let activeDecGraphics = false;
let sawDecOutput = false;
let justResetToDefaultStyle = false;
const mapRow = rowIndex >= 1 && rowIndex <= ROWNO;
if (firstCol > 0) {
if (firstCol > 4) output += `\x1b[${firstCol}C`;
else output += ' '.repeat(firstCol);
}
const rogueMapRow = !!game.level?.flags?.is_rogue && mapRow;
const terrainMode = game.iflags?.terrainmode ?? 0;
const terrainOnlyView = mapRow
&& (terrainMode & TER_MAP) !== 0
&& (terrainMode & (TER_TRP | TER_OBJ | TER_MON)) === 0;
const terrainRevealView = mapRow && !!game._reveal_terrain_active;
function shouldEmitNoopShiftBeforeSpace(spaceCol, runLen = 1) {
const start = spaceCol + runLen;
if (start > lastCol) return false;
const prev = spaceCol > firstCol ? row[spaceCol - 1] : null;
const prevColorRaw = prev?.color ?? CLR_GRAY;
const prevColor = rogueMapRow ? CLR_GRAY : (prevColorRaw === NO_COLOR ? CLR_GRAY : prevColorRaw);
const prevStyled = !!prev && (prevColor !== CLR_GRAY || !!(prev.attr & (ATR_INVERSE | ATR_BOLD | ATR_UNDERLINE)) || !!prev.decgfx);
if ((prev?.ch || '') === '#') return false;
if (!prevStyled) return false;
const preview = row.slice(start, Math.min(lastCol + 1, start + 16))
.map((c) => c.ch || ' ')
.join('');
return preview.startsWith('--More--') || /^[A-Za-z] - /.test(preview);
}
function updateStyle(cell) {
// NO_COLOR (8) maps to ANSI 39 (default), same as CLR_GRAY (7→37 normalized to 39).
// Normalize both to CLR_GRAY so the grid path matches render_map_row's behavior.
const rawColor = cell.color ?? CLR_GRAY;
const wantColor = rogueMapRow ? CLR_GRAY : (rawColor === NO_COLOR ? CLR_GRAY : rawColor);
const wantInverse = !!(cell.attr & ATR_INVERSE);
const wantBold = !!(cell.attr & ATR_BOLD);
const wantUnderline = !!(cell.attr & ATR_UNDERLINE);
if (wantColor === activeColor
&& wantInverse === activeInverse
&& wantBold === activeBold
&& wantUnderline === activeUnderline) {
return;
}
// Preserve C-like minimal SGR when only inverse toggles and color
// stays the same (e.g., highlighted pet glyph on same foreground).
if (wantColor === activeColor
&& wantBold === activeBold
&& wantUnderline === activeUnderline
&& wantInverse !== activeInverse) {
if (wantInverse) {
output += '\x1b[7m';
} else {
// C tty emits inverse-off (27m) rather than full reset (0m)
// when only inverse toggles and other style/color are unchanged.
output += '\x1b[27m';
}
activeInverse = wantInverse;
return;
}
if (wantInverse && !activeInverse
&& !wantBold && !activeBold
&& !wantUnderline && !activeUnderline
&& wantColor !== activeColor) {
output += '\x1b[7m';
if (wantColor !== CLR_GRAY) {
output += `\x1b[${ansiColor(wantColor)}m`;
} else {
output += '\x1b[39m';
}
activeColor = wantColor;
activeInverse = true;
return;
}
const defaultStyle = (wantColor === CLR_GRAY
&& !wantInverse && !wantBold && !wantUnderline);
if (defaultStyle) {
if (activeInverse && !activeBold && !activeUnderline) {
// C tty tends to clear inverse separately from color reset.
output += '\x1b[27m';
if (activeColor !== CLR_GRAY) {
output += '\x1b[39m';
}
} else {
output += (activeInverse || activeBold || activeUnderline)
? '\x1b[0m'
: '\x1b[39m';
}
justResetToDefaultStyle = true;
} else {
const sgr = [];
if (wantColor !== CLR_GRAY) sgr.push(String(ansiColor(wantColor)));
if (wantInverse) sgr.push('7');
if (wantBold) sgr.push('1');
if (wantUnderline) sgr.push('4');
output += `\x1b[${sgr.join(';')}m`;
justResetToDefaultStyle = false;
}
activeColor = wantColor;
activeInverse = wantInverse;
activeBold = wantBold;
activeUnderline = wantUnderline;
}
for (let c = firstCol; c <= lastCol; c++) {
const cell = row[c];
const mx = c + 1;
const my = rowIndex - 1;
const mapLoc = mapRow ? (game.level?.at(mx, my) || null) : null;
let renderCell = cell;
if ((terrainOnlyView || terrainRevealView) && mapLoc) {
// TER_MAP-only view shows underlying terrain without mons/objs/traps.
if (mapLoc.typ === ROOM) {
const roomColor = (mapLoc.glyph === _cmap_to_glyph_int(S_darkroom))
? (defsyms[S_darkroom]?.color ?? NO_COLOR)
: NO_COLOR;
renderCell = { ...cell, ch: '·', color: roomColor };
} else if (mapLoc.typ === CORR) {
renderCell = { ...cell, ch: '#', color: NO_COLOR };
}
}
const rogueDoorSpace = rogueMapRow && cell.ch === ' '
&& (mapLoc?.typ === DOOR || mapLoc?.typ === SDOOR);
if (cell.ch === ' ' && !rogueDoorSpace && c > firstCol && row[c - 1].ch !== ' ') {
let run = 1;
while (c + run <= lastCol) {
const next = row[c + run];
if (rogueMapRow) {
const nmx = c + run + 1;
const nmy = rowIndex - 1;
const nloc = game.level?.at(nmx, nmy) || null;
if (next.ch === ' ' && (nloc?.typ === DOOR || nloc?.typ === SDOOR)) {
break;
}
}
const nc = next.color ?? CLR_GRAY, cc = cell.color ?? CLR_GRAY;
if (next.ch !== ' '
|| (nc === NO_COLOR ? CLR_GRAY : nc) !== (cc === NO_COLOR ? CLR_GRAY : cc)
|| (next.attr ?? 0) !== (cell.attr ?? 0)
|| !!next.decgfx !== !!cell.decgfx) {
break;
}
run++;
}
if (run > 4) {
updateStyle(cell);
if (!activeDecGraphics
&& sawDecOutput
&& justResetToDefaultStyle
&& shouldEmitNoopShiftBeforeSpace(c, run)) {
output += '\x0e\x0f';
justResetToDefaultStyle = false;
}
if (activeDecGraphics) {
output += '\x0f';
activeDecGraphics = false;
}
output += `\x1b[${run}C`;
} else {
updateStyle(cell);
if (!activeDecGraphics
&& sawDecOutput
&& justResetToDefaultStyle
&& shouldEmitNoopShiftBeforeSpace(c, run)) {
output += '\x0e\x0f';
justResetToDefaultStyle = false;
}
if (activeDecGraphics) {
output += '\x0f';
activeDecGraphics = false;
}
output += ' '.repeat(run);
}
c += run - 1;
continue;
}
let styleCell = renderCell;
// C-faithful dark-room tint for grid serialization:
// when out-of-sight room cells are rendered in capture paths,
// preserve dark-room color for parity with tty output.
if (!rogueMapRow
&& mapLoc?.glyph === _cmap_to_glyph_int(S_room)
&& mapLoc?.typ === ROOM
&& game.flags?.dark_room
&& game.iflags?.wc_color !== false
&& (renderCell.ch === '·' || renderCell.ch === '~')
&& !cansee(mx, my)) {
const renderedColor = rendered_color(mx, my);
const locColor = (renderedColor === NO_COLOR) ? CLR_GRAY : renderedColor;
if (locColor === CLR_GRAY) {
styleCell = { ...styleCell, color: CLR_BLACK };
}
}
if (terrainOnlyView && (styleCell.ch === '·' || styleCell.ch === '~') && styleCell.color === CLR_BLACK) {
styleCell = { ...styleCell, color: defsyms[S_room]?.color ?? NO_COLOR };
}
if (!rogueMapRow
&& (terrainOnlyView || terrainRevealView)
&& mapLoc?.typ === ROOM
&& game.flags?.dark_room
&& game.iflags?.wc_color !== false
&& (styleCell.ch === '·' || styleCell.ch === '~')
&& !cansee(mx, my)) {
styleCell = { ...styleCell, color: CLR_BLACK };
}
if (game._reveal_terrain_active
&& mapLoc?.typ === ROOM
&& (styleCell.ch === '·' || styleCell.ch === '~')
&& styleCell.color === CLR_BLACK) {
styleCell = { ...styleCell, color: defsyms[S_room]?.color ?? NO_COLOR };
}
if (game.iflags?.terrainmode && mapLoc) {
// Keep S_darkroom tint (black/dark-gray) C-faithful in terrainmode.
// Overriding it to S_room color causes visible color drift.
if (mapLoc.glyph === _cmap_to_glyph_int(S_litcorr)) {
styleCell = { ...renderCell, color: defsyms[S_corr]?.color ?? NO_COLOR };
}
}
updateStyle(styleCell);
let outCh = renderCell.ch;
if (rogueMapRow) {
const rogueAscii = {
'─': '-',
'│': '|',
'┌': '-', '┐': '-', '└': '-', '┘': '-',
'┼': '-', '┬': '-', '┴': '-', '├': '-', '┤': '-',
'~': '+',
};
outCh = rogueAscii[outCh] || outCh;
if (outCh === '·') {
outCh = (mapLoc?.typ === DOOR || mapLoc?.typ === SDOOR) ? '+' : '.';
} else if (outCh === ' ' && (mapLoc?.typ === DOOR || mapLoc?.typ === SDOOR)) {
outCh = '+';
}
}
const decChar = UNICODE_TO_DEC[outCh];
const wantsDecGraphics = !rogueMapRow && (!!styleCell.decgfx || !!decChar);
if (!wantsDecGraphics
&& outCh === ' '
&& sawDecOutput
&& justResetToDefaultStyle
&& shouldEmitNoopShiftBeforeSpace(c, 1)) {
output += '\x0e\x0f';
justResetToDefaultStyle = false;
}
if (wantsDecGraphics && !activeDecGraphics) {
output += '\x0e';
activeDecGraphics = true;
} else if (!wantsDecGraphics && activeDecGraphics) {
output += '\x0f';
activeDecGraphics = false;
}
if (wantsDecGraphics) sawDecOutput = true;
output += decChar || outCh;
}
if (activeColor !== CLR_GRAY || activeInverse || activeBold || activeUnderline) {
if (activeInverse || activeBold || activeUnderline) {
output += '\x1b[0m';
} else {
output += '\x1b[39m';
}
}
if (activeDecGraphics) output += '\x0f';
return output;
}
export function serialize_terminal_grid(display) {
let output = '';
let lastRow = 0;
let prevNonEmptyRow = '';
let prevNonEmptyIndex = -1;
for (let r = 0; r < display.rows; r++) {
for (let c = 0; c < display.cols; c++) {
if (display.grid[r][c].ch !== ' ') { lastRow = r; break; }
}
}
for (let r = 0; r <= lastRow; r++) {
let rowOutput = serialize_terminal_row(display.grid[r], r);
output += rowOutput;
if (rowOutput) {
prevNonEmptyRow = rowOutput;
prevNonEmptyIndex = r;
}
if (r < lastRow) output += '\n';
}
return output;
}
// newsym is implemented above (display.c:918 port)
// Paint any pending gnew entries to the terminal grid.
// This is the core of C's flush_screen glyph loop, extracted so it can
// also be called from the capture hook to ensure the grid is current.
// Pure output — no game state mutation, no events.
export function flush_glyph_buf() {
const g = game;
const display = g?.nhDisplay;
if (!display?.grid || !g._gbuf_start || !g.level) return;
// Normal gnew pass: paint dirty cells from the gbuf bbox.
for (let y = 0; y < ROWNO; y++) {
const start = g._gbuf_start[y];
const stop = g._gbuf_stop[y];
if (start > stop) continue;
const gbufRow = g.gbuf?.rows?.[y];
for (let x = start; x <= stop; x++) {
const loc = g.level.at(x, y);
if (!loc || !loc.gnew) continue;
const row = y + 1;
const col = x - 1;
const cell = gbufRow?.[x];
if (row >= 0 && row < display.rows && col >= 0 && col < display.cols) {
let gridCh = cell?.ch || ' ';
if (cell?.decgfx && DEC_TO_UNICODE[gridCh]) {
gridCh = DEC_TO_UNICODE[gridCh];
}
display.setCell(col, row, gridCh, cell?.color ?? NO_COLOR, cell?.attr ?? 0, !!cell?.decgfx);
}
loc.gnew = 0;
}
}
reset_glyph_bbox();
// C's terminal accumulates painted cells — once a cell is written,
// it stays until explicitly overwritten. JS's grid loses cells on
// clearScreen(). After a clear, explored cells that were painted by
// earlier flush_glyph_buf calls (gnew already consumed) are gone.
// Repaint any remembered_glyph cell that's missing from the grid.
// This is a no-op when the grid already matches remembered state.
// After clearScreen, repaint explored cells that are missing from the
// grid. Skip cells that already have non-space content (from the gnew
// pass above — including the hero, monsters, and currently-visible
// terrain). Only fill in cells that the grid shows as space but
// remembered_glyph says should show terrain.
if (g._gridCleared && display.grid) {
// gbuf-cleanup Phase 6b: derive from loc.glyph via map_glyphinfo.
for (let y = 0; y < ROWNO; y++) {
for (let x = 1; x < COLNO; x++) {
const loc = g.level.at(x, y);
if (!loc?.glyph) continue;
const gi = map_glyphinfo(loc.glyph, loc);
if (!gi || gi.ch === ' ') continue;
const row = y + 1, col = x - 1;
if (row >= display.rows || col >= display.cols) continue;
const cell = display.grid[row]?.[col];
if (!cell || cell.ch !== ' ') continue; // already has content
let gridCh = gi.ch;
if (gi.decgfx && DEC_TO_UNICODE[gridCh])
gridCh = DEC_TO_UNICODE[gridCh];
display.setCell(col, row, gridCh, gi.color ?? NO_COLOR, 0, !!gi.decgfx);
}
}
}
}
// C ref: display.c:2120 reset_glyph_bbox — reset dirty region to empty.
function reset_glyph_bbox() {
const g = game;
if (!g._gbuf_start) return;
for (let i = 0; i < ROWNO; i++) {
g._gbuf_start[i] = COLNO - 1;
g._gbuf_stop[i] = 0;
}
}
// ── flush_screen ──
export async function flush_screen(mode) {
// C ref: display.c:2250-2317 flush_screen()
// C ref: display.c:2273-2276 — flush_screen(-1) toggles delay
if (mode === -1) {
game._delayFlushing = game._delayFlushing ? 0 : 1;
}
if (game._delayFlushing) return;
// C ref: display.c:2286-2289 — bot() runs FIRST
if (game.disp.botl || game.disp.botlx) {
await bot();
} else if (game.disp.time_botl) {
await timebot();
}
// C ref: display.c:2291-2308 — paint dirty glyphs to terminal
flush_glyph_buf();
// C ref: display.c:2312-2313 — cursor at hero if mode is truthy
if (mode && game.u?.ux) {
const display = game?.nhDisplay;
if (display) display.setCursor(game.u.ux - 1, game.u.uy + 1);
}
}
// C ref: termcap.c tty_delay_output() — called during animations
// (zap beams, thrown objects, explosions) to pause between frames.
// In JS: emit an ^anim_frame[id=N] event for parity tracking (id matches
// C's nomux_anim_id for cross-referencing the animation_frames appendix)
// and yield control to allow browser rendering. Gameplay screen parity
// compares input-boundary frames, not intermediate animation frames.
export async function delay_output() {
pushAnimFrameEvent();
// In browser: yield to allow rendering of the intermediate frame.
// In headless/test: no-op (immediate return).
if (typeof requestAnimationFrame === 'function') {
await new Promise(resolve => requestAnimationFrame(resolve));
}
}
// ── vision ──
// vision_recalc: canonical in vision.js (C: vision.c)
// Imported + re-exported for backward compatibility + default export.
import { vision_recalc } from './vision.js';
export { vision_recalc };
// bot / timebot canonical in botl.js (re-exported below).
// ── curs_on_u ──
// C ref: display.c curs_on_u() → flush_screen(1)
export async function curs_on_u() {
await flush_screen(1);
// C ref: display.c curs_on_u() → tty_curs(WIN_MAP, u.ux, u.uy)
// tty_curs: curx = --x + offx, cury = y + offy. MAP: offx=0, offy=1.
const display = game?.nhDisplay;
if (display && game.u?.ux) {
display.setCursor(game.u.ux - 1, game.u.uy + 1);
}
}
// ── pline: message display ──
// C ref: pline.c → putmesg → tty_putstr → update_topl (topl.c:250)
//
// C's update_topl concatenation logic:
// If toplin == NEED_MORE and new msg fits on same line:
// concatenate with " " separator (no --More--)
// If toplin == NEED_MORE and new msg does NOT fit:
// show --More-- (inline, consuming recorded keys), then replace
// Otherwise (toplin is EMPTY or NON_EMPTY):
// replace the old message, set toplin = NEED_MORE
//
// C's threshold: n0 + strlen(toplines) + 3 < CO - 8 (= 72 for CO=80)
// nhgetch silently transitions toplin NEED_MORE → NON_EMPTY (wintty.c:4098)
// pline, You, Your, You_feel, You_see, You_hear, You_cant, pline_The,
// verbalize — moved to pline.js (canonical: pline.c / topl.c).
// Import from './pline.js' for these functions.
// ── see_* stubs ──
// see_monsters: canonical in mon.js (C: display.c but ported in mon.js)
import { see_monsters } from './mon.js';
import { mdistu, mon_warning, monsndx, warning_of } from './mondata.js';
import { impossible } from './pline.js';
export { see_monsters };
export function see_objects() {}
export function see_traps() {}
// C ref: display.c:1537 set_mimic_blocking()
// Iterates all monsters checking if they block light as mimics.
// No-op until mimic system is fully ported.
export function set_mimic_blocking() {}
// ── shieldeff: shield animation effect ──
// C ref: display.c — visual feedback, no RNG
export function shieldeff(x, y) {
// C ref: display.c:1144-1157
// We don't render shield glyphs yet, but we keep the same animation
// boundary cadence for event parity.
if (game.flags?.sparkle === false) return;
if (!cansee(x, y)) return;
for (let i = 0; i < 21; i++) {
pushAnimFrameEvent();
}
newsym(x, y);
}
// ── unblock_point: mark point as unblocked for vision ──
// C ref: display.c — vision, no RNG
export function unblock_point(x, y) {
// C ref: vision.c:888 unblock_point + vision.c:894-895.
// Rebuild blocker metadata and request full vision recalc when the changed
// tile is currently in viz_array (same trigger C uses).
const wasVisible = !!(game.viz_array?.[y]?.[x]);
if (wasVisible) game.vision_full_recalc = 1;
vision_reset();
}
// ── recalc_block_point: recalculate vision blocking at point ──
// C ref: display.c — vision, no RNG
export function recalc_block_point(x, y) {
// C ref: vision.c:900-906 recalc_block_point + block/unblock side effects.
// JS currently rebuilds blocker metadata globally; preserve C-visible
// scheduling by setting vision_full_recalc when this tile was in viz_array.
const wasVisible = !!(game.viz_array?.[y]?.[x]);
if (wasVisible) game.vision_full_recalc = 1;
vision_reset();
}
// ── Monster visibility predicates ──
// C ref: display.h macros, display.c function versions
// mdistu: imported from mondata.js
// C ref: display.h — can hero sense monster via telepathy?
export function tp_sensemon(mon) {
if (!mon?.data) return false;
// mindless check: M1_MINDLESS
if (mon.data.mflags1 & 0x00010000) return false; // M1_MINDLESS
return (Blind() && Blind_telepat())
|| (Unblind_telepat()
&& mdistu(mon) <= (game.u.unblind_telepat_range || 0));
}
// C ref: display.h — can hero sense monster? (telepathy/detection/warning)
export function sensemon(mon) {
if (!mon) return false;
if (game.u?.uswallow && mon !== game.u?.ustuck) return false;
if (Underwater()) {
if (mdistu(mon) > 2) return false;
}
return Detect_monsters() || tp_sensemon(mon)
|| MATCH_WARN_OF_MON(mon);
}
// C ref: display.h — is monster visible to hero? (not hidden/invisible)
export function mon_visible(mon) {
if (!mon) return false;
return (!mon.minvis || See_invisible())
&& !mon.mundetected;
}
// C ref: display.h — see monster with infravision?
export function see_with_infrared(mon) {
if (!mon?.data) return false;
if (Blind()) return false;
if (!Infravision()) return false;
// infravisible: warm-blooded (not undead, golem, etc.)
const infravisible = !!(mon.data.mflags3 & 0x0200); // M3_INFRAVISIBLE
return infravisible && couldsee(mon.mx, mon.my);
}
// C ref: display.h — can hero see the monster?
export function canseemon(mon) {
if (!mon) return false;
// worm check simplified (no worm_known)
const canSeeSpot = cansee(mon.mx, mon.my) || see_with_infrared(mon);
return canSeeSpot && mon_visible(mon);
}
// C ref: display.h — can hero spot the monster? (see or sense)
export function canspotmon(mon) {
return canseemon(mon) || sensemon(mon);
}
// mon_warning: imported from mondata.js
// warning_of: imported from mondata.js
function display_warning_cell(mon) {
const wl = Hallucination()
? (rn2_on_display_rng(WARNCOUNT - 1) + 1)
: warning_of(mon);
const sym = def_warnsyms[wl] || def_warnsyms[0];
show_glyph_cell(mon.mx, mon.my, sym.ch, sym.color, false, 0, _warning_to_glyph_int(wl));
}
function is_warning_display_char(ch) {
if (!ch) return false;
for (let i = 1; i < WARNCOUNT; i++) {
if (def_warnsyms[i]?.ch === ch) return true;
}
return false;
}
// C ref: display.h — MATCH_WARN_OF_MON (warning match)
function MATCH_WARN_OF_MON(_mon) {
// TODO: implement warn_of_mon matching
return false;
}
export default {
newsym, flush_screen, docrt, vision_recalc, vision_reset,
bot, curs_on_u,
see_monsters, see_objects, see_traps,
shieldeff, unblock_point, recalc_block_point,
mdistu, tp_sensemon, sensemon, mon_visible,
see_with_infrared, canseemon, canspotmon,
};
// Canonical stubs — duplicated in 10+ files
// C ref: display.c:730 map_invisible — show 'I' at location where
// an invisible monster is known to be.
export function map_invisible(x, y) {
if (x === game.u?.ux && y === game.u?.uy) return; // don't display at hero
const loc = game.level?.at?.(x, y);
if (loc && game.level?.flags?.hero_memory) {
// C ref: display.c map_invisible() stores GLYPH_INVISIBLE in hero memory
// so redraws keep showing 'I' until unmap_object()+newsym clears it.
loc.glyph = GLYPH_INVISIBLE;
}
// C: show_glyph(x, y, GLYPH_INVISIBLE) renders 'I' via mapglyph.
show_glyph_cell(x, y, 'I', CLR_GRAY, false, 0, GLYPH_INVISIBLE);
}
export function seemimic(mon) { if (mon) mon.m_ap_type = 0; }
// C: tmp_at(cmd, val) — display temporary effects (beam, flash, etc.)
const TMP_AT_MAX_GLYPHS = COLNO * 2;
const tmpGlyphStack = [];
export async function tmp_at(x, y) {
switch (x) {
case DISP_BEAM:
case DISP_ALL:
case DISP_TETHER:
case DISP_FLASH:
case DISP_ALWAYS:
tmpGlyphStack.push({
saved: [],
style: x,
glyph: y,
});
await flush_screen(0);
return;
case DISP_FREEMEM:
tmpGlyphStack.length = 0;
return;
default:
break;
}
const tglyph = tmpGlyphStack[tmpGlyphStack.length - 1];
if (!tglyph) return;
if (x === DISP_CHANGE) {
tglyph.glyph = y;
return;
}
if (x === DISP_END) {
if (tglyph.style === DISP_BEAM || tglyph.style === DISP_ALL || tglyph.style === DISP_TETHER) {
if (tglyph.style === DISP_TETHER && y === BACKTRACK && tglyph.saved.length > 1) {
for (let i = tglyph.saved.length - 1; i > 0; i--) {
newsym(tglyph.saved[i].x, tglyph.saved[i].y);
show_glyph(tglyph.saved[i - 1].x, tglyph.saved[i - 1].y, tglyph.glyph);
await flush_screen(0);
}
}
for (const pos of tglyph.saved) {
newsym(pos.x, pos.y);
}
} else if (tglyph.saved.length > 0) {
newsym(tglyph.saved[0].x, tglyph.saved[0].y);
}
tmpGlyphStack.pop();
return;
}
if (!isok(x, y)) return;
if (tglyph.style === DISP_BEAM || tglyph.style === DISP_ALL) {
if (tglyph.style !== DISP_ALL && !cansee(x, y))
return;
if (tglyph.saved.length >= TMP_AT_MAX_GLYPHS)
return;
tglyph.saved.push({ x, y });
} else if (tglyph.style === DISP_TETHER) {
if (tglyph.saved.length >= TMP_AT_MAX_GLYPHS)
return;
if (tglyph.saved.length > 0) {
const prev = tglyph.saved[tglyph.saved.length - 1];
show_glyph(prev.x, prev.y, tglyph.glyph);
}
tglyph.saved.push({ x, y });
} else {
if (tglyph.saved.length > 0) {
newsym(tglyph.saved[0].x, tglyph.saved[0].y);
tglyph.saved.length = 0;
}
if (!cansee(x, y) && tglyph.style !== DISP_ALWAYS)
return;
tglyph.saved.push({ x, y });
}
show_glyph(x, y, tglyph.glyph);
await flush_screen(0);
}
// C: show_glyph(x, y, glyph) — paint a specific canonical int glyph at a
// map location. Mirrors C's show_glyph (display.c:1866): the glyph is the
// same encoding C writes to gg.gbuf[y][x].glyphinfo.glyph.
//
// Two object forms are also accepted as host-language conveniences:
// * {ch, color, decgfx} — literal paint (tmp_obj_glyph output).
// * {sym: int, color: int} — canonical int with a color override
// (zapdir_to_glyph; the override wins
// over map_glyphinfo's decoded color).
export function show_glyph(x, y, glyph) {
if (!isok(x, y)) return;
if (glyph && typeof glyph === 'object' && typeof glyph.ch === 'string') {
const ch = glyph.ch.length ? glyph.ch[0] : ' ';
show_glyph_cell(x, y, ch, glyph.color ?? NO_COLOR, !!glyph.decgfx);
return;
}
let g = glyph;
let colorOverride = null;
if (glyph && typeof glyph === 'object') {
g = glyph.sym;
colorOverride = glyph.color;
}
if (!Number.isInteger(g) || g < 0) return;
const loc = game.level?.at(x, y) ?? null;
const gi = map_glyphinfo(g, loc);
const color = (colorOverride != null) ? colorOverride : gi.color;
show_glyph_cell(x, y, gi.ch, color, gi.decgfx, 0, g);
}
// C ref: display.c:2487 swallow_to_glyph — encodes (mnum, location) into a
// canonical glyph int. Decoded by map_glyphinfo's GLYPH_SWALLOW_OFF branch.
function swallow_to_glyph(mnum, loc) {
if (loc < S_sw_tl || loc > S_sw_br) {
impossible('swallow_to_glyph: bad swallow location');
loc = S_sw_br;
}
return ((mnum << 3) | (loc - S_sw_tl)) + GLYPH_SWALLOW_OFF;
}
// C ref: display.c:1366 swallowed
export async function swallowed(first) {
const g = game;
const u = g.u;
if (first) {
await cls();
await bot();
} else {
const lastx = g.display_swallow_lastx ?? 0;
const lasty = g.display_swallow_lasty ?? 0;
/* Clear old location */
for (let y = lasty - 1; y <= lasty + 1; y++) {
for (let x = lastx - 1; x <= lastx + 1; x++) {
if (isok(x, y)) {
show_glyph(x, y, GLYPH_UNEXPLORED);
}
}
}
}
const swallower = monsndx(u.ustuck.data);
const left_ok = isok(u.ux - 1, u.uy);
const rght_ok = isok(u.ux + 1, u.uy);
/*
* Display the hero surrounded by the monster's stomach.
*/
if (isok(u.ux, u.uy - 1)) {
if (left_ok) show_glyph(u.ux - 1, u.uy - 1, swallow_to_glyph(swallower, S_sw_tl));
show_glyph(u.ux, u.uy - 1, swallow_to_glyph(swallower, S_sw_tc));
if (rght_ok) show_glyph(u.ux + 1, u.uy - 1, swallow_to_glyph(swallower, S_sw_tr));
}
if (left_ok) show_glyph(u.ux - 1, u.uy, swallow_to_glyph(swallower, S_sw_ml));
// Middle center is the hero (actually the swallower's depiction).
{
const mon = u.ustuck;
const monForGlyph = { mnum: swallower, female: mon?.female ?? false };
const g = _mon_to_glyph_full(monForGlyph, rn2_on_display_rng, Hallucination());
show_glyph(u.ux, u.uy, g);
}
if (rght_ok) show_glyph(u.ux + 1, u.uy, swallow_to_glyph(swallower, S_sw_mr));
if (isok(u.ux, u.uy + 1)) {
if (left_ok) show_glyph(u.ux - 1, u.uy + 1, swallow_to_glyph(swallower, S_sw_bl));
show_glyph(u.ux, u.uy + 1, swallow_to_glyph(swallower, S_sw_bc));
if (rght_ok) show_glyph(u.ux + 1, u.uy + 1, swallow_to_glyph(swallower, S_sw_br));
}
/* Update the swallowed position. */
g.display_swallow_lastx = u.ux;
g.display_swallow_lasty = u.uy;
}
// C: block_point(x, y) — check if location blocks vision
export function block_point(x, y) {
// C ref: vision.c:872-879 block_point.
// Rebuild blocker metadata and request full vision recalc when the changed
// tile is currently in viz_array.
const wasVisible = !!(game.viz_array?.[y]?.[x]);
if (wasVisible) game.vision_full_recalc = 1;
vision_reset();
}
// C display.h:616 cmap_to_glyph(S_*) — canonical int glyph for a cmap
// symbol. Re-exported from glyph.js. Every show_glyph / tmp_at /
// flash_glyph_at caller now passes the canonical int, decoded by
// map_glyphinfo at paint time.
export { cmap_to_glyph } from './glyph.js';
// C: feel_newsym(x, y) — update map after feeling a location
export function feel_newsym(x, y) {
// C ref: display.c feel_newsym(): blind uses tactile remap,
// otherwise refresh through normal vision-aware newsym path.
if (Blind()) {
feel_location(x, y);
} else {
newsym(x, y);
}
}
// C: glyph_is_monster(glyph) — check if glyph represents a monster
export function glyph_is_monster(glyph) {
// TODO: port from display.c
return false;
}
// C: glyph_to_mon(glyph) — convert glyph to monster index
export function glyph_to_mon(glyph) {
// TODO: port from display.c
return 0;
}
// C: does_block(x, y, lev) — check if location blocks vision
export function does_block(x, y, lev) {
// TODO: port from display.c
return false;
}
export function init_display_globals() {
game.curColor = 39;
game.wins = new Array(MAXWIN).fill(null);
game.WIN_MESSAGE = -1;
game.WIN_STATUS = -1;
game.WIN_MAP = -1;
game.WIN_BASE = -1;
game.WIN_INVEN = -1;
game.erasing_tty_screen = 0;
game.getposx = 0;
game.getposy = 0;
}
|