All files / js pickup.js

75.11% Statements 1446/1925
62.33% Branches 384/616
83.33% Functions 50/60
75.11% Lines 1446/1925

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 192673x 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 78x   73x 73x 73x 73x 73x 38723x 38723x 38723x 1341x 1341x 1341x   1341x 1159x 1159x 1341x 1341x 38723x 38723x 414x 38723x 38309x 38309x 38173x 38173x 38173x 38173x 38309x 38723x 38723x 73x 73x 73x 73x 38713x 38713x 434261x 20775x 434261x 413486x 413486x 413486x 413486x 413486x 434261x 38713x 38713x 38713x 73x 73x 73x 38613x 38613x 38613x 38613x 4666x 4666x 38613x 73x 73x 73x 38475x 38475x 73x 73x 73x 90x 90x 90x 73x 73x 73x 73x 13045x 13045x 13045x 14x 14x 7x 7x 14x 5x 5x 14x 1x 1x 1x 14x 1x 1x 1x 14x 14x 13045x 8x 8x 6x 6x 8x 1x 1x 8x 1x 1x 8x       8x 8x 8x 13045x 13045x 73x 73x 73x 29x 29x 29x 21x 18x 18x 18x 21x 29x 29x 73x 73x 73x 73x 896497x 896497x 896497x 17184459x 17184459x 882418x 896497x 73x 73x 131x 131x 131x 18x 18x 18x 18x 73x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 1x 1x       1x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x   18x 18x 6x 6x 6x 6x 6x 6x 6x 6x 21x 21x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 30x 30x 6x 6x 6x 6x 6x 50x 50x 50x 50x 50x 40x 18x 6x 6x 25x 25x 25x 15x 15x 23x 9x 25x 6x 6x 6x 6x 6x 6x 6x 21x 21x 21x 21x 6x 90x 90x 90x 90x 90x 21x 21x 21x 21x 21x 14x 14x 6x                   6x               6x 73x 73x 73x 73x 13x 13x 13x 13x             13x 73x 2x 2x 2x 2x         2x 73x 73x 73x 73x 128x 128x 128x 128x 128x 1x 1x 1x       128x                                       128x 128x 128x 128x 128x 128x 128x 127x 128x 73x 73x                                             73x 330x 330x 330x 330x 330x 330x 330x 330x 330x 330x       330x 330x 19x 19x 17x     330x 3x 3x 3x 3x 1x 3x 2x 2x 1x 1x 1x 1x 2x 3x     311x   308x   308x   308x   308x   308x   308x 2x 2x 330x 330x 17x 17x 330x 330x 284x 330x 6x 46x 40x 1x 1x 40x 330x 330x 330x 330x 73x 73x                     73x 73x 73x 73x 180x 180x 180x 180x 180x 180x 180x 180x             180x 73x 73x 73x 73x 73x 148x 1494x 1494x 148x 73x 73x 73x 25x 25x 25x 25x 20x 20x 20x 25x 25x 25x 25x 25x 25x 25x 25x 25x 25x 25x 25x 25x 25x 25x 25x 25x 25x 25x 25x 73x 73x 20x 20x 20x 20x 20x 25x 8x 8x 25x 25x 20x 20x 73x 73x             73x 73x 73x 90x 90x 90x 90x 90x 90x 90x 90x       90x 90x 90x 90x 90x 90x       90x 90x 90x 90x                                                                                                                                                         90x 73x 8x 8x 8x 73x 73x 73x 73x 90x 90x 90x 90x 90x       90x 90x 90x             90x 90x 90x 90x   90x 90x 90x         90x 90x 90x 90x 90x 90x 90x 90x 90x 1x   1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x     1x     1x 1x 1x 1x 1x 1x 90x 90x 90x 90x 90x 73x 73x 73x 82x 82x 82x 82x 82x 82x 82x 4x 4x 4x 82x 82x 4x 4x 82x 82x 73x 73x 90x 90x 90x 90x 83x 89x 7x 7x 7x 5x 7x 7x 7x 90x 90x 90x 73x 73x 73x 73x 84x       84x 84x 84x 84x 2x 84x 82x     82x   82x 2x 2x 2x 82x                                           82x 82x 82x       82x 82x 82x 82x 84x 84x 82x 84x 84x 82x 82x 82x 82x 82x 84x 73x   73x 73x 142x 142x 142x 142x 142x 15x 15x 15x 142x 142x 158x 158x 158x 142x 129x 129x 129x 129x 129x 129x 129x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 128x 128x 128x 128x 1x 1x 128x 128x 128x 128x 128x 128x 128x 128x 128x 128x 10x 10x 10x 10x 10x 10x 10x 10x 29x 29x 29x 29x 10x 10x 9x 9x 9x 9x 118x 118x 118x 118x 118x 118x 118x 142x 13x 13x 142x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 94x 94x 94x 94x 94x 70x 79x 94x 94x 73x 73x 73x 73x 73x 73x 73x 4441x 4441x 4441x 4441x 4441x 4441x       4441x 4441x 4441x 4441x 4441x 4441x 4441x 4441x 4441x 4441x 4371x 4441x 4227x 4227x 4227x 4227x 4227x 214x 214x 4441x 2x 2x 2x 2x 2x 2x 2x 212x 4441x 4441x 4441x 122x 122x 122x 122x 121x 121x 90x 4441x 4441x 4441x 4441x 90x 90x 90x 90x 90x 90x 90x 3808x       90x 3670x 20x 20x 20x 20x 20x 8x 8x 8x 8x 8x 8x 3808x 70x 70x 70x 70x 70x 70x             70x 64x 64x 64x 64x 64x 64x 70x 6x 6x 6x 6x 6x 6x 6x 6x 6x 5x 5x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 5x 6x 70x 90x 90x 90x 90x 90x 90x 90x 90x 90x 90x 90x 90x 4441x 4441x 73x 73x 73x       73x 73x     73x 73x 73x 58x 58x 73x 73x 73x 73x 73x 73x 73x         73x 73x 18x 18x 18x 18x       18x     18x     18x       18x 18x 73x 73x 9x 9x     9x       9x 9x 73x 73x 18x 18x 18x 18x 18x 9x 9x 9x 8x 8x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 9x 9x 6x 6x 9x 16x             9x 18x 73x 2x 2x 2x 2x 2x     2x             2x 73x 8x 8x 14x 14x 14x 6x 8x 73x 73x 11x 11x 11x 11x 11x 11x 11x 11x 11x 10x 10x 10x 10x 11x 11x 11x 11x 11x 11x 10x 10x 10x 10x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x         11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 73x 73x 73x 73x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 60x 60x 19x 60x 60x 60x 4x 4x 4x 4x 4x 28x 15x 14x 14x 15x 28x 4x 2x 2x 4x 1x 1x 1x 4x       4x 2x 2x 2x 4x 4x 4x 4x 4x 3x 4x 73x 73x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 35x 35x 35x 9x 5x 5x 8x 4x 4x 4x 4x 4x 3x 3x 3x 3x 1x 1x 3x 4x 9x 9x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 29x 29x 29x 22x 22x 8x 8x 8x 8x 8x 8x 8x 22x 22x 13x 22x 1x 1x 22x 22x 22x 8x 8x 8x 120x 120x 15x 15x 120x 120x 15x 8x 8x 15x 15x 15x   15x 15x   8x 8x 8x 8x 8x 8x 8x 8x 11x 11x 11x 11x 2x 2x 11x 11x 3x 9x 8x 8x 11x                 11x 8x 8x 9x 9x 73x 73x           73x 73x 73x 3x 3x 3x 3x     3x     3x     3x     3x       3x 3x 3x 3x     3x     3x             3x   3x     3x 3x 3x 3x 3x         3x 3x 3x 3x                     3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 73x 73x 73x 8x 8x 8x 8x     8x 1x 1x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x         8x 8x 8x 8x 1x 1x 8x 8x 73x 73x 73x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x       9x       9x                     9x 9x 9x 9x       9x 9x 9x 9x 9x     9x 9x 9x 9x 9x 9x 9x 2x 2x 9x 9x 11x 11x 11x 11x 10x 10x 11x 11x   11x 11x 11x 11x 11x 11x 11x     11x 2x 2x 2x 2x 2x 2x 1x 1x 2x 11x 9x 9x 11x 9x 9x 9x                           9x 9x 9x 9x 9x 9x 7x 1x 1x 1x 7x 6x 6x 7x 7x 7x 7x 9x         9x 2x 8x                     9x 9x 9x 9x 9x 1x       1x 1x 1x 1x 9x 9x 8x 8x 8x 9x 9x 9x 9x     9x 9x 73x 73x 73x 27x 27x 27x 27x 27x 73x 73x 27x 27x 27x 27x 27x 27x 27x 27x     27x 1x 1x 1x 27x               26x 26x 26x 26x 18x 18x 18x 18x 18x                                             18x 18x 18x 21x 21x 18x 18x 18x 18x 18x 18x 21x 21x 18x 18x 18x 26x 27x 8x 2x 2x 2x 2x 2x 2x 2x 2x                                                             8x 6x 6x 8x 27x 27x 73x 73x 73x 72x 72x 72x 72x 72x  
// pickup.js — Item pickup and encumbrance (port of pickup.c + hack.c)
// C ref: pickup.c (4044 lines) + hack.c weight functions
import { game } from './gstate.js';
import { stairway_at } from './stairs.js';
import { impossible, pline, pline1, pline_The, There, You, You_cant, Your } from './pline.js';
import { display_nhwindow, setBotl, newsym, flush_screen, tty_clear_nhwindow, bot } from './display.js';
import { ACURR, ACURRSTR } from './attrib.js';
import { Levitation, Flying, Is_airlevel, Is_waterlevel, Upolyd, Blind, Underwater, Confusion, Stunned } from './macros.js';
import { bigmonst, digests, hides_under, is_rider, nohands, nolimbs, notake, poly_when_stoned, stagger, strongmonst, throws_rocks } from './mondata.js';
import { AMULET_OF_YENDOR, BAG_OF_HOLDING, BAG_OF_TRICKS, BELL_OF_OPENING, BOULDER, CANDELABRUM_OF_INVOCATION, CHEST, COIN_CLASS, CORPSE, GOLD_PIECE, ICE_BOX, Is_box, Is_container, LARGE_BOX, LEASH, LOADSTONE, OILSKIN_SACK, POTION_CLASS, POT_WATER, SACK, SchroedingersBox, SCR_SCARE_MONSTER, SPE_BOOK_OF_THE_DEAD, STATUE, TOOL_CLASS, TOWEL } from './objects.js';
import { MZ_HUMAN, PM_STONE_GOLEM, mons } from './monsters.js';
import { A_CON, A_STR, A_WIS, AUTOUNLOCK_APPLY_KEY, AUTOUNLOCK_UNTRAP, BY_NEXTHERE, CXN_ARTICLE, CXN_SINGULAR, D_BROKEN, D_CLOSED, D_ISOPEN, D_LOCKED, ECMD_OK, ECMD_TIME, engulfing_u, FUMBLING, GETOBJ_ALLOWCNT, GETOBJ_EXCLUDE, GETOBJ_EXCLUDE_SELECTABLE, GETOBJ_PROMPT, GETOBJ_SUGGEST, HAND, Has_contents, ICE, IS_ALTAR, IS_DOOR, IS_FOUNTAIN, IS_FURNITURE, IS_GRAVE, IS_LAVA, is_pit, IS_POOL, IS_SINK, IS_THRONE, isok, LEFT_SIDE, LOST_DROPPED, LOST_EXPLODING, LOST_STOLEN, LOST_THROWN, MENU_ITEMFLAGS_NONE, MENU_ITEMFLAGS_SELECTED, MENU_ITEMFLAGS_SKIPINVERT, OBJ_AT, OBJ_FLOOR, OBJ_INVENT, OBJ_MINVENT, PICK_ANY, PICK_NONE, PICK_ONE, RIGHT_SIDE, STAIRS, STONE, W_ACCESSORY, W_ARMOR, WOUNDED_LEGS } from './const.js';
import { addinv, carrying, delobj, merge_choice, merged, nxtobj, prinv,
         freeinv, carried, getobj, observe_object } from './invent.js';
import { add_to_container, obj_extract_self, splitobj, unbless, unsplitobj, weight } from './mkobj.js';
import { Tobjnam, Yname2, Ysimple_name2, an, ansimpleoname, corpse_xname, cxname_singular, doname, doname_with_price, killer_xname, otense, safe_qbuf, the, thesimpleoname, xname, yname, ysimple_name } from './objnam.js';
import { nomul } from './cmd.js';
import { read_engr_at } from './engrave.js';
import { addtobill, costly_spot, remote_burglary, sellobj, sellobj_state } from './shk.js';
import { is_pool, is_lava } from './dbridge.js';
import { can_reach_floor, check_capacity, inv_cnt, money_cnt, pickup_checks } from './hack.js';
// observe_object: moved to invent.js import below
import { dist2, plur, s_suffix, surface } from './hacklib.js';
import { revive_corpse, trycall, Norep } from './do.js';
import { m_at, t_at } from './map_access.js';
import { more, ynFunction, ynq } from './input.js';
import { pushRngLogEntry, rn2 } from './rng.js';
import { create_nhwindow_menu, start_menu, add_menu, add_menu_heading, add_menu_str, end_menu,
         select_menu, destroy_nhwindow_menu } from './menu.js';
import { ATR_NONE, ATR_INVERSE, NO_COLOR } from './terminal.js';
import { setuwep, setuswapwep, setuqwep, welded, weldmsg } from './wield.js';
import { container_contents } from './end.js';
import { hideunder, mon_nam, update_inventory } from './mon.js';
import { obj_is_burning } from './light.js';
import { snuff_lit } from './apply.js';
import { freehand } from './engrave.js';
import { chest_trap, instapetrify, uescaped_shaft, unconscious, uteetering_at_seen_pit } from './trap.js';
import { autokey, pick_lock } from './lock.js';
import { getdir } from './cmd.js';
import { touch_artifact } from './artifact.js';
import { u_safe_from_fatal_corpse } from './eat.js';
import { polymon } from './polyself.js';
import { exercise } from './attrib.js';
import { back_on_ground } from './trap.js';
import { useupf } from './eat.js';
import { def_oc_syms } from './symbols.js';
import { ceiling } from './dig.js';
// ── Shop sell constants (from hack.h) ──
const SELL_NORMAL = 0;
const SELL_DELIBERATE = 1;
const SELL_DONTSELL = 2;
// ── Encumbrance constants (from const.js / hack.h) ──
const UNENCUMBERED = 0;
const SLT_ENCUMBER = 1;
const MOD_ENCUMBER = 2;
const HVY_ENCUMBER = 3;
const EXT_ENCUMBER = 4;
const OVERLOADED = 5;
// Weight capacity constants
const WT_WEIGHTCAP_STRCON = 25;
const WT_WEIGHTCAP_SPARE = 50;
const MAX_CARR_CAP = 1000;
const WT_HUMAN = 1450;
const WT_WOUNDEDLEG_REDUCT = 100;
// MZ_HUMAN imported from monsters.js (was incorrectly hardcoded as 4, correct value is 2)
export { UNENCUMBERED, SLT_ENCUMBER, MOD_ENCUMBER, HVY_ENCUMBER,
         EXT_ENCUMBER, OVERLOADED };
// C: pickup.c load prefix strings
const slightloadpfx = "You have a little trouble";
const moderateloadpfx = "You have trouble";
const nearloadpfx = "You have much trouble";
const overloadpfx = "You have extreme difficulty";
const DEFAULT_INV_ORDER = '$")[%?+!=/(*00_';
const INVORDER_SYM_TO_CLASS = { ')': 2, '[': 3, '=': 4, '"': 5, '(': 6, '%': 7,
    '!': 8, '?': 9, '+': 10, '/': 11, '*': 13, '`': 14, '0': 15, '_': 16, '$': 12 };
const MENU_CLASS_NAMES = {
    2: 'Weapons', 3: 'Armor', 4: 'Rings', 5: 'Amulets',
    6: 'Tools', 7: 'Comestibles', 8: 'Potions', 9: 'Scrolls',
    10: 'Spellbooks', 11: 'Wands', 12: 'Coins', 13: 'Gems/Stones',
    14: 'Boulders/Statues', 15: 'Iron balls', 16: 'Chains',
};
// ── ACURRSTR: strength for weight capacity ──
// C ref: attrib.h — exceptional strength above 18
// ACURRSTR: imported from attrib.js
function GOLD_WT(n) { return Math.trunc((n + 50) / 100); }
function GOLD_CAPACITY(w, n) { return (w * -100) - (n + 50) - 1; }
// invlet_basic: 52 (a-z, A-Z)
const invlet_basic = 52;
// ── weight_cap: maximum carrying capacity ──
// C ref: hack.c:4224
export function weight_cap() {
    let carrcap = WT_WEIGHTCAP_STRCON * (ACURRSTR() + ACURR(A_CON))
                  + WT_WEIGHTCAP_SPARE;
    if (Upolyd()) {
        const data = game.youmonst?.data;
        if (data) {
            if (!data.cwt) {
                carrcap = Math.trunc(carrcap * (data.msize || 0) / MZ_HUMAN);
            } else if (!strongmonst(data) || data.cwt > WT_HUMAN) {
                carrcap = Math.trunc(carrcap * data.cwt / WT_HUMAN);
            }
        }
    }
    if (Levitation() || Is_airlevel(game.u?.uz)
        || (game.u?.usteed && strongmonst(game.u.usteed.data))) {
        carrcap = MAX_CARR_CAP;
    } else {
        if (carrcap > MAX_CARR_CAP) carrcap = MAX_CARR_CAP;
        if (!Flying()) {
            const ewounded = game.u?.uprops?.[WOUNDED_LEGS]?.extrinsic || 0;
            if (ewounded & LEFT_SIDE) carrcap -= WT_WOUNDEDLEG_REDUCT;
            if (ewounded & RIGHT_SIDE) carrcap -= WT_WOUNDEDLEG_REDUCT;
        }
    }
    return Math.max(carrcap, 1);
}
// ── inv_weight: how far beyond capacity ──
// C ref: hack.c:4280
// Returns negative if below capacity, positive if over.
export function inv_weight() {
    let wt = 0;
    for (let otmp = game.invent; otmp; otmp = otmp.nobj) {
        if (otmp.oclass === COIN_CLASS) {
            wt += Math.trunc(((otmp.quan || 1) + 50) / 100);
        } else {
            // C: skip boulders if hero throws rocks
            if (otmp.otyp !== BOULDER || !throws_rocks(game.youmonst?.data)) {
                wt += otmp.owt || 0;
            }
        }
    }
    game._wc = weight_cap();
    return wt - game._wc;
}
// ── calc_capacity: encumbrance level (0-5) ──
// C ref: hack.c:4301
export function calc_capacity(xtra_wt) {
    const wt = inv_weight() + (xtra_wt || 0);
    if (wt <= 0) return UNENCUMBERED;
    const wc = game._wc || 1;
    if (wc <= 1) return OVERLOADED;
    const cap = Math.trunc(wt * 2 / wc) + 1;
    return Math.min(cap, OVERLOADED);
}
// ── near_capacity: current encumbrance level ──
// C ref: hack.c:4314
export function near_capacity() {
    return calc_capacity(0);
}
// ── max_capacity: weight until overloaded ──
// C ref: hack.c:4319
export function max_capacity() {
    const wt = inv_weight();
    return wt - 2 * (game._wc || 1);
}
// ── encumber_msg: display encumbrance change message ──
// C ref: pickup.c:1972
// No RNG. Compares old/new capacity and shows message.
export async function encumber_msg() {
    const newcap = near_capacity();
    const oldcap = game._oldcap ?? 0;
    if (oldcap < newcap) {
        switch (newcap) {
        case SLT_ENCUMBER:
            await Your('movements are slowed slightly because of your load.');
            break;
        case MOD_ENCUMBER:
            await You('rebalance your load.  Movement is difficult.');
            break;
        case HVY_ENCUMBER:
            await You(stagger(game.youmonst?.data, 'stagger')
                      + ' under your heavy load.  Movement is very hard.');
            break;
        default:
            await You(`${newcap === EXT_ENCUMBER
                ? "can barely" : "can't even"} move a handspan with this load!`);
            break;
        }
        setBotl('encumber_msg');
    } else if (oldcap > newcap) {
        switch (newcap) {
        case UNENCUMBERED:
            await Your('movements are now unencumbered.');
            break;
        case SLT_ENCUMBER:
            await Your('movements are only slowed slightly by your load.');
            break;
        case MOD_ENCUMBER:
            await You('rebalance your load.  Movement is still difficult.');
            break;
        case HVY_ENCUMBER:
            await You(stagger(game.youmonst?.data, 'stagger')
                      + ' under your load.  Movement is still very hard.');
            break;
        }
        setBotl('encumber_msg');
    }
    game._oldcap = newcap;
}
// ── container_at: is there a container at position? ──
// C ref: pickup.c:2017
export function container_at(x, y, countem) {
    let container_count = 0;
    // Walk the nexthere chain at (x,y)
    for (let cobj = fobj_at(x, y); cobj; cobj = cobj.nexthere) {
        if (Is_container(cobj)) {
            container_count++;
            if (!countem) break;
        }
    }
    return container_count;
}
// ── Helper: find head of nexthere chain at (x,y) ──
// C equivalent: level.objects[x][y]
// In JS, game.level.objects is a flat array; the last entry at (x,y) is the chain head.
export function fobj_at(x, y) {
    const objs = game.level?.objects;
    if (!objs) return null;
    for (let i = objs.length - 1; i >= 0; i--) {
        if (objs[i].ox === x && objs[i].oy === y) return objs[i];
    }
    return null;
}
// ── OBJ_AT: are there objects at position? ──
// ── FOLLOW: traverse object list by nexthere or nobj ──
function FOLLOW(curr, flags) {
    return (flags & BY_NEXTHERE) ? curr.nexthere : curr.nobj;
}
function getPickupInvOrder() {
    const order = game.flags?.inv_order;
    return (typeof order === 'string' && order.length) ? order : DEFAULT_INV_ORDER;
}
// C ref: invent.c:309 loot_xname — name for sorting, strips BUC/dilution/etc.
function loot_xname(obj) {
    if (!obj) return '';
    // Save and suppress sort-irrelevant properties
    const save = {
        odiluted: obj.odiluted,
        blessed: obj.blessed,
        cursed: obj.cursed,
        spe: obj.spe,
        owt: obj.owt,
    };
    if (obj.oclass === POTION_CLASS) {
        obj.odiluted = 0;
        if (obj.otyp === POT_WATER) {
            obj.blessed = false;
            obj.cursed = false;
        }
    }
    if (obj.otyp === TOWEL) obj.spe = 0;
    if (obj.globby) obj.owt = 20;
    let res = cxname_singular(obj);
    // Restore all saved properties
    obj.odiluted = save.odiluted;
    obj.blessed = save.blessed;
    obj.cursed = save.cursed;
    obj.spe = save.spe;
    obj.owt = save.owt;
    // C ref: towel suffix for sort ordering (wet first, dry last)
    if (obj.otyp === TOWEL)
        res += (obj.spe >= 3) ? 'x' : (obj.spe > 0) ? 'y' : 'z';
    // C ref: glob suffix for size ordering (small first)
    if (obj.globby)
        res += (obj.owt <= 100) ? 'a' : (obj.owt <= 300) ? 'b'
             : (obj.owt <= 500) ? 'c' : 'd';
    return res;
}
function addPickupQueryObjlistMenu(win, objchain_head, traverse_how) {
    const sortpack = game.flags?.sortpack !== false;
    // C ref: query_objlist() path expects loot-name ordering unless explicitly
    // disabled; treat missing sortloot as 'l' (loot) rather than unsorted.
    const sortloot = game.flags?.sortloot ?? 'l';
    const sortByLootName = (sortloot === 'l' || sortloot === 'f');
    const objs = [];
    for (let obj = objchain_head; obj; obj = FOLLOW(obj, traverse_how)) {
        objs.push(obj);
    }
    if (objs.length === 0) return;
    // C ref: pickup.c query_objlist() uses sortloot(); for default sortloot='l',
    // items are sorted by class (via sortpack ordering), then by name within
    // class (using loot_xname which strips BUC/quantity for sort key), with
    // stable tiebreak preserving original chain order.
    const origPos = new Map(objs.map((obj, idx) => [obj, idx]));
    if (sortByLootName) {
        // Cache loot_xname results for stable sort performance
        const nameCache = new Map();
        const getName = (obj) => {
            if (!nameCache.has(obj)) nameCache.set(obj, loot_xname(obj).toLowerCase());
            return nameCache.get(obj);
        };
        // C ref: invent.c:295-301 sortloot() discovery status ordering.
        // Lower value sorts first:
        //   1 unseen, 2 seen+undiscovered, 3 seen+named, 4 discovered.
        const getDiscoRank = (obj) => {
            const od = game.objects?.[obj.otyp] || {};
            const seen = !!obj.dknown;
            const discovered = !!od.oc_name_known;
            if (!seen) return 1;
            if (discovered || !od.oc_descr) return 4;
            if (od.oc_uname) return 3;
            return 2;
        };
        objs.sort((a, b) => {
            const da = getDiscoRank(a);
            const db = getDiscoRank(b);
            if (da !== db) return da - db;
            const na = getName(a);
            const nb = getName(b);
            if (na < nb) return -1;
            if (na > nb) return 1;
            return (origPos.get(a) ?? 0) - (origPos.get(b) ?? 0);
        });
    }
    let first = true;
    if (sortpack) {
        const invOrder = getPickupInvOrder();
        const buckets = new Map();
        for (const obj of objs) {
            const cls = obj.oclass;
            if (!buckets.has(cls)) buckets.set(cls, []);
            buckets.get(cls).push(obj);
        }
        for (const sym of invOrder) {
            const cls = INVORDER_SYM_TO_CLASS[sym];
            const bucket = cls != null ? buckets.get(cls) : null;
            if (!bucket || !bucket.length) continue;
            add_menu_heading(win, MENU_CLASS_NAMES[cls] || `Class ${cls}`);
            for (const obj of bucket) {
                const selector = (first && obj.oclass === COIN_CLASS) ? '$' : 0;
                add_menu(win, obj, selector, 0, ATR_NONE, NO_COLOR,
                    doname_with_price(obj), MENU_ITEMFLAGS_NONE);
                first = false;
            }
            buckets.delete(cls);
        }
        for (const [cls, bucket] of buckets.entries()) {
            if (!bucket.length) continue;
            add_menu_heading(win, MENU_CLASS_NAMES[cls] || `Class ${cls}`);
            for (const obj of bucket) {
                const selector = (first && obj.oclass === COIN_CLASS) ? '$' : 0;
                add_menu(win, obj, selector, 0, ATR_NONE, NO_COLOR,
                    doname_with_price(obj), MENU_ITEMFLAGS_NONE);
                first = false;
            }
        }
    } else {
        for (const obj of objs) {
            const selector = (first && obj.oclass === COIN_CLASS) ? '$' : 0;
            add_menu(win, obj, selector, 0, ATR_NONE, NO_COLOR,
                doname_with_price(obj), MENU_ITEMFLAGS_NONE);
            first = false;
        }
    }
}
// ── Stub functions for not-yet-ported features ──
 // TODO
 // TODO
// C ref: pickup.c:285 fatal_corpse_mistake — touching petrifying corpses
async function fatal_corpse_mistake(obj, remotely) {
    if (u_safe_from_fatal_corpse(obj, 0x7) || remotely)
        return false;
    if (poly_when_stoned(game.youmonst?.data) && await polymon(PM_STONE_GOLEM)) {
        await display_nhwindow('message', false);
        return false;
    }
    await pline(`Touching ${corpse_xname(obj, null, CXN_SINGULAR | CXN_ARTICLE)} is a fatal mistake.`);
    await instapetrify(killer_xname(obj));
    return true;
}
// C ref: pickup.c:302 rider_corpse_revival — Rider corpse awakens at touch
async function rider_corpse_revival(obj, remotely) {
    if (!obj || obj.otyp !== CORPSE || !is_rider(mons[obj.corpsenm]))
        return false;
    await pline(`At your ${remotely ? 'attempted acquisition' : 'touch'}, the corpse suddenly moves...`);
    // C: revive_corpse(obj) — full revival
    await revive_corpse(obj);
    await exercise(A_WIS, false);
    return true;
}
function check_autopickup_exceptions(_obj) { return null; } // TODO: port from pickup.c
// useupf: imported from eat.js
// hideunder: imported from mon.js
// C ref: invent.c:4037 dfeature_at — terrain feature name at position
function dfeature_at(x, y) {
    const loc = game.level?.at(x, y);
    if (!loc) return null;
    const ltyp = loc.typ ?? STONE;
    if (IS_DOOR(ltyp)) {
        const mask = loc.doormask ?? loc.flags ?? 0;
        if (mask & D_BROKEN) return 'broken door';
        if (mask & D_ISOPEN) return 'open door';
        if ((mask & (D_CLOSED | D_LOCKED)) === 0) return 'doorway';
        return 'closed door';
    }
    if (ltyp === STAIRS) {
        // C ref: stairs.c:187 stairs_description — includes up/down and
        // destination level if the hero has traversed this stairway before.
        const stway = stairway_at(x, y);
        if (stway) {
            // C ref: stairs.c:211-219 — level 1 up stairs go "out of the dungeon"
            const uz = game.u?.uz;
            if (uz?.dnum === 0 && uz?.dlevel === 1 && stway.up) {
                return 'staircase up out of the dungeon';
            }
            let desc = stway.up ? 'staircase up' : 'staircase down';
            if (stway.u_traversed && stway.tolev) {
                const dng = game.dungeons?.[stway.tolev.dnum];
                const toLev = (dng?.depth_start ?? 0) + (stway.tolev.dlevel ?? 1) - 1;
                desc += ` to level ${toLev}`;
            }
            return desc;
        }
        return 'staircase';
    }
    if (IS_FOUNTAIN(ltyp)) return 'fountain';
    if (IS_SINK(ltyp)) return 'sink';
    if (IS_THRONE(ltyp)) return 'opulent throne';
    if (IS_GRAVE(ltyp)) return 'headstone';
    if (IS_ALTAR(ltyp)) return 'altar';
    if (IS_POOL(ltyp)) return 'pool of water';
    if (IS_LAVA(ltyp)) return 'molten lava';
    return null;
}
// C ref: pickup.c:317 force_decor
export async function force_decor(via_probing) {
    const g = game;
    const u = g.u;
    if (!g.iflags) g.iflags = {};
    
    /* we don't want describe_decor() to defer feedback if hero is fumbling
       with 1 turn left until next slip_or_trip(), or for ice_descr() to
       omit thawing details if hero is probing when levitating while blind */
    g.iflags.decor_fumble_override = true;
    g.iflags.decor_levitate_override = via_probing;
    
    /* force current terrain to be different from previous location */
    g.iflags.prev_decor = STONE;
    
    await describe_decor();
    
    g.iflags.decor_fumble_override = false;
    g.iflags.decor_levitate_override = false;
    
    if (!g.level.lastseentyp) g.level.lastseentyp = [];
    if (!g.level.lastseentyp[u.ux]) g.level.lastseentyp[u.ux] = [];
    g.level.lastseentyp[u.ux][u.uy] = g.level.at(u.ux, u.uy).typ;
}
export async function describe_decor() {
    const g = game;
    const u = g.u;
    const loc = g.level?.at(u.ux, u.uy);
    if (!loc) return false;
    const ltyp = loc.typ ?? STONE;
    const prevDecor = g.iflags?.prev_decor ?? STONE;
    // C ref: pickup.c:360 — fumbling check
    const HFumbling = u.uprops?.[FUMBLING]?.intrinsic || 0;
    const TIMEOUT = 0x00FFFFFF; // C: property.h
    if ((HFumbling & TIMEOUT) === 1 && !g.iflags?.defer_decor && !g.iflags?.decor_fumble_override) {
        await deferred_decor(true);
        return false;
    }
    let dfeature = null;
    if (IS_DOOR(ltyp)) {
        const mask = loc.doormask ?? loc.flags ?? 0;
        if (mask & D_BROKEN) dfeature = 'broken door';
        else if (mask & D_ISOPEN) dfeature = 'open door';
        else if ((mask & (D_CLOSED | D_LOCKED)) === 0) dfeature = 'doorway';
        else dfeature = 'door';
    } else if (ltyp === STAIRS) {
        const stway = stairway_at(u.ux, u.uy);
        if (stway) {
            const uz = game.u?.uz;
            if (uz?.dnum === 0 && uz?.dlevel === 1 && stway.up) {
                dfeature = 'staircase up out of the dungeon';
            } else {
                dfeature = stway.up ? 'staircase up' : 'staircase down';
                if (stway.u_traversed && stway.tolev) {
                    const dng = game.dungeons?.[stway.tolev.dnum];
                    const toLev = (dng?.depth_start ?? 0) + (stway.tolev.dlevel ?? 1) - 1;
                    dfeature += ` to level ${toLev}`;
                }
            }
        } else {
            dfeature = 'staircase';
        }
    } else if (IS_FOUNTAIN(ltyp)) {
        dfeature = 'fountain';
    } else if (IS_SINK(ltyp)) {
        dfeature = 'sink';
    } else if (IS_THRONE(ltyp)) {
        dfeature = 'opulent throne';
    } else if (IS_GRAVE(ltyp)) {
        dfeature = 'headstone';
    } else if (IS_ALTAR(ltyp)) {
        dfeature = 'altar';
    } else if (IS_POOL(ltyp)) {
        dfeature = 'pool of water';
    } else if (IS_LAVA(ltyp)) {
        dfeature = 'molten lava';
    }
    const doorhere = dfeature && (dfeature === 'open door' || dfeature === 'doorway');
    if (doorhere || Underwater() || (ltyp === ICE && IS_POOL(prevDecor))) {
        dfeature = null;
    }
    let res = true;
    if (ltyp === prevDecor && !IS_FURNITURE(ltyp)) {
        res = false;
    } else if (dfeature) {
        await pline(`There is ${an(dfeature)} here.`);
    } else if (!Underwater()) {
        if (IS_POOL(prevDecor) || IS_LAVA(prevDecor) || prevDecor === ICE) {
            await back_on_ground(false);
        }
    }
    if (!g.iflags) g.iflags = {};
    g.iflags.prev_decor = g.flags?.mention_decor ? ltyp : STONE;
    return res;
}
// C ref: pickup.c:338 deferred_decor
export async function deferred_decor(setup) {
    const g = game;
    if (!g.flags?.mention_decor) {
        g.iflags.defer_decor = false;
    } else if (setup) {
        g.iflags.defer_decor = true;
    } else {
        await describe_decor();
        g.iflags.defer_decor = false;
    }
}
function newsym_force(x, y) { newsym(x, y); } // TODO: port full newsym_force from display.c
// C ref: pickup.c:475 add_valid_menu_class()
// Manages the valid_menu_classes filter for item selection menus.
export function add_valid_menu_class(c) {
    if (!game._valid_menu_classes) game._valid_menu_classes = [];
    if (c === 0) {
        game._valid_menu_classes = [];
        game._class_filter = false;
        game._bucx_filter = false;
        game._shop_filter = false;
        game._picked_filter = false;
    } else if (!game._valid_menu_classes.includes(c)) {
        game._valid_menu_classes.push(c);
        if ('BUCX'.includes(String.fromCharCode(c))) game._bucx_filter = true;
        else if (c === 'P'.charCodeAt(0)) game._picked_filter = true;
        else if (c === 'u'.charCodeAt(0)) game._shop_filter = true;
        else game._class_filter = true;
    }
}
// Safe string formatting helpers matching C's safe_qbuf / an / something
const something = "something";
// ── reset_justpicked: clear pickup_prev flags ──
// C ref: pickup.c:616
export function reset_justpicked(olist) {
    for (let otmp = olist; otmp; otmp = otmp.nobj) {
        otmp.pickup_prev = 0;
    }
}
// ── autopick_testobj: test if object should be auto-picked up ──
// C ref: pickup.c:929
export function autopick_testobj(otmp, calc_costly) {
    const g = game;
    const otypes = g.flags?.pickup_types || '';
    // calculate 'costly' just once for a given autopickup operation
    if (calc_costly) {
        game.costly_cached = (otmp.where === OBJ_FLOOR
                          && costly_spot(otmp.ox, otmp.oy));
    }
    // first check: reject if an unpaid item in a shop
    if (game.costly_cached && !otmp.no_charge)
        return false;
    // pickup_thrown/pickup_stolen/nopick_dropped override pickup_types and exceptions
    if ((g.flags?.pickup_thrown && otmp.how_lost === LOST_THROWN)
        || (g.flags?.pickup_stolen && otmp.how_lost === LOST_STOLEN))
        return true;
    if (g.flags?.nopick_dropped && otmp.how_lost === LOST_DROPPED)
        return false;
    if (otmp.how_lost === LOST_EXPLODING)
        return false;
    // check for pickup_types
    const classSym = def_oc_syms[otmp.oclass]?.sym || '\0';
    let pickit = (!otypes || otypes.indexOf(classSym) >= 0);
    // check for autopickup exceptions
    const ape = check_autopickup_exceptions(otmp);
    if (ape)
        pickit = ape.grab;
    return pickit;
}
// ── autopick: build list of objects to auto-pick up ──
// C ref: pickup.c:974
function autopick(olist, follow) {
    let check_costly = true;
    const pick_list = [];
    // first count eligible items (and populate list)
    for (let curr = olist; curr; curr = FOLLOW(curr, follow)) {
        if (autopick_testobj(curr, check_costly)) {
            pick_list.push({ obj: curr, count: curr.quan });
        }
        check_costly = false;
    }
    return pick_list;
}
// ── delta_cwt: weight change when removing obj from container ──
// C ref: pickup.c:1546
function delta_cwt(container, obj) {
    if (!container) return 0;
    // Simplified: for bags of holding, weight is reduced
    // TODO: full delta_cwt with bag-of-holding weight reduction
    return obj.owt || 0;
}
// ── carry_count: how many of obj can be carried ──
// C ref: pickup.c:1569
// Returns { count, wt_before, wt_after }
async function carry_count(obj, container, count, telekinesis) {
    const is_gold = obj.oclass === COIN_CLASS;
    const adjust_wt = container && carried_obj(container);
    const savequan = obj.quan;
    const saveowt = obj.owt;
    const umoney = money_cnt(game.invent);
    const iw = max_capacity();
    if (count !== savequan) {
        obj.quan = count;
        obj.owt = weight(obj);
    }
    let wt = iw + obj.owt;
    if (adjust_wt)
        wt -= delta_cwt(container, obj);
    if (is_gold)
        wt -= (GOLD_WT(umoney) + GOLD_WT(count) - GOLD_WT(umoney + count));
    if (count !== savequan) {
        obj.quan = savequan;
        obj.owt = saveowt;
    }
    const wt_before = iw;
    let wt_after = wt;
    if (wt < 0)
        return { count, wt_before, wt_after };
    // see how many we can lift
    let qq;
    if (is_gold) {
        let iw2 = iw - GOLD_WT(umoney);
        if (!adjust_wt) {
            qq = GOLD_CAPACITY(iw2, umoney);
        } else {
            let oow = 0;
            qq = 50 - (umoney % 100) - 1;
            if (qq < 0) qq += 100;
            for (; qq <= count; qq += 100) {
                obj.quan = qq;
                obj.owt = GOLD_WT(qq);
                let ow = GOLD_WT(umoney + qq);
                ow -= delta_cwt(container, obj);
                if (iw2 + ow >= 0) break;
                oow = ow;
            }
            iw2 -= oow;
            qq -= 100;
        }
        if (qq < 0) qq = 0;
        else if (qq > count) qq = count;
        wt = iw2 + GOLD_WT(umoney + qq);
    } else if (count > 1 || count < obj.quan) {
        let ow;
        for (qq = 1; qq <= count; qq++) {
            obj.quan = qq;
            obj.owt = weight(obj);
            ow = obj.owt;
            if (adjust_wt)
                ow -= delta_cwt(container, obj);
            if (iw + ow >= 0)
                break;
            wt = iw + ow;
        }
        --qq;
    } else {
        // there's only one, and we can't lift it
        qq = 0;
    }
    obj.quan = savequan;
    obj.owt = saveowt;
    if (qq < count) {
        // some message will be given
        const obj_nambuf = doname(obj);
        let where, verb;
        if (container) {
            where = `in ${the(xname(container))}`;
            verb = "carry";
        } else {
            where = "lying here";
            verb = telekinesis ? "acquire" : "lift";
        }
        if (qq > 0) {
            await You(`can only ${verb} ${qq === 1 ? 'one' : 'some'} of the ${obj_nambuf} ${where}.`);
            wt_after = wt;
            return { count: qq, wt_before, wt_after };
        }
        // can't lift any
        const where2 = container ? where : "here";
        let prefx1, prefx2, suffx;
        if (game.invent || umoney) {
            prefx1 = "you cannot ";
            prefx2 = "";
            suffx = " any more";
        } else {
            prefx1 = (obj.quan === 1) ? "it " : "even one ";
            prefx2 = "is too heavy for you to ";
            suffx = "";
        }
        await pline(`There ${otense(obj, 'are')} ${obj_nambuf} ${where2}, but ${prefx1}${prefx2}${verb}${suffx}.`);
        return { count: 0, wt_before, wt_after: iw };
    }
    wt_after = wt;
    return { count: qq, wt_before, wt_after };
}
// Helper: is the object carried in hero inventory?
function carried_obj(obj) {
    return obj && obj.where === 3; // OBJ_INVENT
}
// Helper: "the <foo>"
// ── lift_object: determine whether character is able and willing to carry obj ──
// C ref: pickup.c:1704
// Returns: 1 = lift, 0 = skip, -1 = stop
async function lift_object(obj, container, count_val, telekinesis) {
    const g = game;
    // C: Sokoban macro — checks if on Sokoban level
    const isSokoban = false; // TODO: port Sokoban level check
    if (obj.otyp === BOULDER && isSokoban) {
        await You(`cannot get your hand around this ${xname(obj)}.`);
        return { result: -1, count: count_val };
    }
    // override weight consideration for loadstone or boulder when giant
    if (obj.otyp === LOADSTONE
        || (obj.otyp === BOULDER && throws_rocks(g.youmonst?.data))) {
        if (inv_cnt(false) < invlet_basic || !carrying(obj.otyp)
            || merge_choice(g.invent, obj))
            return { result: 1, count: count_val }; // lift regardless
        await You(`are carrying too much stuff to pick up ${obj.quan === 1 ? 'another' : 'more'} ${xname(obj)}.`);
        return { result: -1, count: count_val };
    }
    const cc = await carry_count(obj, container, count_val, telekinesis);
    let count = cc.count;
    let result;
    if (count < 1) {
        result = -1; // nothing lifted
    } else if (obj.oclass !== COIN_CLASS
               && inv_cnt(false) >= invlet_basic
               && !merge_choice(g.invent, obj)) {
        const goldSuffix = nxtobj(obj, GOLD_PIECE, obj.where === OBJ_FLOOR)
            ? " (except gold)" : "";
        await Your(`knapsack cannot accommodate any more items${goldSuffix}.`);
        result = -1; // nothing lifted
    } else {
        result = 1;
        let prev_encumbr = near_capacity();
        // C ref: options.c:7212 — pickup_burden defaults to MOD_ENCUMBER (2)
        const pickup_burden = g.flags?.pickup_burden ?? MOD_ENCUMBER;
        if (prev_encumbr < pickup_burden)
            prev_encumbr = pickup_burden;
        const next_encumbr = calc_capacity(cc.wt_after - cc.wt_before);
        if (next_encumbr > prev_encumbr) {
            if (telekinesis) {
                result = 0; // don't lift
            } else {
                const prefix = (next_encumbr >= EXT_ENCUMBER) ? overloadpfx
                    : (next_encumbr >= HVY_ENCUMBER) ? nearloadpfx
                      : (next_encumbr >= MOD_ENCUMBER) ? moderateloadpfx
                        : slightloadpfx;
                const savequan = obj.quan;
                obj.quan = count;
                const qbuf = await safe_qbuf('', `${prefix} ${!container ? 'lifting' : 'removing'} `,
                    '.  Continue?', obj, doname, ansimpleoname, something);
                obj.quan = savequan;
                switch (await ynq(qbuf)) {
                case 'q':
                    result = -1;
                    break;
                case 'n':
                    result = 0;
                    break;
                default:
                    break; // 'y' => result == 1
                }
                await tty_clear_nhwindow('message');
            }
        }
    }
    if (obj.otyp === SCR_SCARE_MONSTER && result <= 0 && !container)
        obj.spe = 0;
    return { result, count };
}
// ── pick_obj: do the actual work of picking up from floor to inventory ──
// C ref: pickup.c:1896
export async function pick_obj(otmp) {
    const ox = otmp.ox;
    const oy = otmp.oy;
    const robshop = (!game.u?.uswallow && otmp !== game.u.uball && costly_spot(ox, oy));
    obj_extract_self(otmp);
    newsym(ox, oy);
    // Shop billing
    if (robshop) {
        // TODO: addtobill / remote_burglary for shop items
        await addtobill(otmp, true, false, false);
    }
    const result = addinv(otmp);
    if (robshop) {
        // TODO: remote_burglary if unpaid
    }
    return result;
}
// ── pickup_prinv: print pickup message with encumbrance prefix ──
// C ref: pickup.c:1941
async function pickup_prinv(obj, count, verb) {
    const nearload = near_capacity();
    let prefix = null;
    if (nearload === game._pickup_encumbrance) {
        prefix = null;
    } else {
        prefix = (nearload >= EXT_ENCUMBER) ? overloadpfx
                 : (nearload >= HVY_ENCUMBER) ? nearloadpfx
                   : (nearload >= MOD_ENCUMBER) ? moderateloadpfx
                     : (nearload >= SLT_ENCUMBER) ? slightloadpfx
                       : null;
        game._pickup_encumbrance = nearload;
    }
    const pbuf = prefix ? `${prefix} ${verb}` : '';
    await prinv(pbuf, obj, count);
}
// ── pickup_object: pick up a single object from the floor ──
// C ref: pickup.c:1802
// Returns -1 if caller should break, 0 if nothing picked, 1 if picked something.
export async function pickup_object(obj, count, telekinesis) {
    if (obj.quan < count) {
        impossible(`pickup_object: count ${count} > quan ${obj.quan}?`);
        return 0;
    }
    // In case of auto-pickup, where we haven't had a chance to look at it yet
    if (!Blind())
        observe_object(obj);
    if (obj === game.u.uchain) {
        return 0; // do not pick up attached chain
    } else if (obj.where === OBJ_MINVENT && obj.owornmask !== 0
               && engulfing_u(obj.ocarry)) {
        await You_cant(`pick ${ysimple_name(obj)} up.`);
        return 0;
    } else if (obj.oartifact && !(await touch_artifact(obj, game.youmonst))) {
        return 0;
    } else if (obj.otyp === CORPSE) {
        if (await fatal_corpse_mistake(obj, telekinesis)
            || await rider_corpse_revival(obj, telekinesis))
            return -1;
    } else if (obj.otyp === SCR_SCARE_MONSTER) {
        // Process scare monster scroll special behavior
        // C: carry_count modifies count via pointer; here we reassign
        const cc = await carry_count(obj, null,
                                     count ? count : obj.quan,
                                     false);
        count = cc.count;
        if (count < 1)
            return -1; // couldn't even pick up 1
        if (count > 0 && count < obj.quan)
            obj = splitobj(obj, count);
        if (obj.blessed) {
            unbless(obj);
        } else if (!obj.spe && !obj.cursed) {
            obj.spe = 1;
        } else {
            await pline_The(`scroll${plur(obj.quan)} ${otense(obj, 'turn')} to dust as you ${telekinesis ? 'raise' : 'pick'} ${obj.quan === 1 ? 'it' : 'them'} up.`);
            await trycall(obj);
            useupf(obj, obj.quan);
            return 1; // tried but failed, don't terminate loop
        }
    }
    // Determine if we can/should lift it
    const lo = await lift_object(obj, null, count, telekinesis);
    if (lo.result <= 0) {
        pushRngLogEntry(`DBG_LIFT_FAIL:otyp=${obj.otyp} res=${lo.result}`);
        return lo.result;
    }
    count = lo.count;
    // Gold updates status line
    if (obj.oclass === COIN_CLASS)
        setBotl('pickup_object');
    if (obj.quan !== count && obj.otyp !== LOADSTONE)
        obj = splitobj(obj, count);
    obj = await pick_obj(obj);
    if (game.u.uwep && game.u.uwep === obj)
        game._mrg_to_wielded = true;
    await pickup_prinv(obj, count, "lifting");
    if (obj.ghostly)
        fix_ghostly_obj(obj);
    game._mrg_to_wielded = false;
    return 1;
}
// Stub for ghostly object fixup
function fix_ghostly_obj(_obj) { /* TODO */ }
// ── check_here: look at objects at our location ──
// C ref: pickup.c:428
async function check_here(picked_some) {
    const g = game;
    let ct = 0;
    let skip_dfeature = false;
    if (g.flags?.mention_decor) {
        if (await describe_decor())
            skip_dfeature = true; // describe_decor already showed terrain
    }
    // count the objects here
    for (let obj = fobj_at(g.u.ux, g.u.uy); obj; obj = obj.nexthere) {
        if (obj !== g.u.uchain)
            ct++;
    }
    if (ct) {
        if (g.context?.run)
            nomul(0);
        await flush_screen(1);
        // C ref: invent.c:4185-4208 look_here() blind path.
        // When blind, don't identify floor objects here; emit the generic
        // tactile message instead.
        if (Blind()) {
            await You('try to feel what is lying here on the floor.');
            await more(g.nhDisplay || null);
            if (ct > 1) {
                const qty = (ct === 2) ? 'two'
                    : (ct < 5) ? 'a few'
                        : (ct < 10) ? 'several' : 'many';
                await There(`are ${qty}${picked_some ? ' more' : ''} objects here.`);
            }
            return;
        }
        // C ref: invent.c:4220-4238 — show terrain feature before objects
        if (!skip_dfeature) {
            const dfeature = dfeature_at(g.u.ux, g.u.uy);
            if (dfeature) {
                await pline(`There is ${an(dfeature)} here.`);
            }
        }
        // C ref: invent.c look_here() single-object branch reads engraving
        // before announcing the object.
        if (ct === 1)
            await read_engr_at(g.u.ux, g.u.uy);
        // C ref: invent.c look_here() multi-object branch clears WIN_MESSAGE
        // via display_nhwindow(WIN_MESSAGE, FALSE) before showing the list.
        // Keep message-boundary parity here; full list-window rendering is
        // handled separately from this pickup-path fix.
        if (ct > 1) {
            await display_nhwindow('message', false);
            const win = create_nhwindow_menu();
            start_menu(win);
            // C ref: look_here() uses putstr on a temporary NHW_MENU window,
            // which renders with a "--More--" style prompt.
            win.textLikePrompt = true;
            add_menu_str(win, `${picked_some ? 'Other things' : 'Things'} that are here:`);
            for (let obj = fobj_at(g.u.ux, g.u.uy); obj; obj = obj.nexthere) {
                if (obj !== g.u.uchain) {
                    add_menu_str(win, doname_with_price(obj));
                }
            }
            end_menu(win, null);
            await select_menu(win, PICK_NONE);
            destroy_nhwindow_menu(win);
            await read_engr_at(g.u.ux, g.u.uy);
            return;
        }
        // Single-object path
        for (let obj = fobj_at(g.u.ux, g.u.uy); obj; obj = obj.nexthere) {
            if (obj !== g.u.uchain) {
                await pline(`You see here ${doname_with_price(obj)}.`);
                break;
            }
        }
    } else {
        await read_engr_at(g.u.ux, g.u.uy);
    }
}
// ── pickup: pick up items at hero's feet ──
// C ref: pickup.c:672
//
// Arg what:
//   >0  autopickup
//   =0  interactive
// pickup_checks: imported from hack.js
// ── dopickup — C ref: hack.c:3792 ──
// The '#pickup' / ',' command handler.
export async function dopickup() {
    const count = game.command_count || 0;
    game.multi = 0; // C ref: hack.c:3797
    const ret = await pickup_checks();
    if (ret >= 0)
        return ret ? ECMD_TIME : ECMD_OK;
    if (ret === -2)
        return 0; // loot_mon not ported
    return (await pickup(-count)) ? ECMD_TIME : ECMD_OK;
}
// ── pickup — C ref: pickup.c:710 ──
//   >0  auto-pickup
//   =0  interactive pickup (from ',')
//   <0  pickup count of something
//
// Returns 1 if tried to pick something up, 0 otherwise.
export async function pickup(what) {
    const g = game;
    const u = g.u;
    let n_tried = 0, n_picked = 0;
    const autopickup_mode = what > 0;
    // Skip if fainted/sleeping and got here via teleport
    if (autopickup_mode && (g.multi ?? 0) < 0 && unconscious()) {
        // iflags.prev_decor = STONE; // TODO
        return 0;
    }
    // used by pickup_prinv() for encumbrance feedback
    game._pickup_encumbrance = 0;
    let count;
    if (what < 0)
        count = -what; // pick N of something
    else
        count = 0; // pick anything
    if (!u.uswallow) {
        // no auto-pick if no-pick move, nothing there, or in a pool
        if (autopickup_mode && ((g.context?.nopick) || !OBJ_AT(u.ux, u.uy)
                               || (is_pool(u.ux, u.uy) && !Underwater())
                               || is_lava(u.ux, u.uy))) {
            if (g.flags?.mention_decor)
                await describe_decor();
            await read_engr_at(u.ux, u.uy);
            return 0;
        }
        // no pickup if levitating & not on air/water level
        const t = t_at(u.ux, u.uy);
        if (!can_reach_floor(t && is_pit(t?.ttyp))) {
            await describe_decor();
            if (((g.multi ?? 0) && !g.context?.run)
                || (autopickup_mode && !g.flags?.pickup)
                || (t && (uteetering_at_seen_pit(t) || uescaped_shaft(t))))
                await read_engr_at(u.ux, u.uy);
            return 0;
        }
        // multi && !context.run means in middle of other action
        if (((g.multi ?? 0) && !g.context?.run)
            || (autopickup_mode && !g.flags?.pickup)
            || notake(g.youmonst?.data)) {
            await check_here(false);
            if (notake(g.youmonst?.data) && OBJ_AT(u.ux, u.uy)
                && (autopickup_mode || g.flags?.pickup))
                await You("are physically incapable of picking anything up.");
            return 0;
        }
        // if there's anything here, stop running
        if (OBJ_AT(u.ux, u.uy) && g.context?.run && g.context.run !== 8
            && !g.context?.nopick)
            nomul(0);
    }
    add_valid_menu_class(0); // reset
    // Get the object chain
    let objchain_head;
    let traverse_how;
    if (!u.uswallow) {
        objchain_head = fobj_at(u.ux, u.uy);
        traverse_how = BY_NEXTHERE;
    } else {
        objchain_head = u.ustuck?.minvent ?? null;
        traverse_how = 0; // nobj
    }
    // Autopickup path
    if (autopickup_mode) {
        const pick_list = autopick(objchain_head, traverse_how);
        if (pick_list.length > 0)
            reset_justpicked(g.invent);
        n_tried = pick_list.length;
        for (let i = 0; i < pick_list.length; i++) {
            const res = await pickup_object(pick_list[i].obj, pick_list[i].count,
                                            false);
            if (res < 0)
                break; // can't continue
            n_picked += res;
        }
    } else {
        // Interactive pickup (menu-based or traditional)
        // For now, simplified: pick up everything at feet (single item case)
        let ct = 0;
        for (let obj = objchain_head; obj; obj = FOLLOW(obj, traverse_how))
            ct++;
        if (ct === 1 && count) {
            const obj = objchain_head;
            const lcount = Math.min(obj.quan, count);
            n_tried++;
            reset_justpicked(g.invent);
            if (await pickup_object(obj, lcount, false) > 0)
                n_picked++;
        } else if (ct === 1) {
            // Single item, no count — pick it up
            const obj = objchain_head;
            n_tried++;
            reset_justpicked(g.invent);
            if (await pickup_object(obj, obj.quan, false) > 0)
                n_picked++;
        } else if (ct >= 2) {
            // Multiple items — C uses interactive selection (query_objlist),
            // not auto-pick-everything.
            const win = create_nhwindow_menu();
            start_menu(win);
            addPickupQueryObjlistMenu(win, objchain_head, traverse_how);
            end_menu(win, 'Pick up what?');
            const result = await select_menu(win, PICK_ANY);
            destroy_nhwindow_menu(win);
            if (result.count > 0) {
                reset_justpicked(g.invent);
                for (const item of result.items) {
                    const obj = item.identifier;
                    if (!obj) continue;
                    const qty = (item.count && item.count > 0)
                        ? Math.min(item.count, obj.quan || 1)
                        : (obj.quan || 1);
                    n_tried++;
                    const res = await pickup_object(obj, qty, false);
                    if (res < 0) break;
                    n_picked += res;
                }
            }
        }
    }
    if (!u.uswallow) {
        if (hides_under(g.youmonst?.data))
            hideunder(g.youmonst);
        // position may need updating (invisible hero)
        if (n_picked)
            newsym_force(u.ux, u.uy);
        // check if there's anything else here after auto-pickup is done
        if (autopickup_mode)
            await check_here(n_picked > 0);
    }
    game._pickup_encumbrance = 0;
    add_valid_menu_class(0); // reset
    return (n_tried > 0) ? 1 : 0;
}
// ── Container interaction state ──
// C ref: gc.current_container, ga.abort_looting, gs.sellobj_first
// C ref: pickup.c:64 #define Icebox
function Icebox() {
    return game.current_container && game.current_container.otyp === ICE_BOX;
}
// C ref: pickup.c:2714 ck_bag
export function ck_bag(obj) {
    return (game.current_container && obj !== game.current_container);
}
// Is_box: imported from objects.js
// C ref: obj.h Is_mbag macro
export function Is_mbag(obj) {
    return obj && obj.otyp === BAG_OF_HOLDING;
}
// SchroedingersBox: imported from objects.js
// Simplified Ysimple_name2 (C: objnam.c)
// Ysimple_name2: imported from objnam.js
// ── unsplitobj: merge a split object back ──
// C ref: mkobj.c unsplitobj()
// unsplitobj: imported from mkobj.js
// C ref: pickup.c
function reverse_loot() {
    // TODO: port reverse_loot (puts inventory on floor when confused)
    return false;
}
// ── able_to_loot: can hero loot at position? ──
// C ref: pickup.c:2035
async function able_to_loot(x, y, looting) {
    const verb = looting ? "loot" : "tip";
    const t = t_at(x, y);
    if (!can_reach_floor(t && is_pit(t?.ttyp))) {
        // TODO: usteed/rider_cant_reach
        await You(`cannot ${verb} things that are on the ground.`);
        return false;
    } else if ((is_pool(x, y) && (looting || !Underwater())) || is_lava(x, y)) {
        await You(`cannot ${verb} things that are deep in the ${is_lava(x, y) ? "lava" : "water"}.`);
        return false;
    } else if (nolimbs(game.youmonst?.data)) {
        await pline(`Without limbs, you cannot ${verb} anything.`);
        return false;
    } else if (looting && !freehand()) {
        await pline(`Without a free hand, you cannot loot anything.`);
        return false;
    }
    return true;
}
// ── u_handsy: check for hands/free hand ──
// C ref: pickup.c:2929
async function u_handsy() {
    if (nohands(game.youmonst?.data)) {
        await You("have no hands!");
        return false;
    } else if (!freehand()) {
        await You("have no free hand.");
        return false;
    }
    return true;
}
// ── do_loot_cont: handle one container during #loot ──
// C ref: pickup.c:2082
async function do_loot_cont(cobjRef, cindex, ccount) {
    let cobj = cobjRef.obj;
    if (!cobj)
        return ECMD_OK;
    if (cobj.olocked) {
        let res = ECMD_OK;
        if (cobj.lknown)
            await pline(`${the(xname(cobj)).charAt(0).toUpperCase() + the(xname(cobj)).slice(1)} is locked.`);
        else
            await pline(`Hmmm, ${the(xname(cobj))} turns out to be locked.`);
        cobj.lknown = 1;
        // C ref: pickup.c:2106-2140 — autounlock
        const g = game;
        if (g.flags?.autounlock) {
            const ox = cobj.ox, oy = cobj.oy;
            g.u.dz = 0;
            let unlocktool = null;
            if (((g.flags.autounlock & AUTOUNLOCK_APPLY_KEY) !== 0
                 && (unlocktool = autokey(true)) !== null)
                || (g.flags.autounlock & AUTOUNLOCK_UNTRAP) !== 0) {
                if (await pick_lock(unlocktool, ox, oy, cobj))
                    res = ECMD_TIME;
                // C ref: pickup.c:2123-2128 — check if cobj survived pick_lock
                let found = false;
                for (let otmp = fobj_at(ox, oy); otmp; otmp = otmp.nexthere) {
                    if (otmp === cobj) { found = true; break; }
                }
                if (!found)
                    cobjRef.obj = null;
                return res;
            }
            // TODO: AUTOUNLOCK_FORCE / cmdq_add_ec(doforce) — not yet ported
        }
        return res;
    }
    cobj.lknown = 1; // floor container, no update_inventory needed
    if (cobj.otyp === BAG_OF_TRICKS) {
        await You(`carefully open ${the(xname(cobj))}...`);
        await pline("It develops a huge set of teeth and bites you!");
        // TODO: rnd(10) damage, losehp, makeknown
        game.abort_looting = true;
        return ECMD_TIME;
    }
    return await use_container(cobjRef, false, cindex < ccount);
}
 
async function get_adjacent_loot_loc(prompt, errormsg, sx, sy, cc) {
    if (!(await getdir(prompt))) {
        await pline('Never mind.');
        return false;
    }
    const nx = sx + game.u.dx;
    const ny = sy + game.u.dy;
    if (cc && isok(nx, ny)) {
        cc.x = nx;
        cc.y = ny;
        return true;
    }
    if (errormsg) await pline(errormsg);
    return false;
}
 
function mon_beside(x, y) {
    for (let mtmp = game.fmon; mtmp; mtmp = mtmp.nmon) {
        if (mtmp.dead) continue;
        if (dist2(mtmp.mx, mtmp.my, x, y) <= 2) return true;
    }
    return false;
}
// ── in_or_out_menu: menu for MENU_FULL/MENU_PARTIAL container action ──
// C ref: pickup.c:3384
async function in_or_out_menu(prompt, obj, outokay, inokay, alreadyused, more_containers) {
    const lootchars = "_:oibrsnq";
    const menuselector = lootchars; // TODO: flags.lootabc support
    const win = create_nhwindow_menu();
    start_menu(win);
    // ':' — look inside
    add_menu(win, 1, menuselector[1], 0, ATR_NONE, NO_COLOR,
             `Look inside ${thesimpleoname(obj)}`, MENU_ITEMFLAGS_NONE);
    if (outokay) {
        // 'o' — take out
        add_menu(win, 2, menuselector[2], 0, ATR_NONE, NO_COLOR,
                 "take something out", MENU_ITEMFLAGS_NONE);
    }
    if (inokay) {
        // 'i' — put in
        add_menu(win, 3, menuselector[3], 0, ATR_NONE, NO_COLOR,
                 "put something in", MENU_ITEMFLAGS_NONE);
    }
    if (outokay) {
        // 'b' — both
        add_menu(win, 4, menuselector[4], 0, ATR_NONE, NO_COLOR,
                 `${inokay ? "both; " : ""}take out, then put in`, MENU_ITEMFLAGS_NONE);
    }
    if (inokay) {
        // 'r' — reversed
        add_menu(win, 5, menuselector[5], 0, ATR_NONE, NO_COLOR,
                 `${outokay ? "both reversed; " : ""}put in, then take out`, MENU_ITEMFLAGS_NONE);
        // 's' — stash
        add_menu(win, 6, menuselector[6], 0, ATR_NONE, NO_COLOR,
                 `stash one item into ${thesimpleoname(obj)}`, MENU_ITEMFLAGS_NONE);
    }
    add_menu_str(win, "");
    if (more_containers) {
        // 'n' — next
        add_menu(win, 7, menuselector[7], 0, ATR_NONE, NO_COLOR,
                 "loot next container", MENU_ITEMFLAGS_SELECTED);
    }
    // 'q' — quit/done
    add_menu(win, 8, menuselector[8], 0, ATR_NONE, NO_COLOR,
             alreadyused ? "done" : "do nothing",
             more_containers ? MENU_ITEMFLAGS_NONE : MENU_ITEMFLAGS_SELECTED);
    end_menu(win, prompt);
    const result = await select_menu(win, PICK_ONE);
    destroy_nhwindow_menu(win);
    if (result.count > 0) {
        let k = result.items[0].identifier;
        if (result.count > 1 && k === (more_containers ? 7 : 8))
            k = result.items[1].identifier;
        return lootchars[k]; // :,o,i,b,r,s,n,q
    }
    return (result.count === 0 && more_containers) ? 'n' : 'q';
}
// ── query_category_menu: show class selection for MENU_FULL mode ──
// C ref: pickup.c:1226 query_category
// Only called when multiple object classes are present.
// Returns array of class numbers (ints), or ['A'] for auto-all, or ['all'].
async function query_category_menu(qstr, olist, put_in, classesSeen) {
    const win = create_nhwindow_menu();
    start_menu(win);
    // 'A' — auto-select all
    add_menu(win, 'A', 'A', 0, ATR_NONE, NO_COLOR,
             'Auto-select every relevant item', MENU_ITEMFLAGS_SKIPINVERT);
    // C ref: pickup.c:1330 — hint line for Auto-select
    add_menu_str(win, '    (ignored unless some other choices are also picked)');
    add_menu_str(win, '');
    // 'a' — all types
    add_menu(win, 'all', 'a', 0, ATR_NONE, NO_COLOR,
             'All types', MENU_ITEMFLAGS_SKIPINVERT);
    // Per-class entries matching C's inv_order
    const inv_order = getPickupInvOrder();
    let invlet = 'b'; // 'a' used by "All types"
    for (const ch of inv_order) {
        const cls = INVORDER_SYM_TO_CLASS[ch];
        if (cls == null || !classesSeen.has(cls)) continue;
        add_menu(win, cls, invlet, ch.charCodeAt(0), ATR_NONE, NO_COLOR,
                 MENU_CLASS_NAMES[cls] || `Class ${cls}`, MENU_ITEMFLAGS_NONE);
        invlet = String.fromCharCode(invlet.charCodeAt(0) + 1);
    }
    // C ref: pickup.c:1402-1433 — BUC filter entries
    // Add entries for items with known blessed/cursed/uncursed status
    {
        let hasBlessed = false, hasCursed = false, hasUncursed = false;
        for (let o = olist; o; o = o.nobj) {
            if (o.bknown) {
                if (o.blessed) hasBlessed = true;
                else if (o.cursed) hasCursed = true;
                else hasUncursed = true;
            }
        }
        if (hasBlessed || hasCursed || hasUncursed) {
            add_menu_str(win, '');
        }
        if (hasBlessed) {
            add_menu(win, 'B', 'B', 0, ATR_NONE, NO_COLOR,
                     'Items known to be Blessed', MENU_ITEMFLAGS_SKIPINVERT);
        }
        if (hasCursed) {
            add_menu(win, 'C', 'C', 0, ATR_NONE, NO_COLOR,
                     'Items known to be Cursed', MENU_ITEMFLAGS_SKIPINVERT);
        }
        if (hasUncursed) {
            add_menu(win, 'U', 'U', 0, ATR_NONE, NO_COLOR,
                     'Items known to be Uncursed', MENU_ITEMFLAGS_SKIPINVERT);
        }
    }
    end_menu(win, qstr);
    const result = await select_menu(win, PICK_ANY);
    destroy_nhwindow_menu(win);
    if (result.count <= 0) return [];
    return result.items.map(item => item.identifier);
}
// ── menu_loot: menu-based in/out container operation ──
// C ref: pickup.c:3252
async function menu_loot(retry, put_in) {
    let n_looted = 0;
    let all_categories = true;
    let loot_everything = false;
    const action = put_in ? "Put in" : "Take out";
    game._pickup_encumbrance = 0;
    let autopick = false;
    let selected_classes = null; // null = all
    // C ref: pickup.c:3268 — MENU_FULL uses query_category.
    // query_category shows a class selection menu ONLY when multiple
    // classes are present. With 1 class, it auto-selects (no menu, no keys).
    if (!retry) {
        const objlist = put_in ? game.invent : (game.current_container ? game.current_container.cobj : null);
        const classesSeen = new Set();
        for (let obj = objlist; obj; obj = obj.nobj) {
            if (put_in && obj === game.current_container) continue;
            classesSeen.add(obj.oclass);
        }
        if (classesSeen.size <= 1) {
            // C: ccount == 1 → auto-select the single class (no menu shown)
            all_categories = true;
        } else {
            // Multiple classes: show category menu (consumes keys)
            all_categories = false;
            const qresult = await query_category_menu(
                `${action} what type of objects?`, objlist, put_in, classesSeen);
            if (!qresult || qresult.length === 0) return ECMD_OK;
            selected_classes = [];
            for (const pick of qresult) {
                if (pick === 'A') { loot_everything = autopick = true; selected_classes = null; break; }
                if (pick === 'all') { all_categories = true; selected_classes = null; break; }
                selected_classes.push(pick);
            }
            if (selected_classes === null) all_categories = true;
        }
    }
    if (loot_everything || all_categories || (selected_classes && selected_classes.length > 0)) {
        // Build a menu of items to select from
        const objlist = put_in ? game.invent : (game.current_container ? game.current_container.cobj : null);
        if (!objlist) return ECMD_OK;
        const win = create_nhwindow_menu();
        start_menu(win);
        // C ref: pickup.c:3341-3352 — INVORDER_SORT + class headings (add_menu_heading)
        const inv_order = getPickupInvOrder();
        // Bucket items by oclass
        const classBuckets = {};
        for (let otmp = objlist; otmp; otmp = otmp.nobj) {
            if (put_in && otmp === game.current_container) continue;
            if (selected_classes && !selected_classes.includes(otmp.oclass)) continue;
            if (!classBuckets[otmp.oclass]) classBuckets[otmp.oclass] = [];
            classBuckets[otmp.oclass].push(otmp);
        }
        // C ref: pickup.c:3342 — USE_INVLET only set for put_in with
        // invlet_constant option. For take-out (or put_in without flag),
        // accelerator is 0 (auto-assigned by end_menu) except for the first
        // COIN_CLASS item which uses '$'.
        const useInvlet = put_in && !!(game.flags?.invlet_constant ?? true);
        let firstItemOverall = true;
        const addItem = (otmp) => {
            let accel = 0;
            if (useInvlet && otmp.invlet) {
                accel = otmp.invlet;
            } else if (firstItemOverall && otmp.oclass === COIN_CLASS) {
                accel = '$';
            }
            add_menu(win, otmp, accel, 0, ATR_NONE, NO_COLOR,
                     doname(otmp), MENU_ITEMFLAGS_NONE);
            firstItemOverall = false;
        };
        // Add items in inv_order with class headings
        for (const ch of inv_order) {
            const cls = INVORDER_SYM_TO_CLASS[ch];
            if (cls == null || !classBuckets[cls]) continue;
            // C: add_menu_heading uses iflags.menu_headings.attr = ATR_INVERSE
            add_menu(win, null, 0, 0, ATR_INVERSE, NO_COLOR,
                     MENU_CLASS_NAMES[cls] || `Class ${cls}`, 0);
            for (const otmp of classBuckets[cls]) addItem(otmp);
        }
        // Items of unknown class (not in inv_order) — append at end
        for (const cls in classBuckets) {
            const sym = Object.keys(INVORDER_SYM_TO_CLASS)
                .find(k => INVORDER_SYM_TO_CLASS[k] === +cls);
            if (sym && inv_order.includes(sym)) continue;
            add_menu(win, null, 0, 0, ATR_INVERSE, NO_COLOR,
                     MENU_CLASS_NAMES[cls] || `Class ${cls}`, 0);
            for (const otmp of classBuckets[cls]) addItem(otmp);
        }
        end_menu(win, `${action} what?`);
        const result = await select_menu(win, PICK_ANY);
        destroy_nhwindow_menu(win);
        if (result.count > 0) {
            if (!put_in && game.current_container)
                game.current_container.cknown = 1;
            n_looted = result.count;
            for (let i = 0; i < result.items.length; i++) {
                const otmp_orig = result.items[i].identifier;
                const count = result.items[i].count;
                let otmp = otmp_orig;
                if (count > 0 && count < otmp.quan) {
                    otmp = splitobj(otmp, count);
                }
                let res;
                if (put_in) {
                    res = await in_container(otmp);
                } else {
                    res = await out_container(otmp);
                }
                if (res <= 0) {
                    if (!game.current_container) {
                        break; // container exploded
                    } else if (otmp && otmp !== otmp_orig) {
                        unsplitobj(otmp);
                    }
                    if (res < 0)
                        break;
                }
            }
        }
    }
    return n_looted ? ECMD_TIME : ECMD_OK;
}
// ── stash_ok: getobj callback for stash ──
// C ref: pickup.c:2943
function stash_ok(obj) {
    if (!obj) return GETOBJ_EXCLUDE;
    if (!ck_bag(obj)) return GETOBJ_EXCLUDE_SELECTABLE;
    return GETOBJ_SUGGEST;
}
// ── in_container: put an item into the current container ──
// C ref: pickup.c:2552
// Returns: -1 to stop, 1 item inserted, 0 item not inserted
async function in_container(obj) {
    const floor_container = !carried(game.current_container);
    let was_unpaid = false;
    if (!game.current_container) {
        impossible("<in> no game.current_container?");
        return 0;
    } else if (obj === game.u.uball || obj === game.u.uchain) {
        await You("must be kidding.");
        return 0;
    } else if (obj === game.current_container) {
        await pline("That would be an interesting topological exercise.");
        return 0;
    } else if (obj.owornmask & (W_ARMOR | W_ACCESSORY)) {
        await Norep(`You cannot ${Icebox() ? "refrigerate" : "stash"} something you are wearing.`);
        return 0;
    } else if (obj.otyp === LOADSTONE && obj.cursed) {
        obj.bknown = 1;
        await pline_The(`stone${plur(obj.quan)} won't leave your person.`);
        return 0;
    } else if (obj.otyp === AMULET_OF_YENDOR
               || obj.otyp === CANDELABRUM_OF_INVOCATION
               || obj.otyp === BELL_OF_OPENING
               || obj.otyp === SPE_BOOK_OF_THE_DEAD) {
        await pline(`${the(xname(obj)).charAt(0).toUpperCase() + the(xname(obj)).slice(1)} cannot be confined in such trappings.`);
        return 0;
    } else if (obj.otyp === LEASH && obj.leashmon) {
        await pline(`${Tobjnam(obj, 'are')} attached to your pet.`);
        return 0;
    } else if (obj === game.u.uwep) {
        if (welded(obj)) {
            await weldmsg(obj);
            return 0;
        }
        await setuwep(null);
        if (game.u.uwep) return 0; // unwielded, died, rewielded
    } else if (obj === game.u.uswapwep) {
        setuswapwep(null);
    } else if (obj === game.u.uquiver) {
        setuqwep(null);
    }
    if (await fatal_corpse_mistake(obj, false))
        return -1;
    // boxes, boulders, and big statues can't fit
    if (obj.otyp === ICE_BOX || Is_box(obj) || obj.otyp === BOULDER
        || (obj.otyp === STATUE && bigmonst(game.mons?.[obj.corpsenm]))) {
        const buf = the(xname(obj));
        await You(`cannot fit ${buf} into ${the(xname(game.current_container))}.`);
        return 0;
    }
    freeinv(obj);
    if (obj_is_burning(obj))
        snuff_lit(obj);
    if (floor_container && costly_spot(game.u.ux, game.u.uy)) {
        if (obj.oclass !== COIN_CLASS) {
            was_unpaid = !!obj.unpaid;
            if (game.sellobj_first_flag) {
                sellobj_state(game.current_container.no_charge
                              ? SELL_DONTSELL : SELL_DELIBERATE);
                game.sellobj_first_flag = false;
            }
            sellobj(obj, game.u.ux, game.u.uy);
        }
    }
    // TODO: Icebox age handling, cursed bag of holding explosion
    if (game.current_container) {
        const buf = the(xname(game.current_container));
        await You(`put ${doname(obj)} into ${buf}.`);
        if (floor_container && obj.oclass === COIN_CLASS)
            sellobj(obj, game.current_container.ox, game.current_container.oy);
        add_to_container(game.current_container, obj);
        game.current_container.owt = weight(game.current_container);
    }
    await bot();
    return game.current_container ? 1 : -1;
}
// ── out_container: take an item out of the current container ──
// C ref: pickup.c:2721
// Returns: -1 to stop, 1 item removed, 0 item not removed
async function out_container(obj) {
    let res;
    const is_gold = (obj.oclass === COIN_CLASS);
    if (!game.current_container) {
        impossible("<out> no game.current_container?");
        return -1;
    } else if (is_gold) {
        obj.owt = weight(obj);
    }
    if (obj.oartifact && !(await touch_artifact(obj, game.youmonst)))
        return 0;
    if (await fatal_corpse_mistake(obj, false))
        return -1;
    let count = obj.quan;
    const lo = await lift_object(obj, game.current_container, count, false);
    res = lo.result;
    count = lo.count;
    if (res <= 0) return res;
    if (obj.quan !== count && obj.otyp !== LOADSTONE)
        obj = splitobj(obj, count);
    // Remove from container
    obj_extract_self(obj);
    game.current_container.owt = weight(game.current_container);
    // TODO: Icebox removed_from_icebox handling
    if (!obj.unpaid && !carried(game.current_container)
        && costly_spot(game.current_container.ox, game.current_container.oy)) {
        obj.ox = game.current_container.ox;
        obj.oy = game.current_container.oy;
        await addtobill(obj, false, false, false);
    }
    // TODO: is_pick shopkeeper feedback
    const otmp = addinv(obj);
    await pickup_prinv(otmp, count, "removing");
    if (is_gold) {
        await bot();
    }
    return 1;
}
// ── use_container: main container interaction ──
// C ref: pickup.c:2959
export async function use_container(objRef, held, more_containers) {
    let obj = objRef.obj;
    let quantum_cat, cursed_mbag, loot_out, loot_in, loot_in_first,
        stash_one, inokay, outokay, outmaybe;
    let c;
    let used = ECMD_OK;
    game.abort_looting = false;
    game.sellobj_first_flag = true;
    if (!(await u_handsy()))
        return ECMD_OK;
    if (!obj.lknown) {
        obj.lknown = 1;
        if (held) update_inventory();
    }
    if (obj.olocked) {
        await pline(`${Tobjnam(obj, 'are')} locked.`);
        if (held) await You("must put it down to unlock.");
        return ECMD_OK;
    } else if (obj.otrapped) {
        if (held) await You(`open ${the(xname(obj))}...`);
        await chest_trap(obj, HAND, false);
        if ((game.multi ?? 0) >= 0) {
            nomul(-1);
            game.multi_reason = "opening a container";
            game.nomovemsg = "";
        }
        game.abort_looting = true;
        return ECMD_TIME;
    }
    game.current_container = obj;
    // Schroedinger's Cat
    quantum_cat = SchroedingersBox(game.current_container);
    if (quantum_cat) {
        // TODO: observe_quantum_cat
        used = ECMD_TIME;
    }
    // Cursed bag of holding
    cursed_mbag = Is_mbag(game.current_container)
        && game.current_container.cursed
        && Has_contents(game.current_container);
    if (cursed_mbag) {
        // TODO: boh_loss
    }
    // Can we put things in? (have inventory other than the container)
    inokay = (game.invent !== null
              && (game.invent !== game.current_container || game.invent.nobj));
    // Can we take things out? (container not empty)
    outokay = Has_contents(game.current_container);
    let emptymsg = '';
    if (!outokay) {
        emptymsg = `${Yname2(game.current_container)} is ${(quantum_cat || cursed_mbag) ? "now " : ""}empty.`;
    }
    // Prompt loop: repeats for '?' or ':'
    for (;;) {
        outmaybe = (outokay || !game.current_container.cknown);
        let qbuf;
        if (!outmaybe)
            qbuf = `${Yname2(game.current_container)} is empty.  Do what with it?`;
        else
            qbuf = `Do what with ${yname(game.current_container)}?`;
        // Use menu-based prompt for MENU_PARTIAL/MENU_FULL (default)
        if (!inokay && !outmaybe) {
            c = 'b'; // nothing to take out, nothing to put in
        } else {
            c = await in_or_out_menu(qbuf, game.current_container,
                                     outmaybe, inokay,
                                     used !== ECMD_OK,
                                     more_containers);
        }
        if (c === '?') {
            // TODO: explain_container_prompt
            continue;
        } else if (c === ':') {
            if (!game.current_container.cknown)
                used = ECMD_TIME;
            await container_contents(game.current_container, false, false, true);
            // After looking, outokay might change
            outokay = Has_contents(game.current_container);
            if (!outokay) {
                emptymsg = `${Yname2(game.current_container)} is empty.`;
            }
            continue;
        } else {
            break;
        }
    }
    if (c === 'q')
        game.abort_looting = true;
    if (c === 'n' || c === 'q') {
        // containerdone
        if (used) {
            if (game.current_container) game.current_container.cknown = 1;
            update_inventory();
        }
        sellobj_state(SELL_NORMAL);
        objRef.obj = game.current_container;
        if (game.current_container)
            game.current_container = null;
        else
            game.abort_looting = true;
        return used;
    }
    loot_out = (c === 'o' || c === 'b' || c === 'r');
    loot_in = (c === 'i' || c === 'b' || c === 'r');
    loot_in_first = (c === 'r');
    stash_one = (c === 's');
    // out-only or out before in
    if (loot_out && !loot_in_first) {
        if (!Has_contents(game.current_container)) {
            await pline1(emptymsg);
            if (!game.current_container.cknown) used = ECMD_TIME;
            game.current_container.cknown = 1;
        } else {
            used |= await menu_loot(0, false);
        }
        // recalculate inokay
        inokay = (game.invent && (game.invent !== game.current_container
                                  || game.invent.nobj));
    }
    if ((loot_in || stash_one) && !inokay) {
        await You(`don't have anything${game.invent ? " else" : ""} to ${stash_one ? "stash" : "put in"}.`);
        loot_in = false;
        stash_one = false;
    }
    if (loot_in) {
        used |= await menu_loot(0, true);
    } else if (stash_one) {
        const otmp = await getobj("stash", stash_ok,
                                  GETOBJ_PROMPT | GETOBJ_ALLOWCNT);
        if (otmp) {
            if (await in_container(otmp)) {
                used = 1;
            } else {
                unsplitobj(otmp);
            }
        }
    }
    // container might have been destroyed by magic bag explosion
    if (!game.current_container)
        loot_out = false;
    // out after in (reversed)
    if (loot_out && loot_in_first) {
        if (!Has_contents(game.current_container)) {
            await pline1(emptymsg);
            if (!game.current_container.cknown) used = 1;
            game.current_container.cknown = 1;
        } else {
            used |= await menu_loot(0, false);
        }
    }
    // containerdone
    if (used) {
        if (game.current_container) game.current_container.cknown = 1;
        update_inventory();
    }
    sellobj_state(SELL_NORMAL);
    objRef.obj = game.current_container;
    if (game.current_container)
        game.current_container = null;
    else
        game.abort_looting = true;
    return used;
}
// ── doloot: loot a container ──
// C ref: pickup.c:2160
export async function doloot() {
    game.loot_reset_justpicked = true;
    const res = await doloot_core();
    game.loot_reset_justpicked = false;
    return res;
}
// ── doloot_core: main loot logic ──
// C ref: pickup.c:2172
async function doloot_core() {
    let c = -1;
    let timepassed = 0;
    const cc = { x: game.u.ux, y: game.u.uy };
    let underfoot = true;
    const dont_find_anything = "don't find anything";
    game.abort_looting = false;
    if (check_capacity(null)) {
        return ECMD_OK;
    }
    if (nohands(game.youmonst?.data)) {
        await You("have no hands!");
        return ECMD_OK;
    }
    if (Confusion()) {
        if (rn2(6) && reverse_loot())
            return ECMD_TIME;
        if (rn2(2)) {
            await pline("Being confused, you find nothing to loot.");
            return ECMD_TIME;
        }
    }
    // TODO: iflags.menu_requested → goto lootmon
    // lootcont:
    let num_conts = container_at(cc.x, cc.y, true);
    if (num_conts > 0) {
        let anyfound = false;
        if (!(await able_to_loot(cc.x, cc.y, true)))
            return ECMD_OK;
        // TODO: blind cockatrice check
        if (num_conts > 1) {
            // Multiple containers: show menu to pick which ones
            const win = create_nhwindow_menu();
            start_menu(win);
            for (let cobj = fobj_at(cc.x, cc.y); cobj; cobj = cobj.nexthere) {
                if (Is_container(cobj)) {
                    add_menu(win, cobj, 0, 0, ATR_NONE, NO_COLOR,
                             doname(cobj), MENU_ITEMFLAGS_NONE);
                }
            }
            end_menu(win, "Loot which containers?");
            const result = await select_menu(win, PICK_ANY);
            destroy_nhwindow_menu(win);
            if (result.count > 0) {
                for (let i = 0; i < result.items.length; i++) {
                    const cobjRef = { obj: result.items[i].identifier };
                    timepassed |= await do_loot_cont(cobjRef, i + 1, result.count);
                    if (game.abort_looting) {
                        return timepassed ? ECMD_TIME : ECMD_OK;
                    }
                }
            }
            if (result.count !== 0) c = 'y';
        } else {
            // Single container
            for (let cobj = fobj_at(cc.x, cc.y); cobj; ) {
                const nobj = cobj.nexthere;
                if (Is_container(cobj)) {
                    anyfound = true;
                    const cobjRef = { obj: cobj };
                    timepassed |= await do_loot_cont(cobjRef, 1, 1);
                    if (game.abort_looting)
                        return timepassed ? ECMD_TIME : ECMD_OK;
                }
                cobj = nobj;
            }
            if (anyfound) c = 'y';
        }
    }
    // TODO: IS_GRAVE check
    if (c !== 'y') {
        if (mon_beside(game.u.ux, game.u.uy) || game.iflags?.menu_requested) {
            let looted_mon = false;
            if (!(await get_adjacent_loot_loc('Loot in what direction?',
                                              'Invalid loot location',
                                              game.u.ux, game.u.uy, cc))) {
                return ECMD_OK;
            }
            underfoot = (cc.x === game.u.ux && cc.y === game.u.uy);
            if (underfoot && container_at(cc.x, cc.y, false)) {
                // C uses goto lootcont here; this path is unchanged because cc
                // is at hero position.
            } else {
                if (game.u.dz < 0) {
                    await You(`${dont_find_anything} to loot on the ${ceiling(cc.x, cc.y)}.`);
                    return ECMD_TIME;
                }
                const mtmp = m_at(cc.x, cc.y);
                if (mtmp) {
                    // TODO: loot_mon is not ported yet.
                    // Keep C-like control flow without inventing behavior.
                    looted_mon = false;
                }
                if (Confusion() || Stunned()) {
                    timepassed = 1;
                }
                if (!looted_mon) {
                    if (!underfoot && container_at(cc.x, cc.y, false)) {
                        if (mtmp) {
                            await You_cant(`loot anything there with ${mon_nam(mtmp)} in the way.`);
                            return timepassed ? ECMD_TIME : ECMD_OK;
                        } else {
                            await You('have to be at a container to loot it.');
                        }
                    } else {
                        await You(`${dont_find_anything} ${underfoot ? '' : 't'}here to loot.`);
                        return timepassed ? ECMD_TIME : ECMD_OK;
                    }
                }
            }
        } else {
            await You(`${dont_find_anything} ${underfoot ? 'here' : 'there'} to loot.`);
        }
    }
    return timepassed ? ECMD_TIME : ECMD_OK;
}
// Export current_container accessor for other modules
export function get_current_container() { return game.current_container; }
export function init_pickup_globals() {
    game.costly_cached = false;
    game.current_container = null;
    game.abort_looting = false;
    game.sellobj_first_flag = true;
}