All files / js trap.js

55.01% Statements 1854/3370
53.35% Branches 366/686
62.5% Functions 70/112
55.01% Lines 1854/3370

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 3263 3264 3265 3266 3267 3268 3269 3270 3271 3272 3273 3274 3275 3276 3277 3278 3279 3280 3281 3282 3283 3284 3285 3286 3287 3288 3289 3290 3291 3292 3293 3294 3295 3296 3297 3298 3299 3300 3301 3302 3303 3304 3305 3306 3307 3308 3309 3310 3311 3312 3313 3314 3315 3316 3317 3318 3319 3320 3321 3322 3323 3324 3325 3326 3327 3328 3329 3330 3331 3332 3333 3334 3335 3336 3337 3338 3339 3340 3341 3342 3343 3344 3345 3346 3347 3348 3349 3350 3351 3352 3353 3354 3355 3356 3357 3358 3359 3360 3361 3362 3363 3364 3365 3366 3367 3368 3369 3370 337173x 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 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 2x 2x 2x 73x 73x 2x 2x 2x     2x   2x 1x 1x 1x 1x                             1x 1x 1x 2x 73x 73x                                                                                                       4x 4x 4x 4x 4x 4x 4x 4x 4x 3x   3x         3x 3x 3x 3x 4x 1x             1x 1x 1x                               4x 4x           4x 4x 4x 4x 4x 1x 1x 1x 1x 1x 1x 1x 1x       1x 1x     1x 1x 1x 1x       1x 1x 1x 1x 1x 1x 1x 1x         1x 1x 1x 73x 73x 73x 73x 73x 73x 73x   73x 73x 3x 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 13x 13x 13x 13x 13x 13x 13x 13x 13x 5x 5x 5x 5x 5x 5x 3x 3x 3x 13x 2x 2x 13x         13x       13x       13x   13x 13x 13x 13x 5x 13x     9x 1x 5x 3x 3x 3x 4x               1x 1x 1x 1x 1x 1x 1x     1x 1x 1x                   13x 73x 73x 73x 73x 73x                                   73x 73x 73x 73x 73x 3x 3x 3x 3x 3x 3x 3x 3x 12x 12x 2x 2x     12x 3x 3x 3x 3x 3x 3x 1x 1x 12x 1x 1x     12x         12x 6x 6x     12x       3x 73x 73x 73x 4x 4x 4x 4x 4x 73x 73x 73x 1x 1x 1x 1x   1x 1x                                               1x 73x 73x 73x 73x 1232x 1232x 1232x 1232x 1232x 960x 960x 1232x 407x 407x 393x 1232x       1232x 2x 2x   1232x 349x 349x 219x 1232x 1232x 1232x 1232x 47x 47x 47x 1232x 10x 10x 2x 1232x   1232x 43x 1232x 2x 2x 2x 1232x   1232x 100x 1232x 763x 1232x 73x 73x 73x 73x 13x 13x 13x 13x 13x 13x 4x 4x     13x 13x 2x 2x 13x 11x 11x 11x 11x 11x 11x 11x 9x 9x 2x 2x 2x 11x 11x 11x 3x 3x 3x 3x 3x 3x 3x 11x     11x 13x 3x 3x 13x 2x 2x 13x 13x 73x 73x 73x 73x 7x 7x 7x 7x         7x 7x 73x 73x 73x 73x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 73x 73x 73x 73x 73x 73x 1x 1x 1x 1x 1x 1x 1x 1x 1x                   1x                                                   1x                                                                                                                                                                                                     1x 73x 73x 73x 73x 73x                                                         73x 73x 73x 73x 26x 26x 26x 73x 73x 73x 73x 23x 23x 73x 73x 73x 73x 3x 3x                             3x   3x         3x   3x 3x 3x 3x 3x 3x 3x 3x 73x 73x 73x 73x 73x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 2x 2x         2x 4x 4x 4x 4x 4x 4x   4x 4x 4x 4x 4x 4x 4x 4x 4x 4x                             4x 4x 4x 4x 4x 4x 4x 4x 73x 73x 73x 73x 73x 10x 10x 10x 10x 10x         10x     10x         10x 3x 3x 3x 3x 3x 3x 3x 10x 7x   7x 7x 7x 7x 7x 7x 10x 73x 73x 73x 95x 95x 95x 95x 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 2x 73x 73x 73x 15x 15x 15x 15x 73x 73x 1115x 1115x 1115x 73x 73x 73x 73x     73x 73x 73x 73x 73x                                         73x 73x 73x 73x                     73x 73x 73x 73x 73x 4x 4x 4x 4x 4x 4x 4x 4x 4x     4x 4x       4x                             4x 4x   4x               4x 4x       4x 4x     4x 4x                 4x 4x                     4x 4x 2x       2x                   2x 1x 1x 1x 1x 1x 1x 1x 2x 2x 4x 73x 73x 73x 73x 1x 1x         1x 73x 73x 73x 73x 73x                                                                                                                                                       73x 73x 73x 73x 1x     1x 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 1x 1x 1x 1x 1x                     1x 1x 1x 1x 1x 73x 73x 73x 73x     73x 73x 73x 73x     73x 73x 21x 21x   73x 73x 73x 73x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x     18x 18x 73x 73x 73x 73x                           73x 73x 73x 73x 73x             73x 73x 73x 9x 9x 9x 9x 9x 9x 9x 9x 9x 73x 73x 73x 73x 73x 53x 53x 53x 53x 53x 53x 53x 53x 53x 73x 73x 73x 46x 46x 46x 46x 5x 5x 5x 46x 73x 73x 73x 73x 73x 73x 73x 73x 22x 6x 6x 6x 22x 73x 73x 8x 8x 8x 8x 73x 73x 73x 1360x 1360x 1360x 1360x 1360x 1360x 1237x 1360x 123x 1360x 1360x 73x 73x 73x 16x 16x 73x 73x 73x 73x 73x 19x 19x 19x 19x 19x 19x 19x 19x 19x 19x 19x 19x 19x 19x 19x 19x 19x 19x   19x 19x           19x 19x 19x 19x       19x 19x 19x 19x 19x 19x 19x 19x 19x 19x 19x 19x 19x 19x 19x 19x 19x 73x 73x 19x 19x 19x 19x 4x 4x 19x     19x 2x 2x 19x 1x 1x 19x 2x 2x 19x 1x 1x 19x     19x 19x 2x 2x 19x 19x 2x 2x 19x 2x 2x 19x     19x 1x 1x 19x     19x     19x     19x     19x 1x 1x 19x     19x 1x 1x 19x     19x     19x 19x 73x 73x 73x 73x 73x 14489x 14489x 14489x 14489x 14489x 14489x 14191x 14489x 182x 182x 182x 182x     182x 182x 1x             1x 1x           1x 1x 182x                           182x 298x 116x 116x 116x 116x 116x 116x 116x 116x 116x 116x 115x 52x 52x 115x 115x 115x 43x 43x 43x 43x 43x 116x 116x 43x 43x 43x 43x 116x 7x 7x 116x 14416x 14489x 73x 73x 43x 43x 43x 43x 43x 43x 43x 43x 4x 4x             4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 43x 1x             1x 1x 1x 1x 1x 1x 1x 1x 43x 4x 4x 1x 1x 1x 1x     4x 3x 3x 3x 3x 4x 4x 43x 43x 1x 1x 1x 1x 1x 1x 1x 1x       1x 1x 1x 1x 1x 1x 1x 1x 43x                 43x 43x                                     43x 16x 16x 16x 15x       16x 1x 1x 1x 1x 1x 1x 1x 1x 1x     1x 1x 1x 1x 1x 16x 16x 16x 16x 43x 43x 7x 7x 7x 7x 7x 7x     7x 7x 2x 2x 2x 2x 2x 7x 7x 7x 7x 7x 7x 7x 43x 43x 3x 3x 3x 3x             3x 3x 43x     43x 43x   43x 43x 1x 43x 43x                                                               43x   43x 43x 3x 3x 3x 43x 43x 1x 1x 1x 1x       1x 1x                     1x 1x 1x 43x                     43x 1x 1x 1x 1x         1x 1x 1x 1x 1x 1x 1x 1x   1x 1x 1x 1x 1x 1x 1x 1x 1x 43x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x       1x 1x 1x 43x 43x                   43x 43x   43x 43x 73x 73x 73x 73x 73x 73x 73x 73x 73x 4x 4x 4x 4x                   4x 4x 4x 1x 3x 3x 3x 4x 4x 4x 4x 4x     4x 4x 4x 4x 4x 4x 4x 4x 1x 1x         1x 1x 4x 3x 3x 3x 3x 3x 4x 73x 73x 73x 73x                                       73x 73x 73x 73x 2x 2x 2x 2x               2x 2x 2x 2x 2x 2x 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 2x 2x           2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 73x 73x 73x 73x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x       1x       1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 73x 73x 73x 73x 2x 2x 2x   2x 2x 2x 2x 2x 2x 73x 73x 73x 73x 1x 1x 1x 1x 1x       1x         1x         1x 1x 1x 1x         1x 1x 1x 1x       1x     1x 73x 73x 73x         73x 73x 73x                                                                                         73x 73x 73x 2x 2x 2x 2x 73x 73x 73x 2x 2x 2x 2x 73x 73x 73x         73x 73x 73x 73x 1x 1x 1x 1x 1x 1x 1x 1x 1x           1x 1x   1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x                     1x 1x 73x 73x 73x       73x 73x 73x 73x                                 73x 73x 73x                                                                                                                                                                                         73x 73x 73x 73x                                                 73x 73x 73x 73x                             73x 73x 73x 73x 73x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x               1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 73x 73x 73x 73x                       73x 73x 73x 1x 1x 1x 1x 73x 73x 73x         73x 73x 73x 73x               73x 73x 73x 73x           73x 73x 73x     73x 73x 73x 73x 73x 73x 848x 848x 73x 73x 73x 73x 73x 73x 23x 23x 23x 73x 73x 73x 73x 20x 20x 20x 73x 73x 73x 73x                   73x 73x 73x 73x 73x 73x               73x 126x 126x 126x 126x 126x 126x 126x 126x 142x 142x 142x 142x 126x 73x 597x 597x 597x 597x 73x 1998x 1998x 1998x 1998x 1998x 1998x 1998x 73x 1147x 1147x 1147x 1147x 2917x 2917x 2917x 2917x 2917x 2917x 2917x     2917x 220x 220x 220x 1147x 73x 73x 27x 27x 27x 27x 27x 27x 27x 27x 27x 27x 27x 597x 597x 597x 597x 597x 597x 597x 47x 597x 550x 550x 597x 597x 597x 597x 597x 597x 596x 596x 597x 7x 27x 73x 73x 27x 27x 27x 27x 27x 7x 7x 7x 25x 20x 20x 20x 20x 20x 20x 20x 20x 27x 27x 27x 27x 27x     27x 27x 27x 73x 73x 73x 73x 73x 73x 73x 73x 6x 6x 6x 6x 6x 6x 6x 6x 6x 73x 85x 85x 85x 85x 395x 395x 395x 85x 1020x 1020x 85x 85x 73x 73x 21x 21x 21x 21x 22x 21x 21x 21x 21x 21x 21x 21x 21x 23x 23x 23x 23x 23x 21x 21x 21x 21x 21x 21x 21x 21x 21x                 21x 21x 21x 21x 21x 21x 73x 73x 73x 73x 1237x 1237x 18x 18x 1219x 1219x 1237x 1219x 1219x 1219x 1219x 1237x 6x 6x               1237x 1213x 1213x 1213x 1213x 1213x 1213x 1213x 1213x 1213x 1213x 11x 11x 1202x 1202x 1202x 1202x 1202x 1208x 1208x 1208x 1208x 1208x 1208x 1208x 1208x 1208x 1208x 1237x 85x 85x 1237x 21x 21x 1237x 27x 27x 1208x 1237x 1237x 1208x 1237x 1202x 1202x 1237x 1208x 1237x 73x 73x 73x 73x 10x 10x 10x 10x 10x 10x 10x 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 4x 4x 4x 4x 4x   4x 4x  
// trap.js — Trap effects (port of trap.c)
// C ref: trap.c — 7186 lines
//
// Key RNG consumers:
//   dotrap() — rn2(5) escape check, then trap-specific RNG
//   mintrap() — rn2(40) escape, per-type monster dispatch
//   domagictrap() — rnd(20) fate dispatch with 20 cases
//   trapeffect_pit() — rn1(6,2) for duration, rnd for damage
//   trapeffect_bear_trap() — rn1(4,4) for duration, d(2,4) damage
//   trapeffect_magic_trap() — rn2(30) explosion check
//   trapeffect_landmine() — rnd(16) damage, rn1(35,41) leg wounds
//   trapeffect_anti_magic() — d(2,6) drain, rnd(drain/2) split
 
import { game } from './gstate.js';
import { trap_to_glyph } from './glyph.js';
import { destroy_items, erosion_matters, ignite_items, melt_ice } from './mkobj.js';
import { rn2, rnd, rn1, c_d, rnl, pushRngLogEntry } from './rng.js';
import { pline, pline_mon, pline_The, urgent_pline, You, You_feel, You_hear, You_see, Your } from './pline.js';
import { Acid_resistance, Antimagic, Blind, Cold_resistance, Deaf, Drain_resistance, Fire_resistance, Flying, Fumbling, Half_physical_damage, Half_spell_damage, Hallucination, has_ceiling, HStun, In_endgame, In_quest, Inhell, Invis, Is_airlevel, Is_waterlevel, Levitation, Maybe_Half_Phys, Passes_walls, Poison_resistance, Punished, Role_if, See_invisible, Shock_resistance, Sleep_resistance, Stone_resistance, Stunned, u_locomotion, Unchanging, Underwater, Upolyd } from './macros.js';
import { getdir, nomul } from './cmd.js';
import { levl, t_at, m_at } from './map_access.js';
import { canseemon, canspotmon, map_invisible, map_trap, newsym, seemimic, setBotl, shieldeff, unblock_point } from './display.js';
import { pchar_sym } from './drawing.js';
import { defsyms, S_arrow_trap } from './symbols.js';
import { burn_away_slime, can_reach_floor, crawl_destination, done, helpless, losehp, monkilled, spoteffects } from './hack.js';
import { stumble_onto_mimic } from './uhitm.js';
import { dmgval } from './weapon.js';
import { AD_ACID, AD_ELEC, AD_FIRE, AD_MAGM, AD_PHYS, AD_RBRE, AD_RUST, AD_SLEE, mons, MZ_HUGE, MZ_SMALL, PM_BALROG, PM_BALUCHITHERIUM, PM_BUGBEAR, PM_CYCLOPS, PM_FLESH_GOLEM, PM_FOG_CLOUD, PM_GELATINOUS_CUBE, PM_GREMLIN, PM_IRON_GOLEM, PM_JABBERWOCK, PM_KRAKEN, PM_LEATHER_GOLEM, PM_LORD_SURTUR, PM_MASTODON, PM_NORN, PM_ORION, PM_OWLBEAR, PM_PAPER_GOLEM, PM_PIT_FIEND, PM_PIT_VIPER, PM_PURPLE_WORM, PM_RANGER, PM_ROGUE, PM_STEAM_VORTEX, PM_STRAW_GOLEM, PM_TITANOTHERE, PM_WATER_ELEMENTAL, PM_WOOD_GOLEM, S_DRAGON, S_GIANT, S_SPIDER } from './monsters.js';
import { A_CHA, A_CON, A_DEX, A_STR, A_WIS, ANTI_MAGIC, ARM, ARROW_TRAP, BEAR_TRAP, BOLT_LIM, CORPSTAT_NONE, COST_BURN, COST_CORRODE, COST_CRACK, COST_ROT, COST_RUST, D_CLOSED, D_LOCKED, D_NODOOR, D_TRAPPED, DART_TRAP, DOOR, DRAWBRIDGE_UP, ECMD_OK, FINGER, FIRE_TRAP, FLYING, FOOT, HAND, HEAD, HOLE, HVY_ENCUMBER, I_SPECIAL, IS_AIR, IS_DOOR, IS_FURNITURE, is_hole, is_pit, is_xport, isok, Is_botlevel, Is_stronghold, KILLED_BY, KILLED_BY_AN, LADDER, LANDMINE, LAVAPOOL, LEG, LEVEL_TELEP, LEFT_SIDE, LEVITATION, M_AP_FURNITURE, M_AP_OBJECT, M_AP_TYPE, MAGIC_PORTAL, MAGIC_TRAP, MAXULEV, MIGR_PORTAL, MIGR_RANDOM, MM_NOCOUNTBIRTH, MM_NOMSG, MOAT, N_DIRS, NO_KILLER_PREFIX, NO_MINVENT, NO_MM_FLAGS, NON_PM, NOTELL, OBJ_MINVENT, PIT, POLY_TRAP, POOL, RIGHT_SIDE, ROCKTRAP, ROLLING_BOULDER_TRAP, RUST_TRAP, SHOPBASE, SLP_GAS_TRAP, SPIKED_PIT, SPINE, SQKY_BOARD, STAIRS, STATUE_TRAP, STONING, TELEP_TRAP, TIMEOUT, TRAPDOOR, TRAPPED_CHEST, TRAPPED_DOOR, TT_BEARTRAP, TT_BURIEDBALL, TT_INFLOOR, TT_LAVA, TT_NONE, TT_PIT, TT_WEB, u_at, Is_knox_level, UTOTYPE_ATSTAIRS, UTOTYPE_FALLING, UTOTYPE_NONE, UTOTYPE_PORTAL, VIBRATING_SQUARE, WATER, WEB, W_ARM, W_ARMC, W_ARMF, W_ARMG, W_ARMH, W_ARMS, W_ARMU, xdir, ydir, ZAP_POS } from './const.js';
import { cansee, couldsee } from './vision.js';
import { mksobj } from './mkobj.js';
import { level_difficulty, makemon, goodpos } from './makemon.js';
import { exercise, adjattrib, ACURR, poisoned } from './attrib.js';
import { hliquid } from './do_name.js';
import { Norep, schedule_goto, set_wounded_legs } from './do.js';
import { rloc, rloc_to, rloc_to_core, tele, level_tele, vault_tele, teleds, noteleport_level } from './teleport.js';
import { RLOC_MSG, TELEDS_ALLOW_DRAG, TELEDS_TELEPORT } from './const.js';
import { polyself, body_part, float_vs_flight } from './polyself.js';
import { acidic, amorphous, breathless, completelyrusts, control_teleport, flaming, grounded, is_animal, is_clinger, is_floater, is_flyer, is_mindless as mindless, is_unicorn, is_vampshifter, is_whirly, mdistu, mon_knows_traps, mon_learns_traps, monsndx, nohands, nonliving, passes_rocks, passes_walls, resists_blnd, resists_fire, resists_magm, resists_sleep, sticks as sticks_fn, strongmonst, throws_rocks, webmaker } from './mondata.js';
import { encumber_msg, near_capacity, pickup } from './pickup.js';
import { burn_floor_objects, resist } from './zap.js';
import { Can_fall_thru, is_ice } from './dbridge.js';
import { buried_ball_to_punishment, ceiling } from './dig.js';
import { migrate_to_level, tamedog } from './dog.js';
import { make_blinded, make_hallucinated, make_stunned, self_invis_message } from './potion.js';
import { a_monnam, DEADMONSTER, mon_nam, mongone, Monnam, newcham, setmangry, stop_occupation, update_inventory, wake_nearby, wake_nearto, m_in_air } from './mon.js';
import { ARROW, BOULDER, CAN_OF_GREASE, CHEST, DART, ICE_BOX, IRON_SHOES, Is_container, is_corrodeable, is_crackable, is_flammable, is_rottable, is_rustprone, LARGE_BOX, OILSKIN_SACK, POT_ACID, POT_WATER, POTION_CLASS, ROCK, SCR_BLANK_PAPER, SCROLL_CLASS, SPBOOK_CLASS, SPE_BLANK_PAPER, SPE_BOOK_OF_THE_DEAD, STATUE, stone_missile, TOWEL, WAND_CLASS } from './objects.js';
import { bimanual as bimanual_fn, depth, on_level, s_suffix, surface, unsolid } from './hacklib.js';
import { welded as welded_fn } from './wield.js';
import { sobj_at, place_object, obj_extract_self, obfree, dealloc_obj, mkcorpstat, rndmonnum_adj, add_to_container } from './mkobj.js';
import { nxtobj, stackobj, delobj, observe_object } from './invent.js';
import { artilist, ART_MASTER_KEY_OF_THIEVERY } from './artifacts.js';
import { enexto } from './makemon.js';
import { weight } from './mkobj.js';
import { which_armor, find_mac } from './worn.js';
import { carried } from './invent.js';
import { catch_lit, next_to_u, splash_lit } from './apply.js';
// observe_object: moved to invent.js import below
import { rndcolor, rndmonnam } from './do_name.js';
import { sleep_monst } from './mhitm.js';
import { cxname, doname, makeplural, the, vtense, xname } from './objnam.js';
import { ohitmon, thitu } from './mthrowu.js';
import { mpickobj, remove_worn_item } from './steal.js';
import { add_damage, costly_alteration, in_rooms } from './shk.js';
import { extract_from_minvent } from './worn.js';
import { more_experienced, newexplevel } from './exper.js';
import { ynFunction, ynq } from './input.js';
import { Sokoban } from './mklev.js';
import { fall_asleep } from './timeout.js';
import { assign_level, dunlevs_in_dungeon, get_level, ledger_no } from './dungeon.js';
import { mon_has_amulet } from './wizard.js';
 
// ── Trap flags ──
const FORCETRAP = 0x01;
const NOWEBMSG = 0x02;
const FORCEBUNGLE = 0x04;
const RECURSIVETRAP = 0x08;
const TOOKPLUNGE = 0x10;
const VIASITTING = 0x20;
const FAILEDUNTRAP = 0x40;
const NO_TRAP_FLAGS = 0;
 
// Trap_Effect return codes (C: enum in trap.h)
const Trap_Effect_Finished = 0;
const Trap_Killed_Mon = 1;
const Trap_Caught_Mon = 2;
const Trap_Is_Gone = 3;
const Trap_Moved_Mon = 4;
 
// Erosion types (C: erode.h)
const ERODE_BURN = 0;
const ERODE_RUST = 1;
const ERODE_ROT = 2;
const ERODE_CORRODE = 3;
const ERODE_CRACK = 4;
 
// Erosion flags
const EF_NONE = 0;
const EF_GREASE = 0x01;
const EF_DESTROY = 0x02;
const EF_VERBOSE = 0x04;
const EF_PAY = 0x08;
 
// Erosion results
const ER_NOTHING = 0;
const ER_GREASED = 1;
const ER_DAMAGED = 2;
const ER_DESTROYED = 3;
 
const MAX_ERODE = 3;
 
// ── Local stubs for functions not yet ported elsewhere ──
// These return safe defaults and consume no RNG.
 
// Sokoban: imported from mklev.js
// ceiling: imported from dig.js
// webmaker: imported from mondata.js
// flaming: imported from mondata.js
// acidic imported from mondata.js
// grounded: imported from mondata.js
// mindless: imported from mondata.js as is_mindless
function metallivorous(ptr) { return !!(ptr?.mflags1 & 0x80000000); } // M1_METALLIVORE
// map_trap: imported from display.js
// C ref: trap.c:910 activate_statue_trap — animates a statue via statue trap
export async function activate_statue_trap(trap, x, y, shatter) {
    const otmp = sobj_at(STATUE, x, y);
    if (!otmp) return null;
    deltrap(trap);
    // C: animate_statue — simplified port
    const mnum = otmp.corpsenm ?? NON_PM;
    if (mnum < 0 || mnum >= mons.length) return null;
    const mptr = mons[mnum];
    if (!mptr) return null;
    // Create the monster
    const mon = await makemon(mptr, x, y, NO_MINVENT | MM_NOMSG);
    if (!mon) return null;
    // Statue trap always hostile
    if (!shatter) {
        mon.mtame = 0;
        mon.mpeaceful = 0;
    }
    mon.msleeping = 0;
    mon.mundetected = false;
    if (M_AP_TYPE(mon)) seemimic(mon);
    // Messages
    const comes_to_life = !canspotmon(mon) ? 'disappears'
        : (nonliving(mon.data) || is_vampshifter(mon)) ? 'moves'
        : 'comes to life';
    if (u_at(x, y)) {
        await pline('The statue ' + comes_to_life + '!');
    } else if (Hallucination()) {
        await pline_The(rndmonnam() + ' suddenly seems more animated.');
    } else if (shatter) {
        await pline('Instead of shattering, the statue suddenly ' + comes_to_life + '!');
    } else {
        await You('find ' + (canspotmon(mon) ? a_monnam(mon) : 'something') + ' posing as a statue.');
        if (!canspotmon(mon) && Blind()) map_invisible(x, y);
        await stop_occupation();
    }
    // Transfer statue contents to monster inventory
    while (otmp.cobj) {
        const item = otmp.cobj;
        obj_extract_self(item);
        await mpickobj(mon, item);
    }
    delobj(otmp);
    return mon;
}
// enexto: imported from makemon.js
// rloc_to: imported from teleport.js
 
function In_tutorial(uz) {
    return Number.isInteger(game.tutorial_dnum) && uz?.dnum === game.tutorial_dnum;
}
 
// C ref: teleport.c:1492 tele_trap — handle hero stepping on teleport trap
async function tele_trap(trap) {
    const u = game.u;
    if (In_endgame() || Antimagic() || noteleport_level(game.youmonst)) {
        if (Antimagic()) shieldeff(u.ux, u.uy);
        await You_feel('a wrenching sensation.');
    } else if (!await next_to_u()) {
        await You('shudder for a moment.');
    } else if (trap.once) {
        deltrap(trap);
        newsym(u.ux, u.uy);
        await vault_tele();
    } else if (isok(trap.launch?.x, trap.launch?.y)) {
        // Fixed-destination teleport trap
        const mtmp = m_at(trap.launch.x, trap.launch.y);
        if (mtmp) {
            // Try to move the monster out of the way
            const cc = {};
            if (!enexto(cc, mtmp.mx, mtmp.my, mtmp.data)) {
                await You('shudder for a moment.');
            } else {
                await rloc_to(mtmp, cc.x, cc.y);
                await teleds(trap.launch.x, trap.launch.y, TELEDS_TELEPORT);
            }
        } else {
            await teleds(trap.launch.x, trap.launch.y, TELEDS_TELEPORT);
        }
    } else {
        await tele();
    }
}
async function level_tele_trap(_trap, _trflags) { await level_tele(); } // simplified
// C ref: teleport.c:1962 mtele_trap — monster teleported by trap
async function mtele_trap(mtmp, trap, in_sight) {
    if (noteleport_level(mtmp)) return;
    // C ref: teleport.c:786 teleport_pet — check constraints
    if (mtmp === game.u?.usteed) return;
    if (mtmp.mleashed) {
        // C: cursed leash prevents teleport, uncursed unleashes
        // Simplified: leashed monsters don't teleport from traps
        return;
    }
    // Monster teleports
    if (trap.once) {
        // vault trap — mvault_tele simplified to rloc
        await rloc(mtmp, 0);
    } else if (isok(trap.launch?.x, trap.launch?.y)) {
        // Fixed-destination teleport trap
        if (!m_at(trap.launch.x, trap.launch.y)
            && !(game.u?.ux === trap.launch.x && game.u?.uy === trap.launch.y)) {
            await rloc_to_core(mtmp, trap.launch.x, trap.launch.y, RLOC_MSG);
        }
    } else {
        await rloc(mtmp, 0);
    }
    if (in_sight) {
        if (canseemon(mtmp))
            await pline(`${Monnam(mtmp)} seems disoriented.`);
        else
            await pline(`${Monnam(mtmp)} suddenly disappears!`);
    }
}
function random_teleport_level_local() {
    const cur_depth = depth(game.u?.uz);
    if (!rn2(5) || Is_knox_level(game.u?.uz) || In_endgame(game.u?.uz))
        return cur_depth;
    let min_depth, max_depth;
    if (In_quest(game.u?.uz)) {
        min_depth = game.dungeons?.[game.u.uz.dnum]?.depth_start || 1;
        max_depth = dunlevs_in_dungeon(game.u?.uz)
            + (game.dungeons?.[game.u.uz.dnum]?.depth_start || 1) - 1;
    } else {
        min_depth = 1;
        max_depth = dunlevs_in_dungeon(game.u?.uz)
            + (game.dungeons?.[game.u.uz.dnum]?.depth_start || 1) - 1;
        if (Inhell() && !game.u?.uevent?.invoked)
            max_depth -= 1;
    }
    let nlev = rn2(cur_depth + 3 - min_depth) + min_depth;
    if (nlev >= cur_depth) nlev++;
    if (nlev > max_depth) nlev = max_depth;
    if (nlev < min_depth) nlev = min_depth;
    return nlev;
}
async function mlevel_tele_trap(mtmp, trap, force_it, in_sight) {
    const tt = trap ? trap.ttyp : LEVEL_TELEP;
    if (mtmp === game.u?.ustuck) return Trap_Effect_Finished;
    if (mtmp === game.u?.usteed) return Trap_Effect_Finished;
    if (mtmp.mleashed && !force_it) return Trap_Effect_Finished;
 
    const tolevel = {};
    let migrate_typ = MIGR_RANDOM;
    if (is_hole(tt)) {
        if (Is_stronghold(game.u?.uz) && game.valley_level) {
            assign_level(tolevel, game.valley_level);
        } else if (Is_botlevel(game.u?.uz)) {
            if (in_sight && trap?.tseen) {
                await pline_mon(mtmp, `${Monnam(mtmp)} avoids the ${tt === HOLE ? 'hole' : 'trap'}.`);
            }
            return Trap_Effect_Finished;
        } else {
            assign_level(tolevel, trap?.dst || game.u?.uz);
            clamp_hole_destination(tolevel);
        }
    } else if (tt === MAGIC_PORTAL) {
        if (In_endgame(game.u?.uz) && (mon_has_amulet(mtmp) || rn2(7))) {
            if (in_sight) {
                await pline_mon(mtmp, `${Monnam(mtmp)} seems to shimmer for a moment.`);
                if (trap) seetrap(trap);
            }
            return Trap_Effect_Finished;
        }
        assign_level(tolevel, trap?.dst || game.u?.uz);
        migrate_typ = MIGR_PORTAL;
    } else if (tt === LEVEL_TELEP) {
        if (mon_has_amulet(mtmp) || In_endgame(game.u?.uz)) {
            if (in_sight)
                await pline_mon(mtmp, `${Monnam(mtmp)} seems very disoriented for a moment.`);
            return Trap_Effect_Finished;
        }
        const nlev = random_teleport_level_local();
        if (nlev === depth(game.u?.uz)) {
            if (in_sight)
                await pline_mon(mtmp, `${Monnam(mtmp)} shudders for a moment.`);
            return Trap_Effect_Finished;
        }
        get_level(tolevel, nlev);
    } else {
        return Trap_Effect_Finished;
    }
 
    if (in_sight) {
        await pline_mon(mtmp, `Suddenly, ${mon_nam(mtmp)} ${tt === HOLE
            ? 'falls into a hole'
            : (tt === TRAPDOOR ? 'falls through a trap door' : 'disappears out of sight')}.`);
        if (trap) seetrap(trap);
    }
    if (is_xport(tt) && !control_teleport(mtmp.data))
        mtmp.mconf = 1;
    await migrate_to_level(mtmp, ledger_no(tolevel), migrate_typ, null);
    return Trap_Moved_Mon;
}
async function domagicportal(trap) {
    const u = game.u;
    let stunmsg = null;
 
    if (u.utrap && u.utraptype === TT_BURIEDBALL)
        await buried_ball_to_punishment();
 
    if (!await next_to_u()) {
        await You('shudder for a moment.');
        return;
    }
 
    if (!on_level(u.uz, u.uz0)) {
        return;
    }
 
    await You('activated a magic portal!');
 
    if (In_endgame(u.uz) && !u.uhave?.amulet) {
        await You_feel('dizzy for a moment, but nothing happens...');
        return;
    }
 
    const target_level = { ...(trap?.dst || {}) };
    let totype;
 
    if (In_tutorial(u.uz) && !In_tutorial(target_level)) {
        totype = UTOTYPE_ATSTAIRS;
        stunmsg = 'Resuming regular play.';
    } else {
        totype = UTOTYPE_PORTAL;
        stunmsg = !Stunned() ? 'You feel slightly dizzy.' : 'You feel dizzier.';
        await make_stunned((HStun() & TIMEOUT) + 3, false);
    }
 
    schedule_goto(target_level, totype, stunmsg, null);
}
function steedintrap(_trap, _otmp) { return 0; } // no steed support yet
// destroy_items: imported from mkobj.js
// ignite_items: imported from mkobj.js
// melt_ice: imported from mkobj.js
// erosion_matters: imported from mkobj.js
 // simplified
 // simplified
function clear_nhwindow(_win) { /* TODO */ }
// Can_fall_thru: imported from dbridge.js
// assign_level: imported from dungeon.js
function clamp_hole_destination(dlev) { return dlev; }
// u_locomotion: imported from macros.js
 // simplified
 // simplified
 // simplified
 // simplified
 // simplified
// stone_missile imported from objects.js
// passes_rocks imported from mondata.js
 
const ROLL = 0x01;
const LAUNCH_UNSEEN = 0x40;
const LAUNCH_KNOWN = 0x80;
 
// S_SPIDER, S_GIANT, S_DRAGON: imported from monsters.js
// MZ_SMALL, MZ_HUGE: imported from monsters.js
 
// AD_FIRE, AD_ACID, AD_PHYS, AD_RBRE, AD_ELEC, AD_RUST, AD_SLEE, AD_MAGM:
// imported from monsters.js
 
// TT_LAVA, TT_INFLOOR, TT_BURIEDBALL imported from const.js
 
const ANIMATE_NORMAL = 0;
const ANIMATE_SHATTER = 1;
const ANIMATE_SPELL = 2;
 
const AS_OK = 0;
const AS_NO_MON = 1;
const AS_MON_IS_UNIQUE = 2;
 
// ============================================================================
// erode_obj — Generic erode-item function
// C ref: trap.c:172
// RNG: rnl(4) for blessed protection
// ============================================================================
export async function erode_obj(otmp, ostr, type, ef_flags) {
    const action = ['smoulder', 'rust', 'rot', 'corrode', 'crack'];
    const msg = ['burnt', 'rusted', 'rotten', 'corroded', 'cracked'];
    const bythe = ['heat', 'oxidation', 'decay', 'corrosion', 'impact'];
 
    let vulnerable = false, is_primary = true;
    let check_grease = !!(ef_flags & EF_GREASE);
    const print = !!(ef_flags & EF_VERBOSE);
 
    if (!otmp) return ER_NOTHING;
 
    const uvictim = carried(otmp);
    // simplified: no vismon/visobj checks
 
    switch (type) {
    case ERODE_BURN:
        vulnerable = is_flammable(otmp);
        check_grease = false;
        break;
    case ERODE_RUST:
        vulnerable = is_rustprone(otmp);
        break;
    case ERODE_ROT:
        vulnerable = is_rottable(otmp);
        check_grease = false;
        is_primary = false;
        break;
    case ERODE_CORRODE:
        vulnerable = is_corrodeable(otmp);
        is_primary = false;
        break;
    case ERODE_CRACK:
        vulnerable = is_crackable(otmp);
        is_primary = true;
        break;
    default:
        return ER_NOTHING;
    }
    const erosion = is_primary ? (otmp.oeroded || 0) : (otmp.oeroded2 || 0);
 
    if (!ostr) ostr = cxname(otmp);
 
    if (check_grease && otmp.greased) {
        await grease_protect(otmp, ostr, uvictim ? game.u : null);
        return ER_GREASED;
    } else if (!erosion_matters(otmp)) {
        return ER_NOTHING;
    } else if (!vulnerable || (otmp.oerodeproof && otmp.rknown)) {
        if (print && uvictim)
            await pline(`Your ${ostr} ${vtense(ostr, 'are')} not affected by ${bythe[type]}.`);
        return ER_NOTHING;
    } else if (otmp.oerodeproof || (otmp.blessed && !rnl(4))) {
        if ((print || otmp.oerodeproof) && uvictim)
            await pline(`Somehow, your ${ostr} ${vtense(ostr, 'are')} not affected by the ${bythe[type]}.`);
        if (otmp.oerodeproof) {
            otmp.rknown = true;
            if (uvictim) update_inventory();
        }
        return ER_NOTHING;
    } else if (erosion < MAX_ERODE) {
        const adverb = (erosion + 1 === MAX_ERODE) ? ' completely'
            : erosion ? ' further' : '';
        if (uvictim)
            await pline(`Your ${ostr} ${vtense(ostr, action[type])}${adverb}!`);
        if (is_primary)
            otmp.oeroded = (otmp.oeroded || 0) + 1;
        else
            otmp.oeroded2 = (otmp.oeroded2 || 0) + 1;
        if (uvictim) update_inventory();
        return ER_DAMAGED;
    } else if (ef_flags & EF_DESTROY) {
        if (uvictim)
            await pline(`Your ${ostr} ${vtense(ostr, action[type])} away!`);
        delobj(otmp);
        return ER_DESTROYED;
    } else {
        if (print && uvictim)
            await pline(`Your ${ostr} ${vtense(ostr, Blind() ? 'feel' : 'look')} completely ${msg[type]}.`);
        return ER_NOTHING;
    }
}
 
// ── grease_protect — Protect an item from erosion with grease ──
// C ref: trap.c:361
// RNG: rn2(2) grease dissolution
export async function grease_protect(otmp, ostr, victim) {
    const txt = 'protected by the layer of grease!';
    const uvictim = victim === game.u;

    if (ostr) {
        if (uvictim)
            await pline(`Your ${ostr} ${vtense(ostr, 'are')} ${txt}`);
    }
    if (!rn2(2)) {
        otmp.greased = 0;
        if (carried(otmp)) {
            await pline_The('grease dissolves.');
            update_inventory();
        }
        return true;
    }
    return false;
}
 
// ── burnarmor — fire damages armor ──
// C ref: trap.c:89
// RNG: rn2(oldspe+1) for towel, rn2(5) body part selection
export async function burnarmor(victim) {
    if (!victim) return false;
    const hitting_u = (victim === game.youmonst || victim === game.u);
 
    // towel drying — simplified (no towel tracking yet)
 
    const burn_dmg = async (obj, descr) => await erode_obj(obj, descr, ERODE_BURN, EF_GREASE);
 
    while (true) {
        switch (rn2(5)) {
        case 0: {
            const item = hitting_u ? game.u?.uarmh : which_armor(victim, W_ARMH);
            if (!await burn_dmg(item, 'helmet')) continue;
            break;
        }
        case 1: {
            let item = hitting_u ? game.u?.uarmc : which_armor(victim, W_ARMC);
            if (item) { await burn_dmg(item, 'cloak'); return true; }
            item = hitting_u ? game.u?.uarm : which_armor(victim, W_ARM);
            if (item) { await burn_dmg(item, xname(item)); return true; }
            item = hitting_u ? game.u?.uarmu : which_armor(victim, W_ARMU);
            if (item) await burn_dmg(item, 'shirt');
            return true;
        }
        case 2: {
            const item = hitting_u ? game.u?.uarms : which_armor(victim, W_ARMS);
            if (!await burn_dmg(item, 'wooden shield')) continue;
            break;
        }
        case 3: {
            const item = hitting_u ? game.u?.uarmg : which_armor(victim, W_ARMG);
            if (!await burn_dmg(item, 'gloves')) continue;
            break;
        }
        case 4: {
            const item = hitting_u ? game.u?.uarmf : which_armor(victim, W_ARMF);
            if (!await burn_dmg(item, 'boots')) continue;
            break;
        }
        }
        break;
    }
    return false;
}
 
// ── wearing_iron_shoes — check if monster wears iron shoes ──
// C ref: trap.c:1099
function wearing_iron_shoes(mtmp) {
    if (!mtmp) return false;
    const armf = which_armor(mtmp, W_ARMF);
    return armf && armf.otyp === IRON_SHOES;
}
 
// ── mu_maybe_destroy_web — monster or hero may destroy a web ──
// C ref: trap.c:973
async function mu_maybe_destroy_web(mtmp, domsg, trap) {
    const isyou = (mtmp === game.u);
    const mptr = mtmp?.data;
    if (!mptr) return false;

    if (amorphous(mptr) || is_whirly(mptr) || flaming(mptr)
        || unsolid(mptr) || monsndx(mptr) === PM_GELATINOUS_CUBE) {
        const x = trap.tx;
        const y = trap.ty;

        if (flaming(mptr) || acidic(mptr)) {
            if (domsg) {
                if (isyou)
                    await pline(`You ${flaming(mptr) ? 'burn' : 'dissolve'} ${trap.madeby_u ? 'your' : 'a'} spider web!`);
                else
                    await pline(`${Monnam(mtmp)} ${flaming(mptr) ? 'burns' : 'dissolves'} ${trap.madeby_u ? 'your' : 'a'} spider web!`);
            }
            deltrap(trap);
            newsym(x, y);
            return true;
        }
        if (domsg) {
            if (isyou)
                await pline(`You flow through ${trap.madeby_u ? 'your' : 'a'} spider web.`);
            else
                await pline(`${Monnam(mtmp)} flows through ${trap.madeby_u ? 'your' : 'a'} spider web.`);
        }
        return true;
    }
    return false;
}
 
// ── m_harmless_trap — is trap harmless to monster? ──
// C ref: trap.c:1107
export function m_harmless_trap(mtmp, ttmp) {
    const mdat = mtmp?.data;
    if (!mdat || !ttmp) return false;
 
    if (!Sokoban() && floor_trigger(ttmp.ttyp) && check_in_air_mon(mtmp, 0))
        return true;
 
    switch (ttmp.ttyp) {
    case BEAR_TRAP:
        if (mdat.msize <= MZ_SMALL || amorphous(mdat) || is_whirly(mdat) || unsolid(mdat))
            return true;
        break;
    case SLP_GAS_TRAP:
        if (resists_sleep(mtmp))
            return true;
        break;
    case RUST_TRAP:
        if (monsndx(mdat) !== PM_IRON_GOLEM)
            return true;
        break;
    case FIRE_TRAP:
        if (resists_fire(mtmp))
            return true;
        break;
    case PIT:
    case SPIKED_PIT:
    case HOLE:
    case TRAPDOOR:
        if (is_clinger(mdat) && !Sokoban())
            return true;
        break;
    case WEB:
        if (amorphous(mdat) || webmaker(mdat) || is_whirly(mdat) || unsolid(mdat))
            return true;
        break;
    case STATUE_TRAP:
        return true;
    case MAGIC_TRAP:
        return true;
    case ANTI_MAGIC:
        if (resists_magm(mtmp))
            return true;
        break;
    case VIBRATING_SQUARE:
        return true;
    default:
        break;
    }
    return false;
}
 
// ── thitm — trap missile hits a monster ──
// C ref: trap.c:6689
// RNG: rnd(20) for hit check, dmgval for damage
async function thitm(tlev, mon, obj, d_override, nocorpse) {
    let strike;
    let trapkilled = false;
 
    if (d_override)
        strike = 1;
    else if (obj)
        strike = (find_mac(mon) + tlev + (obj.spe || 0) <= rnd(20));
    else
        strike = (find_mac(mon) + tlev <= rnd(20));
 
    if (!strike) {
        if (obj && cansee(mon.mx, mon.my))
            await pline(`${Monnam(mon)} is almost hit by ${doname(obj)}!`);
    } else {
        let dam = 1;
        const harmless = obj && stone_missile(obj) && passes_rocks(mon.data);
 
        if (obj && cansee(mon.mx, mon.my))
            await pline(`${Monnam(mon)} is hit by ${doname(obj)}${harmless ? ' but is not harmed.' : '!'}`);
 
        if (d_override) {
            dam = d_override;
        } else if (obj) {
            dam = dmgval(obj, mon);
            if (dam < 1) dam = 1;
        }
        if (!harmless) {
            mon.mhp -= dam;
            if (mon.mhp <= 0) {
                const xx = mon.mx, yy = mon.my;
                await monkilled(mon, '', nocorpse ? -AD_RBRE : AD_PHYS);
                if (mon.mhp <= 0) {
                    newsym(xx, yy);
                    trapkilled = true;
                }
            }
        } else {
            strike = 0;
        }
    }
    if (obj && (!strike || d_override)) {
        place_object(obj, mon.mx, mon.my);
        stackobj(obj);
    } else if (obj) {
        dealloc_obj(obj);
    }
    return trapkilled;
}
 
// ── fill_pit — a boulder fills a pit or hole at x,y ──
// C ref: trap.c:3988
export function fill_pit(x, y) {
    const t = t_at(x, y);
    if (t && (is_pit(t.ttyp) || is_hole(t.ttyp))) {
        const otmp = sobj_at(BOULDER, x, y);
        if (otmp) {
            obj_extract_self(otmp);
            // C: flooreffects(otmp, x, y, "settle") — simplified
            place_object(otmp, x, y);
        }
    }
}
 
// ── blow_up_landmine — landmine explosion effects ──
// C ref: trap.c:3169
export function blow_up_landmine(trap) {
    const x = trap.tx, y = trap.ty;
    // C: scatter, del_engr_at, drawbridge destruction — simplified
    wake_nearto(x, y, 400);
 
    const loc = levl(x, y);
    if (loc && loc.typ === DOOR)
        loc.doormask = 8; // D_BROKEN
 
    // convert landmine into pit
    const t = t_at(x, y);
    if (t) {
        t.ttyp = PIT;
        t.madeby_u = false;
        seetrap(t);
    }
    fill_pit(x, y);
}
 
// ── launch_obj — Move obj from (x1,y1) to (x2,y2) ──
// C ref: trap.c:3257
// RNG: rn2(3) for boulder snatch, rn2(10) landmine trigger,
//      rn2(20) iron bars, various other checks
export async function launch_obj(otyp, x1, y1, x2, y2, style) {
    let otmp = sobj_at(otyp, x1, y1);
    let otherside = false;
 
    // Try the other side too for rolling boulder traps
    if (!otmp && otyp === BOULDER) {
        otherside = true;
        otmp = sobj_at(otyp, x2, y2);
    }
    if (!otmp) return 0;

    if (otherside) {
        const tx = x1, ty = y1;
        x1 = x2; y1 = y2;
        x2 = tx; y2 = ty;
    }

    // Extract the single object
    let singleobj;
    if ((otmp.quan || 1) === 1) {
        obj_extract_self(otmp);
        singleobj = otmp;
        otmp = null;
    } else {
        // splitobj — simplified
        singleobj = { ...otmp, quan: 1 };
        otmp.quan = (otmp.quan || 1) - 1;
    }
    newsym(x1, y1);

    let dist = Math.max(Math.abs(x2 - x1), Math.abs(y2 - y1));
    let x = x1, y = y1;
    const dx = Math.sign(x2 - x1);
    const dy = Math.sign(y2 - y1);
    let used_up = false;

    if (style & LAUNCH_KNOWN) {
        singleobj.otrapped = 1;
    }

    // Set the object in motion
    // C ref: trap.c:3360-3361 — initialize bhitpos to starting position
    if (!game.bhitpos) game.bhitpos = { x: 0, y: 0 };
    game.bhitpos.x = x;
    game.bhitpos.y = y;
    while (dist-- > 0 && !used_up) {
        // C ref: trap.c:3372-3373 — update bhitpos as object moves
        x = (game.bhitpos.x += dx);
        y = (game.bhitpos.y += dy);

        // Check for monster
        const mtmp = m_at(x, y);
        if (mtmp) {
            // C ref: trap.c:3376 — throws_rocks monsters snatch boulders
            if (otyp === BOULDER && throws_rocks(mtmp.data)) {
                if (rn2(3)) {
                    if (cansee(x, y))
                        await pline(`${Monnam(mtmp)} snatches the boulder.`);
                    singleobj.otrapped = 0;
                    await mpickobj(mtmp, singleobj);
                    used_up = true;
                    break;
                }
            }
            // C ref: trap.c:3388 — real ohitmon for launched projectile
            if (await ohitmon(mtmp, singleobj, (style & ROLL) ? -1 : dist, false)) {
                used_up = true;
                break;
            }
        }

        // Check for hero
        if (x === game.u?.ux && y === game.u?.uy) {
            if (game.multi) nomul(0);
            // C ref: trap.c:3400 — thitu check, stop_occupation if hit
            if (await thitu(9 + (singleobj.spe || 0), dmgval(singleobj, game.youmonst),
                      { obj: singleobj }, null)) {
                await stop_occupation();
            }
        }

        // Check for traps
        if (style === ROLL || (style & ROLL)) {
            const t = t_at(x, y);
            if (t && otyp === BOULDER) {
                switch (t.ttyp) {
                case LANDMINE:
                    if (rn2(10) > 2) {
                        await pline('KAABLAMM!!!');
                        deltrap(t);
                        place_object(singleobj, x, y);
                        singleobj.otrapped = 0;
                        used_up = true;
                    }
                    break;
                case PIT:
                case SPIKED_PIT:
                case HOLE:
                case TRAPDOOR:
                    // boulder falls into pit/hole
                    place_object(singleobj, x, y);
                    used_up = true;
                    dist = -1;
                    break;
                default:
                    break;
                }
                if (used_up || dist === -1) break;
            }

            // Check for another boulder
            const otmp2 = sobj_at(BOULDER, x, y);
            if (otyp === BOULDER && otmp2) {
                await pline('You hear a loud crash!');
                wake_nearto(x, y, 100);
                obj_extract_self(otmp2);
                otmp2.otrapped = singleobj.otrapped;
                singleobj.otrapped = 0;
                place_object(singleobj, x, y);
                singleobj = otmp2;
            }
        }

        // Check for wall
        if (dist > 0 && isok(x + dx, y + dy)) {
            const loc = levl(x + dx, y + dy);
            if (loc) {
                const typ = loc.typ;
                if (typ >= 1 && typ <= 5) { // IS_STWALL or IS_TREE
                    if (!Deaf()) await pline('Thump!');
                    wake_nearto(x, y, 16);
                    break;
                }
            }
        }
    }

    if (!used_up) {
        singleobj.otrapped = 0;
        place_object(singleobj, x2, y2);
        newsym(x2, y2);
        return 1;
    }
    return 2;
}
 
// ── drain_en — drain magical energy ──
// C ref: trap.c:5180
// RNG: rnd(n) for throttling, rnd(-u.uen) for max drain
export async function drain_en(n, max_already_drained) {
    const punct = max_already_drained ? '!' : '.';
    let mesg;

    if ((game.u?.uenmax || 0) < 1) {
        if (game.u?.uen || game.u?.uenmax) {
            game.u.uen = game.u.uenmax = 0;
        }
        mesg = 'momentarily lethargic';
    } else {
        // throttle further loss
        if (n > Math.trunc(((game.u?.uen || 0) + (game.u?.uenmax || 0)) / 3))
            n = rnd(n);

        mesg = 'your magical energy drain away';
        let exclaim = max_already_drained ? '!' : '.';
        if (n > (game.u?.uen || 0)) exclaim = '!';

        game.u.uen = (game.u?.uen || 0) - n;
        if (game.u.uen < 0) {
            game.u.uenmax = (game.u?.uenmax || 0) - rnd(-game.u.uen);
            if (game.u.uenmax < 0) game.u.uenmax = 0;
            game.u.uen = 0;
        } else if (game.u.uen > game.u.uenmax) {
            game.u.uen = game.u.uenmax;
        }
    }
    await You_feel(`${mesg}${punct}`);
}
 
// ── selftouch — cockatrice corpse check when hero touches something ──
// C ref: trap.c:3861
export async function selftouch(arg) {
    // C: checks uwep and uswapwep for corpse + touch_petrifies
    // simplified — no corpse wielding system yet
}
 
// ── mselftouch — monster cockatrice corpse check ──
// C ref: trap.c:3891
export function mselftouch(mon, arg, byplayer) {
    // simplified — no monster weapon corpse tracking yet
}
 
// ── float_up — start levitating ──
// C ref: trap.c:3915
export async function float_up() {
    setBotl('float_up');
    if (game.u?.utrap) {
        if (game.u.utraptype === TT_PIT) {
            await reset_utrap(false);
            await You(`float up, out of the ${trapname(PIT, false)}!`);
            game.vision_full_recalc = 1; // C ref: trap.c:3922
            fill_pit(game.u.ux, game.u.uy);
        } else if (game.u.utraptype === TT_LAVA || game.u.utraptype === TT_INFLOOR) {
            await Your(`body pulls upward, but your ${makeplural(body_part(LEG))} are still stuck.`);
        } else if (game.u.utraptype === TT_BURIEDBALL) {
            await You(`feel lighter, but your ${body_part(LEG)} is still chained to the ground.`);
        } else if (game.u.utraptype === TT_BEARTRAP) {
            await You('float up slightly, but your leg is still stuck.');
        } else if (game.u.utraptype === TT_WEB) {
            await You(`float up slightly, but you are still stuck in the ${trapname(WEB, false)}.`);
        }
    } else if (game.u?.uinwater) {
        await spoteffects(true);
    } else if (game.u?.uswallow) {
        if (is_animal(game.u.ustuck?.data))
            await You(`float away from the ${surface(game.u.ux, game.u.uy)}.`);
        else
            await You(`spiral up into ${mon_nam(game.u.ustuck)}.`);
    } else if (Hallucination()) {
        await pline("Up, up, and awaaaay!  You're walking on air!");
    } else {
        await You('start to float in the air!');
    }
    if (Flying())
        await You('are no longer able to control your flight.');
    float_vs_flight();
    await encumber_msg();
}
 
// ── float_down — stop levitating ──
// C ref: trap.c:4002
// RNG: rnd(2) Sokoban fall damage
export async function float_down(hmask, emask) {
    // C: HLevitation &= ~hmask; ELevitation &= ~emask;
    // simplified — we don't track these bitmasks yet
    if (Levitation()) return 0;
 
    setBotl('float_down');
    nomul(0);
 
    // C ref: trap.c float_down() — if BFlying has I_SPECIAL from levitation,
    // clear/recompute it before deciding whether flight continues.
    if (game.u?.uprops?.[FLYING]?.blocked) {
        float_vs_flight();
        if (Flying()) {
            await You('have stopped levitating and are now flying.');
            await encumber_msg();
            return 1;
        }
    }
 
    // Check trap at current position
    let trap = t_at(game.u?.ux, game.u?.uy);
 
    if (!trap) {
        if (Hallucination()) {
            await pline("Bummer!  You've hit the ground");
        } else {
            await You(`float gently to the ${surface(game.u?.ux, game.u?.uy)}.`);
        }
    }
 
    // C ref: trap.c float_down() — after down-message, before trap handling.
    await encumber_msg();
 
    const currentDungeonLevel = { ...game.u.uz };
    if (trap) {
        switch (trap.ttyp) {
        case STATUE_TRAP:
            break;
        case HOLE:
        case TRAPDOOR:
            if (!Can_fall_thru(game.u?.uz))
                break;
            // fall through
        default:
            if (!game.u?.utrap)
                await dotrap(trap, NO_TRAP_FLAGS);
            break;
        }
    }
    if (!Is_airlevel(game.u?.uz)
        && !Is_waterlevel(game.u?.uz)
        && !game.u?.uswallow
        && on_level(game.u?.uz, currentDungeonLevel)) {
        await pickup(1);
    }
    return 1;
}
 
// ── climb_pit — shared code for climbing out of a pit ──
// C ref: trap.c:4161
// RNG: rn2(2) boulder stuck, rn2(5) hallucination message
export async function climb_pit() {
    if (!game.u?.utrap || game.u?.utraptype !== TT_PIT)
        return;
 
    const pitname = trapname(PIT, false);
    if (Passes_walls()) {
        await You(`ascend from the ${pitname}.`);
        await reset_utrap(false);
        fill_pit(game.u.ux, game.u.uy);
        game.vision_full_recalc = 1; // C ref: trap.c:4174
    } else if (!rn2(2) && sobj_at(BOULDER, game.u.ux, game.u.uy)) {
        await Your(`${body_part(LEG)} gets stuck in a crevice.`);
        await You(`free your ${body_part(LEG)}.`);
    } else if ((Flying() || is_clinger(game.u?.data)) && !Sokoban()) {
        await You(`${u_locomotion('climb')} from the ${pitname}.`);
        await reset_utrap(false);
        fill_pit(game.u.ux, game.u.uy);
        game.vision_full_recalc = 1; // C ref: trap.c:4186
    } else if (!(--game.u.utrap) || m_easy_escape_pit(game.youmonst)) {
        await reset_utrap(false);
        const edgeVerb = (Sokoban() && Levitation())
            ? 'struggle against the air currents and float'
            : game.u.usteed ? 'ride' : 'crawl';
        await You(`${edgeVerb} to the edge of the ${pitname}.`);
        fill_pit(game.u.ux, game.u.uy);
        game.vision_full_recalc = 1; // C ref: trap.c:4195
    } else if (game.u?.dz || game.flags?.verbose) {
        if (game.u?.usteed) {
            await Norep(`${Monnam(game.u.usteed)} is still in a pit.`);
        } else {
            await Norep((Hallucination() && !rn2(5))
                ? "You've fallen, and you can't get up."
                : 'You are still in a pit.');
        }
    }
}
 
// ── m_easy_escape_pit — can monster easily escape pit? ──
// C ref: trap.c:3704
function m_easy_escape_pit(mtmp) {
    return monsndx(mtmp?.data) === PM_PIT_FIEND
        || (mtmp?.data?.msize || 0) >= MZ_HUGE;
}
 
// ── fall_through — hero falls through hole/trapdoor ──
// C ref: trap.c:603
export async function fall_through(td, ftflags) {
    const plunged = !!(ftflags & TOOKPLUNGE);
    let t = null;
 
    if (Blind() && Levitation() && !Sokoban())
        return;
 
    if (td) {
        t = t_at(game.u?.ux, game.u?.uy);
        if (t) feeltrap(t);
        if (!Sokoban() && !plunged) {
            if (t && t.ttyp === TRAPDOOR)
                await pline('A trap door opens up under you!');
            else
                await pline("There's a gaping hole under you!");
        }
    } else {
        await pline_The(`${surface(game.u?.ux, game.u?.uy)} opens up under you!`);
    }
 
    // Check if hero can avoid falling
    if (Levitation() || Flying()) {
        await You("don't fall in.");
        return;
    }
 
    // C ref: trap.c:672-697 — schedule_goto(&dtmp, ...)
    // Prefer trap destination when present, otherwise fall one level down.
    const tolev = (t?.dst && Number.isInteger(t.dst.dnum) && Number.isInteger(t.dst.dlevel))
        ? { dnum: t.dst.dnum, dlevel: t.dst.dlevel }
        : { dnum: game.u?.uz?.dnum ?? 0, dlevel: (game.u?.uz?.dlevel ?? 1) + 1 };
    schedule_goto(tolev, !Flying() ? UTOTYPE_FALLING : UTOTYPE_NONE, null, null);
}
 
// ── check_in_air — hero version (used in dotrap) ──
// C ref: trap.c:1087
function check_in_air(trflags) {
    const plunged = (trflags & (TOOKPLUNGE | VIASITTING)) !== 0;
    return Levitation() || (Flying() && !plunged);
}
 
// ── check_in_air_mon — monster version ──
function check_in_air_mon(mtmp, trflags) {
    return m_in_air(mtmp);
}
 
// ── unconscious — is hero unconscious? ──
// C ref: trap.c:6754
export function unconscious() {
    return (game.multi || 0) < 0 && !!game.u?.usleep;
}
 
// ── fire_damage — set an item on fire ──
// C ref: trap.c:4433
// RNG: rn2(chance) for containers, rn2(20) for luck
export async function fire_damage(obj, force, x, y) {
    if (!obj) return false;
    if (await catch_lit(obj)) return false;

    if (Is_container(obj)) {
        let chance;
        switch (obj.otyp) {
        case 0: return false; // ICE_BOX/STATUE
        default: chance = 20; break;
        }
        if (!force && (game.u?.Luck || 0) + 5 > rn2(chance))
            return false;
        delobj(obj);
        return true;
    } else if (!force && (game.u?.Luck || 0) + 5 > rn2(20)) {
        return false;
    } else if (await erode_obj(obj, null, ERODE_BURN, EF_DESTROY) === ER_DESTROYED) {
        return true;
    }
    return false;
}
 
// ── fire_damage_chain — apply fire_damage to chain ──
// C ref: trap.c:4528
export async function fire_damage_chain(chain, force, here, x, y) {
    let num = 0;
    let obj = chain;
    while (obj) {
        const nobj = here ? obj.nexthere : obj.nobj;
        if (await fire_damage(obj, force, x, y))
            ++num;
        obj = nobj;
    }
    return num;
}
 
// ── water_damage — water damages an object ──
// C ref: trap.c:4690
// RNG: rn2(2) grease loss, rn2(20) luck protection, rnd(7-spe) towel
export async function water_damage(obj, ostr, force) {
    if (!obj) return ER_NOTHING;
 
    if (splash_lit(obj)) return ER_DAMAGED;
 
    if (!ostr) ostr = cxname(obj);
 
    const in_invent = carried(obj);
    // C ref: trap.c:4708 CAN_OF_GREASE (spe>0) → no damage.
    if (obj.otyp === CAN_OF_GREASE && (obj.spe || 0) > 0) {
        return ER_NOTHING;
    }
    // C ref: trap.c:4710 TOWEL absorbs water.
    if (obj.otyp === TOWEL && (obj.spe || 0) < 7) {
        // TODO: wet_a_towel not yet ported; consume no RNG and return.
        return ER_NOTHING;
    }
    if (obj.greased) {
        if (!rn2(2)) {
            obj.greased = 0;
            if (in_invent) {
                await pline_The(`grease on your ${ostr} washes off.`);
                update_inventory();
            }
            // C ref: trap.c:4724 ungreased POT_ACID destroyed by water.
            if (obj.otyp === POT_ACID) {
                // TODO: pot_acid_damage
                return ER_DESTROYED;
            }
        }
        return ER_GREASED;
    }
    // C ref: trap.c:4731-4750 container handling.
    if (Is_container(obj) && !(obj.otyp === ICE_BOX || obj.otyp === CHEST
                                || obj.otyp === LARGE_BOX
                                || obj.otyp === OILSKIN_SACK)) {
        // Non-waterproof container: contents get damaged.
        if (in_invent) {
            await pline(`Some water gets into your ${ostr}!`);
        }
        await water_damage_chain(obj.cobj, false);
        return ER_DAMAGED;
    }
    if (obj.otyp === ICE_BOX || obj.otyp === CHEST || obj.otyp === LARGE_BOX
        || obj.otyp === OILSKIN_SACK) {
        // Waterproof: message + ER_DAMAGED (see C comment).
        return ER_DAMAGED;
    }
    // C ref: trap.c:4751 luck-based save (only when !force).
    if (!force && (game.u?.Luck || 0) + 5 > rn2(20)) {
        return ER_NOTHING;
    }
    // C ref: trap.c:4758 SCROLL_CLASS → blank.
    if (obj.oclass === SCROLL_CLASS) {
        if (obj.otyp === SCR_BLANK_PAPER) return ER_NOTHING;
        if (in_invent) await Your(`${ostr} fades.`);
        obj.otyp = SCR_BLANK_PAPER;
        obj.dknown = 0;
        obj.spe = 0;
        if (in_invent) update_inventory();
        return ER_DAMAGED;
    }
    // C ref: trap.c:4773 SPBOOK_CLASS → blank.
    if (obj.oclass === SPBOOK_CLASS) {
        if (obj.otyp === SPE_BOOK_OF_THE_DEAD || obj.otyp === SPE_BLANK_PAPER) {
            return ER_NOTHING;
        }
        if (in_invent) await Your(`${ostr} fades.`);
        obj.otyp = SPE_BLANK_PAPER;
        if (obj.spestudied) obj.spestudied = rn2(obj.spestudied);
        obj.dknown = 0;
        if (in_invent) update_inventory();
        return ER_DAMAGED;
    }
    // C ref: trap.c:4804 POTION_CLASS → dilute or destroy.
    if (obj.oclass === POTION_CLASS) {
        if (obj.otyp === POT_ACID) {
            // TODO: pot_acid_damage
            return ER_DESTROYED;
        }
        if (obj.odiluted) {
            if (in_invent) await Your(`${ostr} dilutes further.`);
            obj.otyp = POT_WATER;
            obj.dknown = 0;
            obj.blessed = 0;
            obj.cursed = 0;
            obj.odiluted = 0;
            if (in_invent) update_inventory();
            return ER_DAMAGED;
        }
        if (obj.otyp !== POT_WATER) {
            if (in_invent) await Your(`${ostr} dilutes.`);
            obj.odiluted = (obj.odiluted || 0) + 1;
            if (in_invent) update_inventory();
            return ER_DAMAGED;
        }
        return ER_NOTHING;
    }
    // Default: corrodible items erode.
    return await erode_obj(obj, ostr, ERODE_RUST, EF_NONE);
}
 
// ── water_damage_chain — apply water_damage to chain ──
// C ref: trap.c:4833
export async function water_damage_chain(chain, here) {
    let obj = chain;
    while (obj) {
        const otmp = here ? obj.nexthere : obj.nobj;
        await water_damage(obj, null, false);
        obj = otmp;
    }
}
 
// ── chest_trap — trapped chest effect ──
// C ref: trap.c:6272
// RNG: rn2(13+Luck) for save, rn2(13) for lucky msg, rn2(20)/rn2(13-Luck)/rn2(26) for effect
export async function chest_trap(obj, bodypart, disarm) {
    obj.tknown = 0;
    obj.otrapped = 0;

    await You(disarm ? 'set it off!' : 'trigger a trap!');

    if ((game.u?.Luck || 0) > -13 && rn2(13 + (game.u?.Luck || 0)) > 7) {
        // saved by luck
        let msg;
        switch (rn2(13)) {
        case 12: case 11: msg = 'explosive charge is a dud'; break;
        case 10: case 9: msg = 'electric charge is grounded'; break;
        case 8: case 7: msg = 'flame fizzles out'; break;
        case 6: case 5: case 4: msg = 'poisoned needle misses'; break;
        default: msg = 'gas cloud blows away'; break;
        }
        await pline(`But luckily the ${msg}!`);
    } else {
        const traptype = rn2(20) ? ((game.u?.Luck || 0) >= 13 ? 0 : rn2(13 - (game.u?.Luck || 0))) : rn2(26);
        switch (traptype) {
        case 25: case 24: case 23: case 22: case 21:
            // explosion
            await pline('It explodes!');
            await losehp(Maybe_Half_Phys(c_d(6, 6)), 'exploding chest', KILLED_BY_AN);
            exercise(A_STR, false);
            break;
        case 20: case 19: case 18: case 17:
            // poison gas
            await pline(`A cloud of noxious gas billows from ${the(xname(obj))}.`);
            if (rn2(3))
                await poisoned('gas cloud', A_STR, 'cloud of poison gas', 15, false);
            exercise(A_CON, false);
            break;
        case 16: case 15: case 14: case 13:
            // poisoned needle
            await You_feel(`a needle prick your ${body_part(bodypart)}.`);
            await poisoned('needle', A_CON, 'poisoned needle', 10, false);
            exercise(A_CON, false);
            break;
        case 12: case 11: case 10: case 9:
            // fire
            await dofiretrap(obj);
            break;
        case 8: case 7: case 6: {
            // electric shock
            const dmg = c_d(4, 4);
            const orig_dmg = dmg;
            await You('are jolted by a surge of electricity!');
            if (Shock_resistance()) {
                shieldeff(game.u?.ux, game.u?.uy);
                await You("don't seem to be affected.");
            } else {
                await losehp(dmg, 'electric shock', KILLED_BY_AN);
            }
            await destroy_items(game.u, AD_ELEC, orig_dmg);
            break;
        }
        case 5: case 4: case 3:
            // paralysis
            await pline('Suddenly you are frozen in place!');
            nomul(-c_d(5, 6));
            exercise(A_DEX, false);
            break;
        case 2: case 1: case 0:
            // stunning gas
            await pline(`A cloud of ${Blind() ? 'humid' : rndcolor()} gas billows from ${the(xname(obj))}.`);
            await make_stunned(rn1(7, 16), false);
            await make_hallucinated(rn1(5, 16), false, 0);
            break;
        default:
            break;
        }
    }
    obj.tknown = 1;
    return false;
}
 
// ── sokoban_guilt — luck penalty for sokoban misbehavior ──
// C ref: trap.c:7017
export function sokoban_guilt() {
    if (Sokoban()) {
        // C: u.uconduct.sokocheat++; change_luck(-1);
    }
}
 
// ── trap_ice_effects — handle traps when ice melts ──
// C ref: trap.c:7150
export function trap_ice_effects(x, y, ice_is_melting) {
    const ttmp = t_at(x, y);
    if (ttmp && ice_is_melting) {
        const mtmp = m_at(x, y);
        if (mtmp && mtmp.mtrapped)
            mtmp.mtrapped = 0;
        if (ttmp.ttyp === LANDMINE || ttmp.ttyp === BEAR_TRAP) {
            cnv_trap_obj(ttmp.ttyp === LANDMINE ? 0 : 0, 1, ttmp, true);
        } else if (!undestroyable_trap(ttmp.ttyp)) {
            deltrap(ttmp);
        }
    }
}
 
// ── lava_effects — hero enters lava ──
// C ref: trap.c:6772
// RNG: d(6,6) for water walking damage
export async function lava_effects() {
    const dmg = c_d(6, 6);
    await burn_away_slime();
    // simplified — full lava effects require extensive infrastructure
    if (Fire_resistance()) {
        return false;
    }
    await You(`fall into the lava!`);
    await losehp(dmg, 'molten lava', KILLED_BY);
    return false;
}
 
// ── sink_into_lava — called each turn when trapped in lava ──
// C ref: trap.c:6969
export async function sink_into_lava() {
    if (!game.u?.utrap || game.u?.utraptype !== TT_LAVA)
        return;
    if (!Fire_resistance())
        game.u.uhp = Math.trunc((game.u.uhp + 2) / 3);
    game.u.utrap -= (1 << 8);
    if (game.u.utrap < (1 << 8)) {
        await urgent_pline('You sink below the surface and die.');
        await burn_away_slime();
        await done('dissolved');
    }
}
 
// ── rnd_nextto_goodpos — find random adjacent safe position ──
// C ref: trap.c:4925
export async function rnd_nextto_goodpos(pos, mtmp) {
    const g = game;
    const is_u = (mtmp === g.youmonst);
    const dirs = [];
    for (let i = 0; i < N_DIRS; ++i) dirs[i] = i;
    // C ref: trap.c:4933-4937 — Fisher-Yates shuffle of directions
    for (let i = N_DIRS; i > 0; --i) {
        const j = rn2(i);
        const k = dirs[j];
        dirs[j] = dirs[i - 1];
        dirs[i - 1] = k;
    }
    for (let i = 0; i < N_DIRS; ++i) {
        const nx = pos.x + xdir[dirs[i]];
        const ny = pos.y + ydir[dirs[i]];
        if (is_u ? await crawl_destination(nx, ny) : goodpos(nx, ny, mtmp, 0)) {
            pos.x = nx;
            pos.y = ny;
            return true;
        }
    }
    return false;
}
 
// ── drown — hero falls into water ──
// C ref: trap.c:5037
// RNG: rn2(5) water effects, rn2(3) gremlin split, d(2,6) iron golem rust
export async function drown() {
    const g = game;
    const u = g.u;
    await You(`fall into the water!`);
    // C ref: trap.c:5123-5127 — wake up / recover from faint
    // (simplified: skip usleep/faint checks)

    // C ref: trap.c:5129-5147 — try to crawl out
    const pos = { x: u.ux, y: u.uy };
    if ((g.multi || 0) >= 0 && g.youmonst?.data?.mmove
        && await rnd_nextto_goodpos(pos, g.youmonst)) {
        // C ref: trap.c:5137-5146 — try to strip gear and crawl out
        await You('try to crawl out of the %s.', hliquid('water'));
        // emergency_disrobe not fully ported — simplified success
        await pline('Pheew!  That was close.');
        await teleds(pos.x, pos.y, TELEDS_ALLOW_DRAG);
        return true;
    }
    // C ref: trap.c:5148-5149 — drowning
    if (u.umonnum === PM_IRON_GOLEM) {
        await You('rust!');
        const i = Maybe_Half_Phys(c_d(2, 6));
        await losehp(i, 'rusting away', KILLED_BY);
    }
    return false;
}
 
// ── back_on_ground — message about landing ──
// C ref: trap.c:4954
// C ref: trap.c:4956 back_on_ground()
export async function back_on_ground(rescued) {
    let preposit = (Levitation() || Flying()) ? 'over' : 'on';
    let surf = surface(game.u?.ux, game.u?.uy);
    if (surf === 'floor' || surf === 'ground') {
        surf = 'solid ground';
    } else if (surf === 'bridge' || surf === 'altar' || surf === 'headstone') {
        surf = `a ${surf}`;
    } else if (surf === 'stairs' || surf === 'lava' || surf === 'bottom') {
        surf = `the ${surf}`;
    } else if (surf === 'air') {
        surf = `the ${surf}`;
        preposit = 'in';
    } else {
        surf = `a ${surf}`;
        preposit = 'in';
    }
    if (rescued)
        await pline(`You find yourself ${preposit} ${surf}.`);
    else
        await pline(`You are back ${preposit} ${surf}.`);
}
 
// ── launch_in_progress — is a launch currently happening? ──
// C ref: trap.c:3233
export function launch_in_progress() {
    return false; // simplified
}
 
// ── force_launch_placement — force place launched object ──
// C ref: trap.c:3241
export function force_launch_placement() {
    // simplified
}
 
// ── conjoined_pits / adj_nonconjoined_pit / clear_conjoined_pits ──
function conjoined_pits(_trap, _otrap, _u_entering) { return false; }
function adj_nonconjoined_pit(_trap) { return false; }
function clear_conjoined_pits(_trap) { /* TODO */ }
 
// ── trapname — get display name for a trap type ──
// C ref: trap.c:7078
export function trapname(ttype, _force) {
    const names = {
        [ARROW_TRAP]: 'arrow trap',
        [DART_TRAP]: 'dart trap',
        [ROCKTRAP]: 'falling rock trap',
        [SQKY_BOARD]: 'squeaky board',
        [BEAR_TRAP]: 'bear trap',
        [SLP_GAS_TRAP]: 'sleeping gas trap',
        [RUST_TRAP]: 'rust trap',
        [FIRE_TRAP]: 'fire trap',
        [PIT]: 'pit',
        [SPIKED_PIT]: 'spiked pit',
        [HOLE]: 'hole',
        [TRAPDOOR]: 'trapdoor',
        [TELEP_TRAP]: 'teleportation trap',
        [LEVEL_TELEP]: 'level teleporter',
        [WEB]: 'web',
        [STATUE_TRAP]: 'statue trap',
        [MAGIC_TRAP]: 'magic trap',
        [ANTI_MAGIC]: 'anti-magic field',
        [POLY_TRAP]: 'polymorph trap',
        [LANDMINE]: 'land mine',
        [ROLLING_BOULDER_TRAP]: 'rolling boulder trap',
        [MAGIC_PORTAL]: 'magic portal',
        [VIBRATING_SQUARE]: 'vibrating square',
    };
    if (Hallucination() && !_force) {
        return 'trap';
    }
    return names[ttype] || 'trap';
}
 
// ── delfloortrap — remove a floor trap ──
// C ref: trap.c:6646
export function delfloortrap(ttmp) {
    if (!ttmp) return true;
    const level = game.level;
    if (level?.traps) {
        const idx = level.traps.indexOf(ttmp);
        if (idx >= 0) level.traps.splice(idx, 1);
    }
    const loc = levl(ttmp.tx, ttmp.ty);
    if (loc) {
        loc.trap = null;
    }
    newsym(ttmp.tx, ttmp.ty);
    return true;
}
 
// ── b_trapped — triggered a booby trap on a chest/door ──
// C ref: trap.c:6672
// RNG: rn1(4, 2) for damage = 2-5
export async function b_trapped(item, bodypart) {
    const dmg = rn1(4, 2);
    await pline('KABOOM!!  You triggered a booby trap!');
    await losehp(Maybe_Half_Phys(dmg), 'booby trap', KILLED_BY_AN);
    exercise(A_STR, false);
    exercise(A_CON, false);
}
 
// ── t_missile — create a trap projectile ──
// C ref: trap.c:1020
function t_missile(otyp, trap) {
    const otmp = mksobj(otyp, true, false);
    otmp.quan = 1;
    otmp.owt = weight(otmp);
    otmp.opoisoned = 0;
    otmp.ox = trap.tx;
    otmp.oy = trap.ty;
    return otmp;
}
 
// ── set_utrap / reset_utrap ──
// C ref: trap.c:1032, 1047
// C ref: trap.c:1032
export function set_utrap(tim, typ) {
    const u = game.u;
    // C ref: trap.c:1037-1038 — SET_BOTL if trap state changes
    if (!u.utrap ^ !tim)
        setBotl('set_utrap');
    u.utrap = tim;
    u.utraptype = tim ? typ : TT_NONE;
    // C ref: trap.c:1043 — float_vs_flight may block Lev/Fly
    float_vs_flight();
}
 
// C ref: trap.c:1047
export async function reset_utrap(msg) {
    const was_Lev = !!Levitation();
    const was_Fly = !!Flying();
    set_utrap(0, 0);
    if (msg) {
        if (!was_Lev && Levitation()) await float_up();
        if (!was_Fly && Flying()) { /* You("can fly."); */ }
    }
}
 
// float_vs_flight: imported from polyself.js
 
// float_up already defined above (line 817)
 
// ── seetrap / feeltrap ──
// C ref: trap.c:3557, 3567
export function seetrap(trap) {
    if (!trap.tseen) {
        trap.tseen = true;
        newsym(trap.tx, trap.ty);
    }
}
 
export function feeltrap(trap) {
    trap.tseen = true;
    map_trap(trap, 1);
    newsym(trap.tx, trap.ty);
}
 
// ── floor_trigger — is trap type triggered by touching the floor? ──
// C ref: trap.c:1062
function floor_trigger(ttype) {
    switch (ttype) {
    case ARROW_TRAP: case DART_TRAP: case ROCKTRAP: case SQKY_BOARD:
    case BEAR_TRAP: case LANDMINE: case ROLLING_BOULDER_TRAP:
    case SLP_GAS_TRAP: case RUST_TRAP: case FIRE_TRAP:
    case PIT: case SPIKED_PIT: case HOLE: case TRAPDOOR:
        return true;
    default:
        return false;
    }
}
 
// C ref: trap.c:unknown — undestroyable_trap
export function undestroyable_trap(ttyp) {
    return ttyp === MAGIC_PORTAL || ttyp === VIBRATING_SQUARE;
}
 
// ── dotrap — hero steps on a trap ──
// C ref: trap.c:2994
// RNG: rn2(5) escape check, then trap-specific effects
export async function dotrap(trap, trflags) {
    if (!trap) return;
    trflags = trflags || 0;
    const ttype = trap.ttyp;
    const already_seen = trap.tseen;
    let forcetrap = ((trflags & FORCETRAP) !== 0
                     || (trflags & FAILEDUNTRAP) !== 0);
    const forcebungle = (trflags & FORCEBUNGLE) !== 0;
    const plunged = (trflags & TOOKPLUNGE) !== 0;
    const conj_pit = conjoined_pits(trap, t_at(game.u?.ux0, game.u?.uy0), true);
    const adj_pit = adj_nonconjoined_pit(trap);
 
    nomul(0);
 
    // C: fixed_tele_trap check — TODO
    // if (fixed_tele_trap(trap)) { trflags |= FORCETRAP; forcetrap = true; }
 
    // Sokoban pit/hole forcing
    if (Sokoban() && (is_pit(ttype) || is_hole(ttype))) {
        await pline(`Air currents pull you down into ${trap.madeby_u ? 'your' : 'a'} ${trapname(ttype, true)}!`);
    } else if (!forcetrap) {
        if (floor_trigger(ttype) && check_in_air(trflags)) {
            if (already_seen) {
                await You(`step over ${trap.madeby_u ? 'your' : (ttype === ARROW_TRAP ? 'an' : 'a')} ${trapname(ttype, false)}.`);
            }
            return;
        }
        if (already_seen && !Fumbling() && !undestroyable_trap(ttype)
            && ttype !== ANTI_MAGIC && !forcebungle && !plunged
            && !conj_pit && !adj_pit
            && (!rn2(5) || (is_pit(ttype) && is_clinger(game.u?.data)))) {
            await You(`escape ${trap.madeby_u ? 'your' : (ttype === ARROW_TRAP ? 'an' : 'a')} ${trapname(ttype, false)}.`);
            return;
        }
    }
 
    // C: mon_learns_traps, mons_see_trap — TODO
 
    // Update remembered glyph for trap visibility
    trap.tseen = true;
    const trapLoc = game.level?.at(trap.tx, trap.ty);
    if (trapLoc) {
        const symIdx = S_arrow_trap + trap.ttyp - 1;
        // gbuf-cleanup Phase 6b: memory derived from loc.glyph via
        // map_glyphinfo at render time; store just the canonical int.
        trapLoc.glyph = trap_to_glyph(symIdx);
    }
 
    // Dispatch to trap-specific effect
    await trapeffect_selector_hero(trap, trflags);
}
 
// ── trapeffect_selector for hero (mtmp == youmonst) ──
async function trapeffect_selector_hero(trap, trflags) {
    switch (trap.ttyp) {
    case ARROW_TRAP:
    case DART_TRAP:
        await trapeffect_arrow_dart(trap, trflags);
        break;
    case ROCKTRAP:
        await trapeffect_rocktrap(trap, trflags);
        break;
    case SQKY_BOARD:
        await trapeffect_sqky_board(trap, trflags);
        break;
    case BEAR_TRAP:
        await trapeffect_bear_trap(trap, trflags);
        break;
    case SLP_GAS_TRAP:
        await trapeffect_slp_gas_trap(trap, trflags);
        break;
    case RUST_TRAP:
        await trapeffect_rust_trap(trap, trflags);
        break;
    case FIRE_TRAP:
        await trapeffect_fire_trap(trap, trflags);
        break;
    case PIT:
    case SPIKED_PIT:
        await trapeffect_pit(trap, trflags);
        break;
    case HOLE:
    case TRAPDOOR:
        await trapeffect_hole(trap, trflags);
        break;
    case TELEP_TRAP:
        await trapeffect_telep(trap, trflags);
        break;
    case LEVEL_TELEP:
        await trapeffect_level_telep_hero(trap, trflags);
        break;
    case WEB:
        await trapeffect_web(trap, trflags);
        break;
    case STATUE_TRAP:
        await trapeffect_statue_trap(trap, trflags);
        break;
    case MAGIC_TRAP:
        await trapeffect_magic_trap(trap, trflags);
        break;
    case ANTI_MAGIC:
        await trapeffect_anti_magic(trap, trflags);
        break;
    case POLY_TRAP:
        await trapeffect_poly_trap(trap, trflags);
        break;
    case LANDMINE:
        await trapeffect_landmine(trap, trflags);
        break;
    case ROLLING_BOULDER_TRAP:
        await trapeffect_rolling_boulder_trap(trap, trflags);
        break;
    case MAGIC_PORTAL:
        await trapeffect_magic_portal_hero(trap, trflags);
        break;
    case VIBRATING_SQUARE:
        await trapeffect_vibrating_square(trap, trflags);
        break;
    default:
        await pline(`A ${trapname(trap.ttyp, false)}!`);
        break;
    }
}
 
// ── mintrap — monster steps on a trap ──
// C ref: trap.c:3712
// RNG: rn2(40) escape check, per-type dispatch
export async function mintrap(mtmp, mintrapflags) {
    if (!mtmp) return 0;
    mintrapflags = mintrapflags || 0;
    const trap = t_at(mtmp.mx, mtmp.my);
    let trap_result = Trap_Effect_Finished;
 
    if (!trap) {
        mtmp.mtrapped = 0;
    } else if (mtmp.mtrapped) {
        // Monster is currently in the trap
        if (!trap.tseen && cansee(mtmp.mx, mtmp.my) && canseemon(mtmp)
            && (is_pit(trap.ttyp) || trap.ttyp === BEAR_TRAP
                || trap.ttyp === HOLE || trap.ttyp === WEB)) {
            seetrap(trap);
        }
 
        if (!rn2(40) || (is_pit(trap.ttyp) && m_easy_escape_pit(mtmp))) {
            if (sobj_at(BOULDER, mtmp.mx, mtmp.my) && is_pit(trap.ttyp)) {
                if (!rn2(2)) {
                    mtmp.mtrapped = 0;
                    if (canseemon(mtmp))
                        await pline(`${Monnam(mtmp)} pulls free...`);
                    fill_pit(mtmp.mx, mtmp.my);
                }
            } else {
                if (canseemon(mtmp)) {
                    if (is_pit(trap.ttyp))
                        await pline(`${Monnam(mtmp)} climbs ${m_easy_escape_pit(mtmp) ? 'easily ' : ''}out of the pit.`);
                    else if (trap.ttyp === BEAR_TRAP || trap.ttyp === WEB)
                        await pline(`${Monnam(mtmp)} pulls free of the ${trapname(trap.ttyp, false)}.`);
                }
                mtmp.mtrapped = 0;
            }
        } else if (metallivorous(mtmp.data)) {
            if (trap.ttyp === BEAR_TRAP) {
                if (canseemon(mtmp))
                    await pline(`${Monnam(mtmp)} eats a bear trap!`);
                deltrap(trap);
                mtmp.meating = 5;
                mtmp.mtrapped = 0;
            } else if (trap.ttyp === SPIKED_PIT) {
                if (canseemon(mtmp))
                    await pline(`${Monnam(mtmp)} munches on some spikes!`);
                trap.ttyp = PIT;
                mtmp.meating = 5;
            }
        }
        trap_result = mtmp.mtrapped ? Trap_Caught_Mon : Trap_Effect_Finished;
    } else {
        // Monster encounters trap for the first time
        const tt = trap.ttyp;
        let forcetrap = ((mintrapflags & FORCETRAP) !== 0);
        const forcebungle = (mintrapflags & FORCEBUNGLE) !== 0;
        const already_seen = (mon_knows_traps(mtmp, tt)
                              || (tt === HOLE && !mindless(mtmp.data)));
 
        // C: fixed_tele_trap — TODO
 
        if (!forcetrap) {
            if (floor_trigger(tt) && m_in_air(mtmp)) {
                return Trap_Effect_Finished;
            }
            if (already_seen && rn2(4) && !forcebungle)
                return Trap_Effect_Finished;
        }
 
        // C ref: trap.c:3794-3795 — monster learns about this trap type
        mon_learns_traps(mtmp, tt);
        // mons_see_trap — TODO (nearby monsters also learn)
 
        if (trap.madeby_u && rnl(5))
            await setmangry(mtmp, false);
 
        trap_result = await trapeffect_selector_mon(mtmp, trap, mintrapflags);
 
        // unhide trapped monster
        if (mtmp.mhp > 0 && mtmp.mtrapped) {
            // C: maybe_unhide_at — simplified
        }
    }
    return trap_result;
}
 
// ── trapeffect_selector for monsters ──
async function trapeffect_selector_mon(mtmp, trap, trflags) {
    const in_sight = canseemon(mtmp);
    const see_it = cansee(mtmp.mx, mtmp.my);
    let trapkilled = false;
 
    switch (trap.ttyp) {
    case ARROW_TRAP:
    case DART_TRAP: {
        const isArrow = trap.ttyp === ARROW_TRAP;
        if (trap.once && trap.tseen && !rn2(15)) {
            if (in_sight && see_it)
                await pline(`${Monnam(mtmp)} triggers a trap but nothing happens.`);
            deltrap(trap);
            newsym(mtmp.mx, mtmp.my);
            return Trap_Is_Gone;
        }
        trap.once = true;
        const otmp = t_missile(isArrow ? ARROW : DART, trap);
        if (!isArrow && !rn2(6))
            otmp.opoisoned = 1;
        if (in_sight)
            seetrap(trap);
        if (await thitm(isArrow ? 8 : 7, mtmp, otmp, 0, false))
            trapkilled = true;
        return trapkilled ? Trap_Killed_Mon : mtmp.mtrapped
            ? Trap_Caught_Mon : Trap_Effect_Finished;
    }
    case ROCKTRAP: {
        if (trap.once && trap.tseen && !rn2(15)) {
            if (in_sight && see_it)
                await pline(`A trap door above ${mon_nam(mtmp)} opens, but nothing falls out!`);
            deltrap(trap);
            newsym(mtmp.mx, mtmp.my);
            return Trap_Is_Gone;
        }
        trap.once = true;
        const otmp = t_missile(ROCK, trap);
        if (in_sight) seetrap(trap);
        if (await thitm(0, mtmp, otmp, c_d(2, 6), false))
            trapkilled = true;
        return trapkilled ? Trap_Killed_Mon : mtmp.mtrapped
            ? Trap_Caught_Mon : Trap_Effect_Finished;
    }
    case SQKY_BOARD:
        if (m_in_air(mtmp)) return Trap_Effect_Finished;
        if (in_sight) {
            if (!Deaf()) {
                await pline(`A board beneath ${mon_nam(mtmp)} squeaks ${trapnote(trap, false)} loudly.`);
                seetrap(trap);
            } else if (!mindless(mtmp.data)) {
                await pline(`${Monnam(mtmp)} stops momentarily and appears to cringe.`);
            }
        } else {
            // C ref: trap.c:1458 — near/far threshold like mzapmsg
            const range = couldsee(mtmp.mx, mtmp.my) ? (BOLT_LIM + 1) : (BOLT_LIM - 3);
            await You_hear(`${trapnote(trap, false)} squeak ${(mdistu(mtmp) <= range * range) ? 'nearby' : 'in the distance'}.`);
        }
        wake_nearto(mtmp.mx, mtmp.my, 40);
        return Trap_Effect_Finished;
 
    case BEAR_TRAP: {
        const mptr = mtmp.data;
        if (mptr && mptr.msize > MZ_SMALL && !amorphous(mptr) && !m_in_air(mtmp)
            && !is_whirly(mptr) && !unsolid(mptr)) {
            mtmp.mtrapped = 1;
            if (in_sight) {
                await pline(`${Monnam(mtmp)} is caught in ${trap.madeby_u ? 'your' : 'a'} bear trap!`);
                seetrap(trap);
            } else {
                if (monsndx(mptr) === PM_OWLBEAR || monsndx(mptr) === PM_BUGBEAR)
                    await You_hear('the roaring of an angry bear!');
            }
        }
        if (mtmp.mtrapped && !wearing_iron_shoes(mtmp)) {
            if (await thitm(0, mtmp, null, c_d(2, 4), false))
                trapkilled = true;
        }
        return trapkilled ? Trap_Killed_Mon : mtmp.mtrapped
            ? Trap_Caught_Mon : Trap_Effect_Finished;
    }
    case SLP_GAS_TRAP:
        if (!resists_sleep(mtmp) && !breathless(mtmp.data)
            && !helpless(mtmp)) {
            if (sleep_monst(mtmp, rnd(25), -1) && in_sight) {
                await pline(`${Monnam(mtmp)} suddenly falls asleep!`);
                seetrap(trap);
            }
        }
        return Trap_Effect_Finished;
 
    case RUST_TRAP: {
        if (in_sight) seetrap(trap);
        const rcase = rn2(5);
        if (in_sight) {
            if (rcase <= 2)
                await pline(`A gush of water hits ${mon_nam(mtmp)} on the head!`);
            else
                await pline(`A gush of water hits ${mon_nam(mtmp)}!`);
        }
        if (completelyrusts(mtmp.data)) {
            if (in_sight) await pline(`${Monnam(mtmp)} falls to pieces!`);
            await monkilled(mtmp, null, AD_RUST);
            if (mtmp.mhp <= 0) trapkilled = true;
        } else if (monsndx(mtmp.data) === PM_GREMLIN && rn2(3)) {
            // C: split_mon — TODO
        }
        return trapkilled ? Trap_Killed_Mon : mtmp.mtrapped
            ? Trap_Caught_Mon : Trap_Effect_Finished;
    }
    case FIRE_TRAP: {
        const orig_dmg = c_d(2, 4);
        if (in_sight) await pline(`A tower of flame erupts from the floor under ${mon_nam(mtmp)}!`);
        if (resists_fire(mtmp)) {
            if (in_sight) {
                shieldeff(mtmp.mx, mtmp.my);
                await pline(`${Monnam(mtmp)} is uninjured.`);
            }
        } else {
            let num = orig_dmg;
            const idx = monsndx(mtmp.data);
            if (idx === PM_PAPER_GOLEM) num = Math.max(num, mtmp.mhpmax || 1);
            else if (idx === PM_STRAW_GOLEM) num = Math.max(num, Math.trunc((mtmp.mhpmax || 1) / 2));
            else if (idx === PM_WOOD_GOLEM) num = Math.max(num, Math.trunc((mtmp.mhpmax || 1) / 4));
            else if (idx === PM_LEATHER_GOLEM) num = Math.max(num, Math.trunc((mtmp.mhpmax || 1) / 8));
 
            mtmp.mhp = (mtmp.mhp || 0) - num;
            if (mtmp.mhp <= 0) {
                await monkilled(mtmp, '', AD_FIRE);
                trapkilled = true;
            } else {
                mtmp.mhpmax = (mtmp.mhpmax || 0) - rn2(num + 1);
                if (mtmp.mhp > mtmp.mhpmax) mtmp.mhp = mtmp.mhpmax;
            }
        }
        if (see_it && t_at(mtmp.mx, mtmp.my)) seetrap(t_at(mtmp.mx, mtmp.my));
        return trapkilled ? Trap_Killed_Mon : mtmp.mtrapped
            ? Trap_Caught_Mon : Trap_Effect_Finished;
    }
    case PIT:
    case SPIKED_PIT: {
        const mptr = mtmp.data;
        const forcetrap = ((trflags & FORCETRAP) !== 0);
        const inescapable = forcetrap || (Sokoban() && !trap.madeby_u);
        const relevant_spikes = trap.ttyp === SPIKED_PIT && !wearing_iron_shoes(mtmp);
 
        if (!grounded(mptr)) {
            if (!inescapable) return Trap_Effect_Finished;
        }
        if (!passes_walls(mptr)) mtmp.mtrapped = 1;
        if (in_sight) {
            await pline(`${Monnam(mtmp)} falls into ${trap.madeby_u ? 'your' : 'a'} pit!`);
            if (monsndx(mptr) === PM_PIT_VIPER || monsndx(mptr) === PM_PIT_FIEND)
                await pline("How pitiful.  Isn't that the pits?");
            seetrap(trap);
        }
        // C ref: trap.c:2000-2002 — use thitm for damage (calls monkilled on death)
        if (DEADMONSTER(mtmp) || await thitm(0, mtmp, null, rnd(relevant_spikes ? 10 : 6), false))
            trapkilled = true;
 
        return trapkilled ? Trap_Killed_Mon : mtmp.mtrapped
            ? Trap_Caught_Mon : Trap_Effect_Finished;
    }
    case HOLE:
    case TRAPDOOR: {
        const mptr = mtmp.data;
        const forcetrap = ((trflags & FORCETRAP) !== 0);
        const inescapable = forcetrap || (Sokoban() && !trap.madeby_u);
        if (!grounded(mptr) || (mptr && mptr.msize >= MZ_HUGE)) {
            if (!inescapable) return Trap_Effect_Finished;
            if (in_sight) {
                await pline(`${Monnam(mtmp)} seems to be yanked down!`);
                seetrap(trap);
            }
        }
        return await mlevel_tele_trap(mtmp, trap, ((trflags & FORCETRAP) !== 0), in_sight);
    }
    case TELEP_TRAP:
        mtele_trap(mtmp, trap, in_sight);
        return Trap_Moved_Mon;
 
    case LEVEL_TELEP:
        return await mlevel_tele_trap(mtmp, trap, ((trflags & FORCETRAP) !== 0), in_sight);
 
    case MAGIC_PORTAL:
        return await mlevel_tele_trap(mtmp, trap, ((trflags & FORCETRAP) !== 0), in_sight);
 
    case WEB: {
        const mptr = mtmp.data;
        if (webmaker(mptr)) return Trap_Effect_Finished;
        if (await mu_maybe_destroy_web(mtmp, in_sight, trap))
            return Trap_Effect_Finished;
        let tear_web = false;
        const idx = monsndx(mptr);
        if (idx === PM_TITANOTHERE || idx === PM_BALUCHITHERIUM
            || idx === PM_PURPLE_WORM || idx === PM_JABBERWOCK
            || idx === PM_IRON_GOLEM || idx === PM_BALROG
            || idx === PM_KRAKEN || idx === PM_MASTODON
            || idx === PM_ORION || idx === PM_NORN
            || idx === PM_CYCLOPS || idx === PM_LORD_SURTUR) {
            tear_web = true;
        } else if (mptr && (mptr.mlet === S_GIANT
            || (mptr.mlet === S_DRAGON && (mptr.mflags2 & 0x02000000)))) { // M2_NASTY
            tear_web = true;
        } else {
            if (in_sight) {
                await pline(`${Monnam(mtmp)} is caught in ${trap.madeby_u ? 'your' : 'a'} spider web.`);
                seetrap(trap);
            }
            mtmp.mtrapped = 1;
        }
        if (tear_web) {
            if (in_sight)
                await pline(`${Monnam(mtmp)} tears through ${trap.madeby_u ? 'your' : 'a'} spider web!`);
            deltrap(trap);
            newsym(mtmp.mx, mtmp.my);
        }
        return mtmp.mtrapped ? Trap_Caught_Mon : Trap_Effect_Finished;
    }
    case STATUE_TRAP:
        return Trap_Effect_Finished;
 
    case MAGIC_TRAP:
        if (!rn2(21))
            return await trapeffect_selector_mon(mtmp, { ...trap, ttyp: FIRE_TRAP }, trflags);
        return Trap_Effect_Finished;
 
    case ANTI_MAGIC: {
        if (!resists_magm(mtmp)) {
            if (!mtmp.mcan) {
                mtmp.mspec_used = (mtmp.mspec_used || 0) + c_d(2, 6);
                if (in_sight) {
                    seetrap(trap);
                    await pline(`${Monnam(mtmp)} seems lethargic.`);
                }
            }
        } else {
            let dmgval2 = rnd(4);
            if (passes_walls(mtmp.data))
                dmgval2 = Math.trunc((dmgval2 + 3) / 4);
            if (in_sight) seetrap(trap);
            mtmp.mhp = (mtmp.mhp || 0) - dmgval2;
            if (mtmp.mhp <= 0) {
                await monkilled(mtmp, in_sight ? 'compression from an anti-magic field' : null, -10);
                if (mtmp.mhp <= 0) trapkilled = true;
            }
        }
        return trapkilled ? Trap_Killed_Mon : mtmp.mtrapped
            ? Trap_Caught_Mon : Trap_Effect_Finished;
    }
    case POLY_TRAP: {
        if (wearing_iron_shoes(mtmp)) {
            // iron shoes get polymorphed — simplified
        } else if (resists_magm(mtmp)) {
            // shieldeff
        } else if (!resist(mtmp, WAND_CLASS, 0, NOTELL)) {
            await newcham(mtmp, null, 0);
            if (in_sight) seetrap(trap);
        }
        return Trap_Effect_Finished;
    }
    case LANDMINE: {
        let damage = rnd(16);
        if (wearing_iron_shoes(mtmp))
            damage = Math.trunc((damage + 3) / 4);
        if (m_in_air(mtmp)) {
            if (rn2(3)) return Trap_Effect_Finished;
            if (in_sight) {
                await pline('The air currents set it off!');
            }
        } else if (in_sight) {
            await pline(`KAABLAMM!!!  ${Monnam(mtmp)} triggers ${trap.madeby_u ? 'your' : 'a'} land mine!`);
        }
        if (!in_sight && !Deaf())
            await pline('Kaablamm!  You hear an explosion in the distance!');
        blow_up_landmine(trap);
        mtmp.mhp = (mtmp.mhp || 0) - damage;
        if (mtmp.mhp <= 0) {
            trapkilled = true;
        } else {
            if (await mintrap(mtmp, trflags | FORCETRAP) === Trap_Killed_Mon)
                trapkilled = true;
        }
        fill_pit(mtmp.mx, mtmp.my);
        if (mtmp.mhp <= 0) trapkilled = true;
        return trapkilled ? Trap_Killed_Mon : mtmp.mtrapped
            ? Trap_Caught_Mon : Trap_Effect_Finished;
    }
    case ROLLING_BOULDER_TRAP:
        if (!m_in_air(mtmp)) {
            newsym(mtmp.mx, mtmp.my);
            if (in_sight)
                await pline(`${Monnam(mtmp)} triggers a rolling boulder trap!`);
            const launched = await launch_obj(BOULDER, trap.launch?.x, trap.launch?.y,
                                       trap.launch2?.x, trap.launch2?.y, ROLL);
            if (!launched) {
                deltrap(trap);
                newsym(mtmp.mx, mtmp.my);
            } else {
                if (in_sight) trap.tseen = true;
                if (mtmp.mhp <= 0) trapkilled = true;
            }
        }
        return trapkilled ? Trap_Killed_Mon : mtmp.mtrapped
            ? Trap_Caught_Mon : Trap_Effect_Finished;
 
    case VIBRATING_SQUARE:
        if (see_it && !Blind()) {
            seetrap(trap);
            if (in_sight) {
                await You_see(`a strange vibration beneath ${mon_nam(mtmp)}.`);
            } else {
                await You_see('the ground vibrate in the distance.');
            }
        }
        return Trap_Effect_Finished;
 
    default:
        return Trap_Effect_Finished;
    }
}
 
// ============================================================================
// Hero trap effect implementations
// ============================================================================
 
// Arrow/dart trap — hero
// C ref: trap.c:1192 (arrow), 1251 (dart)
// RNG: rn2(15) empty check, t_missile creates object (mksobj RNG),
//      rn2(6) dart poison
async function trapeffect_arrow_dart(trap, trflags) {
    const isArrow = trap.ttyp === ARROW_TRAP;
    const missileName = isArrow ? 'arrow' : 'little dart';
    if (trap.once && trap.tseen && !rn2(15)) {
        if (isArrow) {
            await You_hear('a loud click!');
        } else {
            await You_hear('a soft click.');
        }
        deltrap(trap);
        newsym(game.u.ux, game.u.uy);
        return;
    }
    trap.once = true;
    seetrap(trap);
    if (isArrow) {
        await pline('An arrow shoots out at you!');
    } else {
        await pline('A little dart shoots out at you!');
    }
    const oldumort = game.u.umortality;
    let otmp = t_missile(isArrow ? ARROW : DART, trap);
    if (!isArrow && !rn2(6))
        otmp.opoisoned = 1;
    if (game.u.usteed && !rn2(2) && steedintrap(trap, otmp)) {
        return;
    }
 
    const otmpRef = [otmp];
    // C ref: trap.c:1277 — midlog wrapper around thitu call
    pushRngLogEntry('>thitu');
    const hit = await thitu(isArrow ? 8 : 7, dmgval(otmp, game.youmonst), otmpRef, missileName);
    pushRngLogEntry(`<thitu=${hit ? 1 : 0}`);
    otmp = otmpRef[0];
    if (hit) {
        if (otmp) {
            if (!isArrow && otmp.opoisoned) {
                // Match trap.c: poison damage is suppressed to attrib-only if life-saving triggered.
                await poisoned('dart', A_CON, 'little dart',
                    (game.u.umortality > oldumort) ? 0 : 10, true);
            }
            obfree(otmp, null);
        }
    } else if (otmp) {
        place_object(otmp, game.u.ux, game.u.uy);
        if (!Blind()) observe_object(otmp);
        stackobj(otmp);
        newsym(game.u.ux, game.u.uy);
    }
}
 
// Rocktrap — hero
// C ref: trap.c:1323
// RNG: rn2(15) empty, d(2,6) damage, t_missile
async function trapeffect_rocktrap(trap, trflags) {
    if (trap.once && trap.tseen && !rn2(15)) {
        await pline(`A trap door in ${ceiling(game.u.ux, game.u.uy)} opens, but nothing falls out!`);
        deltrap(trap);
        newsym(game.u.ux, game.u.uy);
        return;
    }
    const dmg = c_d(2, 6);
    trap.once = true;
    feeltrap(trap);
    const otmp = t_missile(ROCK, trap);
    place_object(otmp, game.u.ux, game.u.uy);
    await pline(`A trap door in ${ceiling(game.u.ux, game.u.uy)} opens and a rock falls on your ${body_part(HEAD)}!`);
    if (!Blind()) observe_object(otmp);
    stackobj(otmp);
    newsym(game.u.ux, game.u.uy);
    await losehp(Maybe_Half_Phys(dmg), 'falling rock', KILLED_BY_AN);
    exercise(A_STR, false);
}
 
// Squeaky board — hero
// C ref: trap.c:1402
// RNG: none for hero (just messages + wake monsters)
async function trapeffect_sqky_board(trap, trflags) {
    const forcetrap = ((trflags & FORCETRAP) !== 0
                       || (trflags & FAILEDUNTRAP) !== 0);
    if ((Levitation() || Flying()) && !forcetrap) {
        if (!Blind()) {
            seetrap(trap);
            if (Hallucination())
                await You('notice a crease in the linoleum.');
            else
                await You('notice a loose board below you.');
        }
    } else {
        seetrap(trap);
        await pline(`A board beneath you ${Deaf() ? 'vibrates' : 'squeaks '}${Deaf() ? '' : trapnote(trap, false)}${Deaf() ? '' : ' loudly'}.`);
        wake_nearby(false);
    }
}
 
// Pit / spiked pit — hero
// C ref: trap.c:1824
// RNG: rn1(6,2) duration, rnd(10 or 6) damage, rn2(5) stumble msg,
//      rnd(4/6/10) spike damage, rn2(6) poison
async function trapeffect_pit(trap, trflags) {
    const ttype = trap.ttyp;
    const spiked = ttype === SPIKED_PIT;
    let relevant_spikes = spiked;
    const plunged = (trflags & TOOKPLUNGE) !== 0;
    const conj_pit = conjoined_pits(trap, t_at(game.u?.ux0, game.u?.uy0), true);
    const adj_pit = adj_nonconjoined_pit(trap);
 
    if (!Sokoban() && (Levitation() || (Flying() && !plunged)))
        return;
    feeltrap(trap);
    if (!Sokoban() && is_clinger(game.u?.data) && !plunged) {
        if (trap.tseen) {
            await You(`see ${trap.madeby_u ? 'your' : 'a'} ${spiked ? 'spiked ' : ''}pit below you.`);
        } else {
            await pline(`${trap.madeby_u ? 'Your' : 'A'} pit ${spiked ? 'full of spikes ' : ''}opens up under you!`);
            await You("don't fall in!");
        }
        return;
    }
    if (!Sokoban()) {
        if (conj_pit) {
            await You('move into an adjacent pit.');
        } else if (adj_pit) {
            await You(`stumble over debris${!rn2(5) ? ' between the pits' : ''}.`);
        } else {
            const verb = !plunged ? 'fall' : (Flying() ? 'dive' : 'plunge');
            await You(`${verb} into ${trap.madeby_u ? 'your' : 'a'} pit!`);
        }
    }
 
    if (relevant_spikes && wearing_iron_shoes(game.u)) {
        relevant_spikes = false;
    }
 
    set_utrap(rn1(6, 2), TT_PIT);
    if (!steedintrap(trap, null)) {
        if (relevant_spikes) {
            const dmg = rnd(conj_pit ? 4 : adj_pit ? 6 : 10);
            await losehp(Maybe_Half_Phys(dmg), 'fell into a pit of iron spikes', NO_KILLER_PREFIX);
            if (!rn2(6)) {
                await poisoned('spikes', A_STR, 'fell onto a set of iron spikes', 8, false);
            }
        } else {
            if (!conj_pit) {
                const dmg = rnd(adj_pit ? 3 : 6);
                await losehp(Maybe_Half_Phys(dmg),
                    plunged ? 'deliberately plunged into a pit' : 'fell into a pit',
                    NO_KILLER_PREFIX);
            }
        }
        exercise(A_STR, false);
        exercise(A_DEX, false);
    }
    // C ref: trap.c:1960 — vision limits change when hero falls into pit
    game.vision_full_recalc = 1;
}
 
// Bear trap — hero
// C ref: trap.c:1478
// RNG: d(2,4) damage, rn1(4,4) duration, rn2(2) wounded leg side
async function trapeffect_bear_trap(trap, trflags) {
    const forcetrap = ((trflags & FORCETRAP) !== 0
                       || (trflags & FAILEDUNTRAP) !== 0);
    const dmg = c_d(2, 4);
 
    if ((Levitation() || Flying()) && !forcetrap)
        return;
    feeltrap(trap);
    if (amorphous(game.u?.data) || is_whirly(game.u?.data)
        || unsolid(game.u?.data)) {
        await pline(`${trap.madeby_u ? 'Your' : 'A'} bear trap closes harmlessly through you.`);
        return;
    }
    if (game.u?.data?.msize !== undefined && game.u.data.msize <= MZ_SMALL) {
        await pline(`${trap.madeby_u ? 'Your' : 'A'} bear trap closes harmlessly over you.`);
        return;
    }
    set_utrap(rn1(4, 4), TT_BEARTRAP);
    await pline(`${trap.madeby_u ? 'Your' : 'A'} bear trap closes on your ${body_part(FOOT)}!`);
    if (game.u?.umonnum === PM_OWLBEAR || game.u?.umonnum === PM_BUGBEAR)
        await You('howl in anger!');
    if (!wearing_iron_shoes(game.u)) {
        await set_wounded_legs(rn2(2) ? RIGHT_SIDE : LEFT_SIDE, rn1(10, 10));
        await losehp(Maybe_Half_Phys(dmg), 'bear trap', KILLED_BY_AN);
    }
    exercise(A_DEX, false);
}
 
// Sleep gas trap — hero
// C ref: trap.c:1562
// RNG: rnd(25) sleep duration
async function trapeffect_slp_gas_trap(trap, trflags) {
    seetrap(trap);
    if (Sleep_resistance() || breathless(game.u?.data)) {
        await You('are enveloped in a cloud of gas!');
    } else {
        await pline('A cloud of gas puts you to sleep!');
        await fall_asleep(-rnd(25), true);
    }
    steedintrap(trap, null);
}
 
// Rust trap — hero
// C ref: trap.c:1594
// RNG: rn2(5) body part selection, rn2(3) gremlin split
async function trapeffect_rust_trap(trap, trflags) {
    seetrap(trap);
    const bodyCase = rn2(5);
    switch (bodyCase) {
    case 0:
        await pline(`A gush of water hits you on the ${body_part(HEAD)}!`);
        await water_damage(game.u?.uarmh, 'helmet', true);
        break;
    case 1:
        await pline(`A gush of water hits your left ${body_part(ARM)}!`);
        await water_damage(game.u?.uarms, 'shield', true);
        await water_damage(game.u?.uarmg, 'gloves', true);
        break;
    case 2:
        await pline(`A gush of water hits your right ${body_part(ARM)}!`);
        await water_damage(game.u?.uwep, null, true);
        await water_damage(game.u?.uarmg, 'gloves', true);
        break;
    default:
        await pline('A gush of water hits you!');
        if (game.u?.uarmc)
            await water_damage(game.u.uarmc, 'cloak', true);
        else if (game.u?.uarm)
            await water_damage(game.u.uarm, 'armor', true);
        else if (game.u?.uarmu)
            await water_damage(game.u.uarmu, 'shirt', true);
        break;
    }
    update_inventory();
    if (game.u?.umonnum === PM_IRON_GOLEM) {
        const dam = game.u?.mhmax || 1;
        await You('are covered with rust!');
        await losehp(Maybe_Half_Phys(dam), 'rusting away', KILLED_BY);
    } else if (game.u?.umonnum === PM_GREMLIN && rn2(3)) {
        // C: split_mon — TODO
    }
}
 
// Fire trap — hero
// C ref: trap.c:1729 (dispatches to dofiretrap)
async function trapeffect_fire_trap(trap, trflags) {
    seetrap(trap);
    await dofiretrap(null);
}
 
// C ref: trap.c:4211 dofiretrap(box)
// RNG: d(2,4) damage, rn2(2) fire resistance, rn2(num+1) mhpmax reduction
async function dofiretrap(box) {
    const see_it = !Blind();
    const orig_dmg = c_d(2, 4);
    let num = orig_dmg;

    await pline(`A tower of flame ${box ? 'bursts' : 'erupts'} from ${the(box ? xname(box) : surface(game.u?.ux, game.u?.uy))}!`);
    if (Fire_resistance()) {
        shieldeff(game.u.ux, game.u.uy);
        num = rn2(2);
    } else if (Upolyd()) {
        const idx = game.u?.umonnum;
        let alt = 0;
        if (idx === PM_PAPER_GOLEM) alt = game.u.mhmax || 1;
        else if (idx === PM_STRAW_GOLEM) alt = Math.trunc((game.u.mhmax || 1) / 2);
        else if (idx === PM_WOOD_GOLEM) alt = Math.trunc((game.u.mhmax || 1) / 4);
        else if (idx === PM_LEATHER_GOLEM) alt = Math.trunc((game.u.mhmax || 1) / 8);
        if (alt > num) num = alt;
        if ((game.u.mhmax || 0) > 0) {
            game.u.mhmax -= rn2(Math.min(game.u.mhmax, num + 1));
        }
        if (game.u.mh > game.u.mhmax) game.u.mh = game.u.mhmax;
    } else {
        num = c_d(2, 4);
        if ((game.u.uhpmax || 0) > 1) {
            game.u.uhpmax -= rn2(Math.min(game.u.uhpmax, num + 1));
            if (game.u.uhpmax < 1) game.u.uhpmax = 1;
        }
        if (game.u.uhp > game.u.uhpmax) game.u.uhp = game.u.uhpmax;
    }
    if (!num) {
        await You('are uninjured.');
    } else {
        await losehp(num, 'tower of flame', KILLED_BY_AN);
    }
    await burn_away_slime();
    if (await burnarmor(game.u) || rn2(3)) {
        await destroy_items(game.u, AD_FIRE, orig_dmg);
        ignite_items(game.u?.invent);
    }
    if (!box && burn_floor_objects(game.u?.ux, game.u?.uy, see_it, true) && !see_it)
        await You('smell paper burning.');
    if (is_ice(game.u?.ux, game.u?.uy))
        melt_ice(game.u?.ux, game.u?.uy, null);
}
 
// Hole / trapdoor — hero
// C ref: trap.c:2012
async function trapeffect_hole(trap, trflags) {
    seetrap(trap);
    await fall_through(true, (trflags & TOOKPLUNGE) !== 0);
}
 
// Teleport trap — hero
// C ref: trap.c:2069
async function trapeffect_telep(trap, trflags) {
    seetrap(trap);
    await tele_trap(trap);
}
 
// Level teleport — hero
// C ref: trap.c:2087
async function trapeffect_level_telep_hero(trap, trflags) {
    seetrap(trap);
    await level_tele_trap(trap, trflags);
}
 
// Web — hero
// C ref: trap.c:2105
// RNG: rn1 for duration based on strength, rnd(2) at high str
async function trapeffect_web(trap, trflags) {
    const webmsgok = (trflags & NOWEBMSG) === 0;
    const forcetrap = ((trflags & FORCETRAP) !== 0
                       || (trflags & FAILEDUNTRAP) !== 0);
    feeltrap(trap);
    // Check if hero can pass through web
    if (await mu_maybe_destroy_web(game.u, webmsgok, trap))
        return;
    if (webmaker(game.u?.data)) {
        if (webmsgok)
            await pline(trap.madeby_u ? 'You take a walk on your web.'
                  : 'There is a spider web here.');
        return;
    }
    if (webmsgok) {
        if (forcetrap) {
            await You(`are caught by ${trap.madeby_u ? 'your' : 'a'} spider web!`);
        } else {
            await You(`stumble into ${trap.madeby_u ? 'your' : 'a'} spider web!`);
        }
    }
    set_utrap(1, TT_WEB); // initial, adjusted below
 
    // Duration based on strength
    const str = ACURR(A_STR);
    let tim;
    if (str <= 3) tim = rn1(6, 6);
    else if (str < 6) tim = rn1(6, 4);
    else if (str < 9) tim = rn1(4, 4);
    else if (str < 12) tim = rn1(4, 2);
    else if (str < 15) tim = rn1(2, 2);
    else if (str < 18) tim = rnd(2);
    else if (str < 69) tim = 1;
    else {
        tim = 0;
        if (webmsgok)
            await You(`tear through ${trap.madeby_u ? 'your' : 'a'} web!`);
        deltrap(trap);
        newsym(game.u.ux, game.u.uy);
    }
    set_utrap(tim, TT_WEB);
}
 
// Statue trap — hero
// C ref: trap.c:2278
async function trapeffect_statue_trap(trap, trflags) {
    await activate_statue_trap(trap, game.u.ux, game.u.uy, false);
}
 
// Magic trap — hero
// C ref: trap.c:2292
// RNG: rn2(30) explosion, rnd(10) explosion damage, rnd(20) fate dispatch
async function trapeffect_magic_trap(trap, trflags) {
    seetrap(trap);
    if (!rn2(30)) {
        deltrap(trap);
        newsym(game.u.ux, game.u.uy);
        await You('are caught in a magical explosion!');
        await losehp(rnd(10), 'magical explosion', KILLED_BY_AN);
        await Your('body absorbs some of the magical energy!');
        game.u.uen = (game.u.uenmax = (game.u.uenmax || 0) + 2);
        if (game.u.uenmax > (game.u.uenpeak || 0))
            game.u.uenpeak = game.u.uenmax;
        return;
    }
    await domagictrap();
    steedintrap(trap, null);
}
 
// C ref: trap.c:4297 domagictrap()
// RNG: rnd(20) fate, then fate-specific RNG
async function domagictrap() {
    const fate = rnd(20);

    if (fate < 10) {
        // Create monsters
        const cnt = rnd(4);

        if (!resists_blnd(game.u)) {
            await You('are momentarily blinded by a flash of light!');
            const blindtime = rn1(5, 10);
            await make_blinded(blindtime, false);
        } else if (!Blind()) {
            await You('see a flash of light!');
        }

        // Deafness
        if (!Deaf()) {
            await You_hear('a deafening roar!');
            const deaftime = rn1(20, 30);
            // C: incr_itimeout(&HDeaf, deaftime) — TODO
        } else {
            await You_feel('rankled.');
            const deaftime = rn1(5, 15);
            // C: incr_itimeout — TODO
        }
        for (let i = 0; i < cnt; i++) {
            await makemon(null, game.u.ux, game.u.uy, NO_MM_FLAGS);
        }
        wake_nearto(game.u.ux, game.u.uy, 49); // 7*7
    } else {
        switch (fate) {
        case 10:
            // Nothing happens
            break;
        case 11:
            // Toggle intrinsic invisibility
            await You_hear('a low hum.');
            if (!Invis()) {
                if (!Blind())
                    await self_invis_message();
            } else {
                await You_feel(`a little more ${game.u?.hinvis ? 'obvious' : 'hidden'} now.`);
            }
            // C: HInvis toggle
            if (game.u) {
                game.u.hinvis = game.u.hinvis ? 0 : 1;
            }
            newsym(game.u.ux, game.u.uy);
            break;
        case 12:
            // Fire
            await dofiretrap(null);
            break;
        case 13:
            await pline(`A shiver runs up and down your ${body_part(SPINE)}!`);
            break;
        case 14:
            await You_hear(Hallucination() ? 'the moon howling at you.'
                                           : 'distant howling.');
            break;
        case 15:
            await You('suddenly yearn for your distant homeland.');
            break;
        case 16:
            await Your('pack shakes violently!');
            break;
        case 17:
            await You(Hallucination() ? 'smell hamburgers.' : 'smell charred flesh.');
            break;
        case 18:
            await You_feel('tired.');
            break;
        case 19:
            // Tame nearby monsters
            await adjattrib(A_CHA, 1, false);
            for (let i = -1; i <= 1; i++) {
                for (let j = -1; j <= 1; j++) {
                    if (!isok(game.u.ux + i, game.u.uy + j)) continue;
                    const mtmp = m_at(game.u.ux + i, game.u.uy + j);
                    if (mtmp) await tamedog(mtmp, null, true);
                }
            }
            break;
        case 20:
            // Uncurse stuff — simplified
            // C: seffects with SPE_REMOVE_CURSE pseudo-object
            break;
        default:
            break;
        }
    }
}
 
// Anti-magic field — hero
// C ref: trap.c:2322
// RNG: rnd(4) base damage (multiple), d(2,6) drain, rnd(drain/2) max drain
async function trapeffect_anti_magic(trap, trflags) {
    seetrap(trap);
    if (Antimagic()) {
        let dmgval2 = rnd(4);
        if (Half_physical_damage() || Half_spell_damage())
            dmgval2 += rnd(4);
        if (Passes_walls())
            dmgval2 = Math.trunc((dmgval2 + 3) / 4);
        const hp = Upolyd() ? (game.u?.mh || 0) : (game.u?.uhp || 0);
        await You_feel(dmgval2 >= hp ? 'unbearably torpid!'
            : (dmgval2 >= Math.trunc(hp / 4)) ? 'very lethargic.'
            : 'sluggish.');
        await losehp(dmgval2, 'anti-magic implosion', KILLED_BY_AN);
    }

    const drainamt = c_d(2, 6);
    const halfd = rnd(Math.trunc(drainamt / 2) || 1);
    let exclaim_it = false;
    if ((game.u?.uenmax || 0) > drainamt) {
        game.u.uenmax -= halfd;
        exclaim_it = true;
    }
    await drain_en(drainamt - (exclaim_it ? halfd : 0), exclaim_it);
}
 
// Polymorph trap — hero
// C ref: trap.c:2452
// RNG: none directly (polyself has its own RNG)
async function trapeffect_poly_trap(trap, trflags) {
    seetrap(trap);
    await You('step onto a polymorph trap!');
    if (Antimagic() || Unchanging()) {
        shieldeff(game.u.ux, game.u.uy);
        await You_feel('momentarily different.');
    } else {
        steedintrap(trap, null);
        deltrap(trap);
        newsym(game.u.ux, game.u.uy);
        await You_feel('a change coming over you.');
        await polyself(0);
    }
}
 
// Land mine — hero
// C ref: trap.c:2529
// RNG: rnd(16) damage, rn2(3) levitation discover, rn2(3) levitation trigger,
//      rn1(35,41) leg wounds x2
async function trapeffect_landmine(trap, trflags) {
    let damage = rnd(16);
    if (wearing_iron_shoes(game.u))
        damage = Math.trunc((damage + 3) / 4);
 
    const already_seen = trap.tseen;
    const forcetrap = ((trflags & FORCETRAP) !== 0
                       || (trflags & FAILEDUNTRAP) !== 0);
 
    if ((Levitation() || Flying()) && !forcetrap) {
        if (!already_seen && rn2(3))
            return;
        feeltrap(trap);
        await pline(`${already_seen ? 'There is' : 'You discover'} ${trap.madeby_u ? 'the trigger of your mine' : 'a trigger'} in a pile of soil below you.`);
        if (already_seen && rn2(3))
            return;
        await pline('KAABLAMM!!!  The air currents set it off!');
    } else {
        feeltrap(trap);
        await pline(`KAABLAMM!!!  You triggered ${trap.madeby_u ? 'your' : 'a'} land mine!`);
        await set_wounded_legs(LEFT_SIDE, rn1(35, 41));
        await set_wounded_legs(RIGHT_SIDE, rn1(35, 41));
        exercise(A_DEX, false);
    }
    // Convert to pit before losehp
    trap.ttyp = PIT;
    trap.madeby_u = false;
    await losehp(Maybe_Half_Phys(damage), 'land mine', KILLED_BY_AN);
    blow_up_landmine(trap);
    newsym(game.u.ux, game.u.uy);
    // Recursive fall into pit
    const newTrap = t_at(game.u.ux, game.u.uy);
    if (newTrap)
        await dotrap(newTrap, RECURSIVETRAP);
    fill_pit(game.u.ux, game.u.uy);
}
 
// Rolling boulder trap — hero
// C ref: trap.c:2663
// RNG: none directly (launch_obj has its own)
async function trapeffect_rolling_boulder_trap(trap, trflags) {
    const style = ROLL | (trap.tseen ? LAUNCH_KNOWN : 0);
    feeltrap(trap);
    await pline(`${!Deaf() ? 'Click!  ' : ''}You trigger a rolling boulder trap!`);
    if (!await launch_obj(BOULDER, trap.launch?.x, trap.launch?.y,
                    trap.launch2?.x, trap.launch2?.y, style)) {
        deltrap(trap);
        newsym(game.u.ux, game.u.uy);
        await pline('Fortunately for you, no boulder was released.');
    }
}
 
// Magic portal — hero
// C ref: trap.c:2711
async function trapeffect_magic_portal_hero(trap, trflags) {
    feeltrap(trap);
    await domagicportal(trap);
}
 
// Vibrating square — hero
// C ref: trap.c:2726
async function trapeffect_vibrating_square(trap, trflags) {
    feeltrap(trap);
    // Messages handled elsewhere; trap symbol marks the square
}
 
// ── instapetrify: instant petrification ──
// C ref: trap.c:3822
export async function instapetrify(str) {
    if (Stone_resistance()) return;
    await urgent_pline('You turn to stone...');
    if (!game.killer) game.killer = {};
    game.killer.format = KILLED_BY;
    game.killer.name = str || '';
    await done(STONING);
}
 
// ── minstapetrify: monster instant petrification ──
// C ref: trap.c:3835
export function minstapetrify(mon, _byplayer) {
    if (!mon) return;
    if (mon.data && (mon.data.mresists & 0x80)) return; // MR_STONE
    mon.dead = true;
    mon.mhp = 0;
}
 
// C ref: trap.c:6200 — cnv_trap_obj (convert trap to object)
export function cnv_trap_obj(otyp, qty, ttmp, _flag) {
    delfloortrap(ttmp);
}
 
// ── count_traps — count traps of a given type ──
// C ref: trap.c:6494
export function count_traps(ttyp) {
    let count = 0;
    for (const t of (game.level?.traps || [])) {
        if (t.ttyp === ttyp) count++;
    }
    return count;
}
 
// ── uteetering_at_seen_pit — hero on edge of a seen pit? ──
// C ref: trap.c:6626
export function uteetering_at_seen_pit(trap) {
    return (trap && is_pit(trap.ttyp) && trap.tseen
        && (!game.u?.utrap || game.u?.utraptype !== TT_PIT));
}
 
// ── uescaped_shaft — hero escaped from shaft? ──
// C ref: trap.c:6638
export function uescaped_shaft(trap) {
    return (trap && is_hole(trap.ttyp) && trap.tseen
        && (!game.u?.utrap || game.u?.utraptype !== TT_PIT));
}
 
// ── trap_sanity_check ──
// C ref: trap.c:7173
export function trap_sanity_check() {
    for (const t of (game.level?.traps || [])) {
        if (!isok(t.tx, t.ty)) {
            // impossible: trap sanity location
        }
        if (t.ttyp <= 0 || t.ttyp >= 24) { // TRAPNUM
            // impossible: trap sanity type
        }
    }
}
 
// ============================================================================
// maketrap / deltrap + helpers
// C ref: trap.c
// ============================================================================
 
function can_fall_thru(lev) {
    const dnum = lev?.dnum ?? 0;
    const dlevel = lev?.dlevel ?? 1;
    const dungeon = game.dungeons?.[dnum];
    const bottom = dungeon?.num_dunlevs ?? dlevel;
    return dlevel < bottom;
}
 
function hole_destination(dst) {
    const uz = game.u?.uz ?? { dnum: 0, dlevel: 1 };
    const dungeon = game.dungeons?.[uz.dnum];
    const bottom = dungeon?.num_dunlevs ?? uz.dlevel;
 
    dst.dnum = uz.dnum;
    dst.dlevel = uz.dlevel;
    while (dst.dlevel < bottom) {
        dst.dlevel++;
        if (rn2(4))
            break;
    }
}
 
function pool_or_lava_at(x, y) {
    const typ = game.level?.at?.(x, y)?.typ;
    return typ === POOL || typ === MOAT || typ === WATER || typ === LAVAPOOL;
}
 
function closed_door_at(x, y) {
    const loc = game.level?.at?.(x, y);
    if (!loc) return false;
    if (loc.typ !== DOOR) return false;
    const mask = loc.doormask ?? 0;
    return (mask & (D_CLOSED | D_LOCKED)) !== 0;
}
 
function isclearpath(cc, distance, dx, dy) {
    let x = cc.x;
    let y = cc.y;
    while (distance-- > 0) {
        x += dx;
        y += dy;
        if (!isok(x, y)) return false;
        const typ = game.level?.at?.(x, y)?.typ;
        if (typ == null || !ZAP_POS(typ) || closed_door_at(x, y)) return false;
        const t = game.level?.trapAt?.(x, y) || null;
        if (t && (is_pit(t.ttyp) || is_hole(t.ttyp) || is_xport(t.ttyp))) {
            return false;
        }
    }
    cc.x = x;
    cc.y = y;
    return true;
}
 
// C ref: trap.c find_random_launch_coord()
function find_random_launch_coord(ttmp, cc) {
    if (!ttmp || !cc) return false;
 
    const x = ttmp.tx;
    const y = ttmp.ty;
    let mindist = (ttmp.ttyp === ROLLING_BOULDER_TRAP) ? 2 : 4;
    let distance = rn2(5) + 4; // rn1(5,4)
    let tmp = rn2(N_DIRS);
    let trycount = 0;
 
    while (distance >= mindist) {
        const dx = xdir[tmp];
        const dy = ydir[tmp];
        cc.x = x;
        cc.y = y;
        let success;
        if (ttmp.ttyp === ROLLING_BOULDER_TRAP
            && pool_or_lava_at(x + distance * dx, y + distance * dy)) {
            success = false;
        } else {
            success = isclearpath(cc, distance, dx, dy);
        }
        if (ttmp.ttyp === ROLLING_BOULDER_TRAP) {
            const back = { x, y };
            const successOther = isclearpath(back, distance, -dx, -dy);
            if (!successOther) success = false;
        }
        if (success) return true;
        if (++tmp > 7) tmp = 0;
        if ((++trycount % N_DIRS) === 0) distance--;
    }
    return false;
}
 
// C ref: trap.c:3637 mkroll_launch()
function mkroll_launch(ttmp, x, y, otyp, ocount) {
    const cc = { x: 0, y: 0 };
    const success = find_random_launch_coord(ttmp, cc);
 
    if (!success) {
        // Create trap without ammo; launch point stays at trap.
        cc.x = x;
        cc.y = y;
    } else {
        const otmp = mksobj(otyp, true, false);
        if (otmp) {
            otmp.quan = ocount;
            otmp.owt = weight(otmp);
            place_object(otmp, cc.x, cc.y);
            stackobj(otmp);
        }
    }
 
    ttmp.launch = { x: cc.x, y: cc.y };
    if (ttmp.ttyp === ROLLING_BOULDER_TRAP) {
        ttmp.launch2 = { x: x - (cc.x - x), y: y - (cc.y - y) };
    } else {
        ttmp.launch_otyp = otyp;
    }
    newsym(ttmp.launch.x, ttmp.launch.y);
    return 1;
}
 
// C ref: trap.c:3080 choose_trapnote()
// C ref: trap.c:3060 trapnote — get note name for squeaky board
const TRAPNOTE_NAMES = [
    'C note', 'D flat', 'D note', 'E flat',
    'E note', 'F note', 'F sharp', 'G note',
    'G sharp', 'A note', 'B flat', 'B note',
];
function trapnote(trap, noprefix) {
    const tn = TRAPNOTE_NAMES[trap.tnote] || 'C note';
    if (noprefix) return tn;
    // C ref: objnam.c:2113-2115 just_an — single-letter-before-space case
    // "aefhilmnosx" get "an" (pronounced with vowel sounds)
    const c0 = tn[0].toLowerCase();
    const prefix = 'aefhilmnosx'.includes(c0) ? 'an ' : 'a ';
    return prefix + tn;
}
 
function choose_trapnote(ttmp) {
    const tavail = new Array(12).fill(0);
    const tpick = [];
    for (const t of (game.level?.traps || [])) {
        if (t.ttyp === SQKY_BOARD && t !== ttmp)
            tavail[t.tnote || 0] = 1;
    }
    for (let k = 0; k < 12; k++) {
        if (tavail[k] === 0) tpick.push(k);
    }
    return tpick.length > 0 ? tpick[rn2(tpick.length)] : rn2(12);
}
 
// C ref: trap.c:391 mk_trap_statue — create a "living" statue for STATUE_TRAP
async function mk_trap_statue(x, y) {
    let trycount = 10;
    let mptr;
    do {
        mptr = mons[rndmonnum_adj(3, 6)];
    } while (--trycount > 0 && is_unicorn(mptr)
             && Math.sign(game.u?.ualign?.type ?? 0) === Math.sign(mptr.maligntyp ?? 0));
    const statue = mkcorpstat(STATUE, null, mptr, x, y, CORPSTAT_NONE);
    if (!statue) return;
    const mtmp = await makemon(mons[statue.corpsenm], 0, 0,
                               MM_NOCOUNTBIRTH | MM_NOMSG);
    if (!mtmp) return;
    while (mtmp.minvent) {
        const otmp = mtmp.minvent;
        otmp.owornmask = 0;
        obj_extract_self(otmp);
        add_to_container(statue, otmp);
    }
    statue.owt = weight(statue);
    mongone(mtmp);
    // Remove dead temp monster from fmon chain immediately.
    // C's dmonsfree would do this later, but mapstate captures happen first.
    let removed = false;
    if (game.fmon === mtmp) {
        game.fmon = mtmp.nmon || null;
        removed = true;
    } else {
        for (let prev = game.fmon; prev; prev = prev.nmon) {
            if (prev.nmon === mtmp) {
                prev.nmon = mtmp.nmon || null;
                removed = true;
                break;
            }
        }
    }
    // Also check level.monsters array
    if (game.level?.monsters) {
        const idx = game.level.monsters.indexOf(mtmp);
        if (idx >= 0) game.level.monsters.splice(idx, 1);
    }
}
 
// C ref: trap.c — maketrap()
// Event: ^trap[ttyp,x,y]
export async function maketrap(x, y, typ, options = null) {
    const suppressEvent = !!options?.suppressEvent;
    if (typ === TRAPPED_DOOR || typ === TRAPPED_CHEST) {
        return null;
    }
 
    const loc = levl(x, y);
    if (!loc) return null;
 
    // C ref: trap.c:467-490 — reject or overwrite existing trap placement.
    let trap = t_at(x, y);
    const oldplace = !!trap;
    if (trap) {
        if (undestroyable_trap(trap.ttyp)) return null;
        if (game.u?.utrap && game.u?.ux === x && game.u?.uy === y) {
            if ((game.u.utraptype === TT_BEARTRAP && typ !== BEAR_TRAP)
                || (game.u.utraptype === TT_WEB && typ !== WEB)
                || (game.u.utraptype === TT_PIT && !is_pit(typ))
                || (game.u.utraptype === TT_LAVA && game.level?.at(x, y)?.typ !== LAVAPOOL)) {
                await reset_utrap(false);
            }
        }
    } else {
        const isPoolOrLava = (loc.typ === POOL || loc.typ === MOAT
            || loc.typ === WATER || loc.typ === LAVAPOOL);
        const canOverwriteTerrain = (game.iflags?.debug_overwrite_stairs
            || (loc.typ !== LADDER && loc.typ !== STAIRS));
        if (!canOverwriteTerrain
            || isPoolOrLava
            || (IS_FURNITURE(loc.typ) && (typ !== PIT && typ !== HOLE))
            || (loc.typ === DRAWBRIDGE_UP && typ === MAGIC_PORTAL)
            || (IS_AIR(loc.typ) && typ !== MAGIC_PORTAL)
            || (typ === LEVEL_TELEP && Is_knox_level(game.u?.uz))) {
            return null;
        }
        trap = {
            tx: x, ty: y,
            ntrap: null,
        };
    }
 
    // C ref: trap.c:496+ — [re]initialize trap fields except coordinates.
    trap.ttyp = typ;
    // C ref: trap.c:502 — unhideable_trap(HOLE) makes holes always visible
    trap.tseen = (typ === HOLE);
    trap.once = false;
    trap.madeby_u = false;
    trap.dst = { dnum: -1, dlevel: -1 };
    trap.launch = { x: 0, y: 0 };
 
    if (typ === SQKY_BOARD) {
        trap.tnote = choose_trapnote(trap);
    }
    if (typ === STATUE_TRAP) {
        await mk_trap_statue(x, y);
    }
    if (typ === ROLLING_BOULDER_TRAP) {
        mkroll_launch(trap, x, y, BOULDER, 1);
    }
    // C ref: trap.c:522 — hole_destination called unconditionally for HOLE/TRAPDOOR
    if (typ === HOLE || typ === TRAPDOOR)
        hole_destination(trap.dst);
 
    if (game.level && !oldplace) {
        game.level.traps.push(trap);
    }
    if (!suppressEvent) pushRngLogEntry(`^trap[${typ},${x},${y}]`);
    return trap;
}
 
// C ref: trap.c — deltrap()
// Event: ^deltrap[ttyp,x,y]
export function deltrap(trap) {
    if (!trap) return;
    pushRngLogEntry(`^dtrap[${trap.ttyp},${trap.tx},${trap.ty}]`);
    if (game.level) {
        const idx = game.level.traps.indexOf(trap);
        if (idx >= 0) game.level.traps.splice(idx, 1);
    }
}
 
 
// ── could_untrap — preliminary checks for dountrap ──
// C ref: trap.c:5235
export async function could_untrap(verbosely, check_floor) {
    const u = game.u;
    let buf = '';

    if (near_capacity() >= HVY_ENCUMBER) {
        buf = "You're too strained to do that.";
    } else if ((nohands(game.youmonst?.data) && !webmaker(game.youmonst?.data))
               || !game.youmonst?.data?.mmove) {
        buf = 'And just how do you expect to do that?';
    } else if (u.ustuck && sticks_fn(game.youmonst?.data)) {
        buf = `You'll have to let go of ${mon_nam(u.ustuck)} first.`;
    } else if (u.ustuck || (welded_fn(game.u.uwep) && bimanual_fn(game.u.uwep))) {
        buf = `Your ${makeplural(body_part(HAND))} seem to be too busy for that.`;
    } else if (check_floor && !can_reach_floor(false)) {
        buf = `You can't reach the ${surface(u.ux, u.uy)}.`;
    }
    if (buf) {
        if (verbosely) await pline(buf);
        return 0;
    }
    return 1;
}
 
// sticks_fn, welded_fn, bimanual_fn: imported at top from mondata/wield/hacklib
 
// ── untrap_prob — chance of disabling trap ──
// C ref: trap.c:5266
function untrap_prob(ttmp) {
    let chance = 3;
    if (ttmp.ttyp === WEB) {
        if (!webmaker(game.youmonst?.data))
            chance = 7;
    }
    if (Hallucination() || game.u.uconfused) chance++;
    if (Blind()) chance++;
    if (game.u.ustun) chance += 2;
    if (Fumbling()) chance *= 2;
    if (ttmp.madeby_u) chance--;
    if (Role_if(PM_RANGER) && ttmp.ttyp === BEAR_TRAP && chance <= 3)
        return 0;
    if (Role_if(PM_ROGUE)) {
        if (rn2(2 * MAXULEV) < game.u.ulevel) chance--;
        if (game.u.uhave?.questart && chance > 1) chance--;
    } else if (Role_if(PM_RANGER) && chance > 1) {
        chance--;
    }
    if (chance < 1) chance = 1;
    return rn2(chance);
}
 
// ── dountrap — the #untrap command ──
// C ref: trap.c:5226
export async function dountrap() {
    if (!(await could_untrap(true, false)))
        return ECMD_OK;

    return (await untrap(false, 0, 0, null)) ? 1 : ECMD_OK;
}
 
// ── untrap — the main untrap logic ──
// C ref: trap.c:5826 — simplified port
// Full untrap handles traps, containers, doors
async function untrap(force, rx, ry, container) {
    const u = game.u;
    const g = game;
    let x, y;
    const confused = !!(Hallucination() || u.uconfused);

    if (!force && has_magic_key())
        force = true;

    if (!rx && !container) {
        if (!(await getdir(null)))
            return 0;
        x = u.ux + u.dx;
        y = u.uy + u.dy;
    } else {
        if (container) {
            // TODO: untrap_box
            await pline('You cannot disarm that container.');
            return 0;
        }
        x = rx; y = ry;
    }
    if (!isok(x, y)) {
        await pline_The('perils lurking there are beyond your grasp.');
        return 0;
    }

    let ttmp = t_at(x, y);
    if (ttmp && !ttmp.tseen) ttmp = null;
    const trapdescr = ttmp ? trapname(ttmp.ttyp, false) : null;
    const here = u_at(x, y);

    // check for floor trap
    const deal_with_floor_trap = can_reach_floor(false);

    if (deal_with_floor_trap && ttmp) {
        const the_trap = the(trapdescr);
        if (u.utrap) {
            await You(`cannot deal with ${the_trap} while trapped${u_at(x, y) ? ' in it' : ''}!`);
            return 1;
        }
        const mtmp = m_at(x, y);
        if (mtmp && (M_AP_TYPE(mtmp) === M_AP_FURNITURE || M_AP_TYPE(mtmp) === M_AP_OBJECT)) {
            await stumble_onto_mimic(mtmp);
            return 1;
        }
        // attempt to disarm the trap
        switch (ttmp.ttyp) {
        case BEAR_TRAP:
        case WEB:
        case LANDMINE:
        case SQKY_BOARD:
        case DART_TRAP:
        case ARROW_TRAP:
            // simplified disarm attempt
            if (untrap_prob(ttmp)) {
                await You(`fail to disarm ${the_trap}.`);
                if (ttmp.ttyp !== WEB && !rn2(3)) {
                    await pline('Whoops...');
                }
                return 1;
            }
            await You(`disarm ${the_trap}.`);
            // simplified: just remove the trap (full port would convert to objects)
            deltrap(ttmp);
            return 1;
        case PIT:
        case SPIKED_PIT:
            if (here) {
                await You('are already on the edge of the pit.');
                return 0;
            }
            if (!m_at(x, y)) {
                await pline('Try filling the pit instead.');
                return 0;
            }
            // help_monster_out — TODO
            return 0;
        default:
            await You('cannot disable %s trap.', !here ? 'that' : 'this');
            return 0;
        }
    }

    // door handling
    const lv = levl(x, y);
    if (!lv || !IS_DOOR(lv.typ)) {
        if (!ttmp) await You('know of no traps there.');
        return 0;
    }

    switch (lv.doormask) {
    case D_NODOOR:
        await You(`${Blind() ? 'feel' : 'see'} no door there.`);
        return 0;
    case 0x04: /* D_ISOPEN */
        await pline('This door is safely open.');
        return 0;
    case 0x20: /* D_BROKEN */
        await pline('This door is broken.');
        return 0;
    }

    if (((lv.doormask & D_TRAPPED) !== 0
         && (force || (!confused && rn2(MAXULEV - u.ulevel + 11) < 10)))
        || (!force && confused && !rn2(3))) {
        await You('find a trap on the door!');
        exercise(A_WIS, true);
        const ans = await ynq('Disarm it?');
        if (ans !== 'y') return 1;
        if (lv.doormask & D_TRAPPED) {
            const ch = 15 + (Role_if(PM_ROGUE) ? u.ulevel * 3 : u.ulevel);
            exercise(A_DEX, true);
            if (!force && (confused || Fumbling()
                           || rnd(75 + level_difficulty() / 2) > ch)) {
                await You('set it off!');
                await b_trapped('door', FINGER);
                lv.doormask = D_NODOOR;
                unblock_point(x, y);
                newsym(x, y);
                if (in_rooms(x, y, SHOPBASE).length > 0)
                    add_damage(x, y, 0);
            } else {
                await You('disarm it!');
                lv.doormask &= ~D_TRAPPED;
                more_experienced(8, 0);
                await newexplevel();
            }
        } else {
            await pline('This door was not trapped.');
        }
        return 1;
    } else {
        await You('find no traps on the door.');
        return 1;
    }
}
 
// stub helpers for untrap
// C ref: artifact.c:2790 — has_magic_key: check if hero carries Master Key of Thievery
function has_magic_key() {
    const key_otyp = artilist[ART_MASTER_KEY_OF_THIEVERY].otyp;
    for (let o = nxtobj(game.invent, key_otyp, false); o; o = nxtobj(o.nobj, key_otyp, false)) {
        if (o.oartifact === ART_MASTER_KEY_OF_THIEVERY)
            return o;
    }
    return null;
}
// cnv_trap_obj already defined above at line ~2486
 
// mondead / mondied moved to mon.js (canonical C location: mon.c)
// C ref: trap.c:5353 — whether movement is "into" vs "onto" a trap tile.
export function into_vs_onto(traptype) {
    switch (traptype) {
    case BEAR_TRAP:
    case PIT:
    case SPIKED_PIT:
    case HOLE:
    case TELEP_TRAP:
    case LEVEL_TELEP:
    case MAGIC_PORTAL:
    case WEB:
        return true;
    default:
        return false;
    }
}