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 | 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 20772x 20772x 20772x 513x 20772x 20772x 492x 18525x 20772x 20772x 430x 20772x 20772x 20772x 20772x 430x 18347x 430x 430x 430x 20772x 20772x 20772x 420x 420x 20772x 20772x 420x 420x 420x 20772x 20772x 20772x 420x 420x 20772x 20772x 419x 20772x 8x 8x 414x 20772x 20772x 411x 20772x 20772x 411x 18084x 411x 20772x 73x 73x 73x 73x 1193x 1193x 37x 37x 1193x 1193x 2x 1193x 1191x 1191x 1193x 73x 73x 73x 73x 1193x 1193x 1193x 1193x 73x 73x 73x 4670x 4670x 73x 73x 73x 140x 140x 140x 140x 14x 138x 126x 126x 1092x 71x 71x 71x 1092x 126x 140x 140x 140x 140x 8x 8x 8x 140x 140x 132x 132x 140x 140x 73x 73x 73x 73x 73x 1193x 1193x 1193x 1193x 1193x 1193x 1193x 8011x 8011x 8011x 349x 349x 349x 349x 8011x 844x 844x 844x 844x 844x 844x 844x 844x 844x 844x 844x 1193x 73x 73x 73x 845x 845x 845x 845x 1589x 1589x 1589x 1589x 13180x 13180x 11591x 11591x 11591x 5653x 5653x 744x 5653x 5653x 5653x 5653x 5653x 11591x 7527x 7527x 7527x 845x 845x 23182x 23182x 23182x 23182x 73x 73x 73x 1193x 1193x 73x 73x 73x 21x 21x 21x 21x 21x 21x 21x 21x 73x 73x 73x 18x 296x 296x 18x 18x 73x 73x 20x 20x 73x 73x 1160x 25046x 25046x 190x 190x 25046x 970x 1160x 73x 73x 844x 844x 844x 844x 19x 19x 19x 825x 844x 5702x 5702x 5702x 5702x 5702x 5702x 5702x 825x 844x 844x 844x 811x 844x 811x 844x 884x 811x 884x 844x 844x 844x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 286x 286x 286x 286x 286x 286x 286x 286x 286x 286x 286x 286x 286x 283x 283x 283x 11x 3x 3x 3x 3x 3x 3x 19x 19x 19x 3x 3x 3x 19x 3x 11x 11x 286x 3x 3x 283x 284x 248x 248x 29x 29x 29x 248x 283x 283x 283x 285x 3908x 446x 446x 446x 446x 3462x 3896x 986x 986x 3893x 810x 810x 3908x 286x 13x 13x 13x 270x 270x 270x 285x 12x 285x 23x 257x 235x 235x 279x 363x 363x 359x 359x 363x 363x 363x 29x 29x 29x 29x 330x 363x 1x 1x 329x 363x 21x 21x 21x 18x 17x 17x 17x 17x 17x 308x 363x 3176x 219x 219x 219x 3176x 308x 363x 219x 219x 363x 89x 89x 89x 286x 207x 207x 2x 2x 2x 207x 234x 286x 73x 73x 73x 73x 21x 21x 21x 320x 102x 102x 21x 21x 21x 21x 21x 21x 21x 21x 7x 7x 21x 21x 21x 21x 21x 21x 21x 21x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 1x 1x 20x 1x 1x 21x 21x 21x 21x 19x 19x 285x 285x 285x 4425x 4425x 4425x 4425x 4425x 46x 46x 46x 46x 46x 46x 46x 100x 4425x 4425x 4425x 285x 20x 19x 19x 19x 19x 19x 19x 19x 19x 21x 21x 16x 18x 16x 16x 174x 174x 21x 272x 272x 272x 272x 1438x 1438x 1438x 1438x 272x 267x 77x 77x 169x 201x 201x 201x 201x 814x 814x 201x 201x 63x 46x 201x 201x 77x 272x 73x 73x 73x 73x 73x 73x 73x 27x 27x 59x 59x 27x 27x 73x 73x 73x 73x 73x 73x 7x 7x 73x 73x 73x 73x 66x 66x 66x 66x 66x 66x 54x 54x 54x 66x 54x 54x 66x 45x 45x 45x 45x 45x 45x 45x 45x 45x 45x 45x 66x 73x 73x 73x 73x 73x 43x 43x 5x 5x 5x 5x 42x 38x 38x 43x 73x 39x 39x 39x 39x 39x 39x 39x 73x 73x 73x 1x 1x 1x 1x 73x 73x 73x 2390x 2390x 2361x 2390x 2390x 2390x 38689x 38688x 38689x 310x 310x 36988x 41x 41x 41x 41x 38689x 2390x 73x 73x 73x 73x 35x 35x 35x 35x 73x 73x 73x 73x 73x 265x 265x 265x 265x 265x 265x 208x 208x 265x 265x 265x 265x 265x 265x 265x 265x 265x 73x 73x 73x 4523x 4523x 4523x 73x 73x 73x 1397x 1397x 1397x 1397x 1397x 1397x 528x 1397x 1397x 73x 73x 73x 10x 10x 10x 10x 10x 10x 10x 10x 10x 5x 5x 10x 10x 10x 73x 73x 73x 5x 5x 5x 5x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 11x 187x 187x 176x 176x 11x 73x 73x 73x 27x 27x 1x 1x 27x 73x 73x 73x 73x 73x 73x 9458x 9458x 9458x 412x 412x 412x 412x 412x 412x 412x 412x 412x 26x 26x 410x 386x 386x 412x 412x 412x 412x 412x 412x 412x 412x 412x 412x 412x 9046x 9458x 73x 1x 1x 1x 1x 1x 1x 1x 1x 73x 1x 1x 1x 73x 73x 2x 2x 2x 2x 20x 20x 2x 2x 73x 24x 24x 22x 24x 73x 73x 73x 73x 73x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 20x 20x 2x 2x 2x 2x 2x 2x 2x 2x 20x 20x 18x 18x 20x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 1x 1x 1x 2x 2x 1x 1x 2x 2x 2x 2x 11x 11x 11x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 2x 73x 73x 73x 2x 2x 2x 2x 2x 2x 2x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 287x 287x 287x 287x 287x 287x 287x 287x 287x 287x 287x 287x 287x 287x 287x 287x 73x 73x 73x 73x 73x 73x 30x 30x 30x 30x 30x 30x 30x 30x 30x 30x 30x 30x 30x 30x 30x 15x 15x 30x 30x 30x 30x 450x 450x 450x 450x 450x 3920x 194x 194x 194x 3920x 450x 194x 450x 450x 450x 2745x 364x 2745x 2745x 2745x 194x 30x 30x 30x 30x 30x 30x 30x 30x 23x 23x 30x 2x 2x 30x 30x 73x 73x 73x 118x 118x 118x 118x 1995x 842x 1995x 1995x 1995x 2x 2x 118x 115x 115x 118x 118x 118x 11x 11x 11x 11x 11x 11x 11x 11x 11x 118x 118x 118x 2x 2x 118x 118x 118x 118x 118x 118x 73x 73x 73x 3x 3x 61x 61x 3x 3x 1x 11x 3x 3x 3x 11x 3x 2x 2x 2x 2x 6x 6x 6x 6x 6x 6x 90x 90x 90x 1230x 1230x 1230x 645x 20x 20x 20x 1230x 1230x 90x 6x 6x 6x 6x 6x 6x 6x 6x 6x 1x 1x 2x 2x 2x 2x 2x 2x 1x 6x 5x 1x 1x 5x 4x 4x 6x 2x 3x 3x 73x 73x 73x 3x 3x 3x 3x 36x 3x 3x | // invent.js — Inventory management (port of invent.c)
// C ref: invent.c — 5679 lines, ~60 functions
//
// This is a PARTIAL port focusing on the core functions needed by
// other translated files. Full inventory UI (menus, display) is not
// yet ported.
//
// Key exports:
// addinv() — add object to hero inventory
// addinv_nomerge() — add without merging
// freeinv() — remove object from hero inventory
// mergable() — can two objects stack?
// addinv_core1() — pre-add side effects (quest items, gold)
// addinv_core2() — post-add side effects (luck, archeologist)
// carry_obj_effects() — effects of carrying specific items
import { game } from './gstate.js';
import { setBotl } from './display.js';
import { tty_clear_nhwindow } from './display.js';
import { custompline, pline, You } from './pline.js';
import { cmdq_clear, cmdq_pop, more, nhgetch, ynFunction } from './input.js';
import { setnotworn } from './worn.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 { AMULET_CLASS, AMULET_OF_YENDOR, BAG_OF_TRICKS, CHEST, COIN_CLASS, CORPSE, CRYSKNIFE, EGG, FAKE_AMULET_OF_YENDOR, FIRST_OBJECT, FOOD_CLASS, GEM_CLASS, ICE_BOX, ILLOBJ_CLASS, Is_container, LARGE_BOX, MAXOCLASSES, SCR_SCARE_MONSTER, STATUE, TIN, TOOL_CLASS } from './objects.js';
import { weight, place_object, obj_extract_self, erosion_matters } from './mkobj.js';
import { xname, doname } from './objnam.js';
import { NO_COLOR } from './terminal.js';
import { exercise } from './attrib.js';
import { update_inventory } from './mon.js';
import { A_WIS, CMDQ_INT, CMDQ_KEY, CMDQ_USER_INPUT, CONTAINED_SYM, CQ_CANNED, GETOBJ_ALLOWCNT, GETOBJ_DOWNPLAY, GETOBJ_EXCLUDE, GETOBJ_PROMPT, GETOBJ_SUGGEST, Never_mind, OBJ_FREE, OBJ_INVENT, OBJ_FLOOR, Has_contents, BLINDED, PICK_NONE, PICK_ONE, PICK_ANY, LOST_EXPLODING, LOST_NONE, LOST_THROWN, TOPLINE_NEED_MORE } from './const.js';
import { check_unpaid, obfree } from './shk.js';
import { touch_artifact } from './artifact.js';
import { dropy } from './do.js';
import { ddoinv } from './cmd.js';
import { inv_cnt } from './hack.js';
import { Blind, Role_if, Upolyd } from './macros.js';
import { PM_CLERIC } from './monsters.js';
import { pushRngLogEntry } from './rng.js';
// ── Constants ──
// AMULET_OF_YENDOR imported from objects.js (was hardcoded as 322, correct: 213)
const CANDELABRUM_OF_INVOCATION = 262;
const BELL_OF_OPENING = 263;
const SPE_BOOK_OF_THE_DEAD = 409;
const LUCKSTONE = 470;
// ── mergable: can two objects be merged (stacked)? ──
// C ref: invent.c:779 mergable()
// No RNG. Checks type, bless/curse, enchantment, etc.
export function mergable(otmp, obj) {
if (!otmp || !obj) return false;
if (otmp === obj) return false;
if (otmp.otyp !== obj.otyp) return false;
// C ref: invent.c:791 — nomerge checked before oc_merge
if (otmp.nomerge || obj.nomerge) return false;
if (!game.objects[obj.otyp]?.oc_merge) return false;
// C ref: invent.c:797 — coins of the same kind ALWAYS merge
if (obj.oclass === COIN_CLASS) return true;
if (!!obj.cursed !== !!otmp.cursed || !!obj.blessed !== !!otmp.blessed)
return false;
// C ref: invent.c:802-805 — how_lost checks
if ((obj.how_lost || 0) === LOST_EXPLODING || (otmp.how_lost || 0) === LOST_EXPLODING)
return false;
if ((otmp.how_lost || 0) !== LOST_NONE && (obj.how_lost || 0) !== (otmp.how_lost || 0))
return false;
// C ref: invent.c:814 — globs always merge
if (obj.globby) return true;
// C ref: invent.c:818-821 — unpaid, spe, no_charge, obroken, otrapped, lamplit
// Use !! for boolean fields since JS may have false/0/undefined where C has 0
if (!!obj.unpaid !== !!otmp.unpaid || obj.spe !== otmp.spe
|| !!obj.no_charge !== !!otmp.no_charge || !!obj.obroken !== !!otmp.obroken
|| !!obj.otrapped !== !!otmp.otrapped || !!obj.lamplit !== !!otmp.lamplit)
return false;
// C ref: invent.c:823-825 — food: oeaten and orotten must match
if (obj.oclass === FOOD_CLASS
&& ((obj.oeaten || 0) !== (otmp.oeaten || 0) || !!obj.orotten !== !!otmp.orotten))
return false;
// C ref: invent.c:827-830 — dknown, erosion, greased
if (!!obj.dknown !== !!otmp.dknown
|| (obj.oeroded || 0) !== (otmp.oeroded || 0)
|| (obj.oeroded2 || 0) !== (otmp.oeroded2 || 0)
|| !!obj.greased !== !!otmp.greased)
return false;
// C ref: invent.c:832-834 — erosion-specific rknown/oerodeproof
if (erosion_matters(obj)
&& !!obj.oerodeproof !== !!otmp.oerodeproof)
return false;
// C ref: invent.c:836-838 — corpse/egg/tin corpsenm
if (obj.otyp === CORPSE || obj.otyp === EGG || obj.otyp === TIN) {
if (obj.corpsenm !== otmp.corpsenm) return false;
}
// C ref: invent.c:845-849 — hatching eggs don't merge
if (obj.otyp === EGG && ((obj.timed || 0) || (otmp.timed || 0)))
return false;
// Named items don't merge unless same name
if ((otmp.onamelth || 0) !== (obj.onamelth || 0)) return false;
if (otmp.oname && otmp.oname !== obj.oname) return false;
// C ref: invent.c:869 — locked containers
if (otmp.olocked !== obj.olocked) return false;
return true;
}
// ── addinv_core1: pre-add side effects ──
// C ref: invent.c:960
// Sets quest item flags, botl for gold. No RNG.
export function addinv_core1(obj) {
const g = game;
if (obj.oclass === COIN_CLASS) {
setBotl('addinv_core1');
}
if (!g.u?.uhave) g.u.uhave = {};
if (obj.otyp === AMULET_OF_YENDOR) {
g.u.uhave.amulet = 1;
} else if (obj.otyp === CANDELABRUM_OF_INVOCATION) {
g.u.uhave.menorah = 1;
} else if (obj.otyp === BELL_OF_OPENING) {
g.u.uhave.bell = 1;
} else if (obj.otyp === SPE_BOOK_OF_THE_DEAD) {
g.u.uhave.book = 1;
}
}
// ── addinv_core2: post-add side effects ──
// C ref: invent.c:1024
// Luck, archeologist scroll deciphering. No RNG.
export function addinv_core2(obj) {
// C ref: invent.c:1025 — luck and archeologist scroll deciphering
if (obj.otyp === LUCKSTONE) {
// confers_luck → set_moreluck (simplified)
// TODO: full set_moreluck implementation
}
// TODO: archeologist scroll deciphering
}
// ── carry_obj_effects: ongoing effects of carrying items ──
// C ref: invent.c:1135
export function carry_obj_effects(obj) {
// TODO: artifact effects, loadstone, etc.
}
// ── freeinv: remove object from hero inventory ──
// C ref: invent.c:1350
export function freeinv(obj) {
if (!obj) return;
const g = game;
// Unlink from inventory chain
if (g.invent === obj) {
g.invent = obj.nobj;
} else {
let prev = null;
for (let o = g.invent; o; prev = o, o = o.nobj) {
if (o === obj) {
if (prev) prev.nobj = obj.nobj;
break;
}
}
}
obj.nobj = null;
obj.where = OBJ_FREE;
// C: freeinv_core(obj) — update quest item flags, luck, botl
if (obj.oclass === COIN_CLASS) {
setBotl('freeinv_core');
return;
}
if (obj.otyp === AMULET_OF_YENDOR) {
if (g.u?.uhave) g.u.uhave.amulet = 0;
} else if (obj.otyp === CANDELABRUM_OF_INVOCATION) {
if (g.u?.uhave) g.u.uhave.menorah = 0;
} else if (obj.otyp === BELL_OF_OPENING) {
if (g.u?.uhave) g.u.uhave.bell = 0;
} else if (obj.otyp === SPE_BOOK_OF_THE_DEAD) {
if (g.u?.uhave) g.u.uhave.book = 0;
}
if (obj.otyp === LUCKSTONE) {
setBotl('freeinv_core');
}
}
// C ref: invent.c:nxtobj() helper — find next object of type in chain.
// byNexthere=true walks floor chains (nexthere), else inventory/object chain (nobj).
export function nxtobj(obj, otyp, byNexthere = false) {
let cur = byNexthere ? obj?.nexthere : obj?.nobj;
while (cur) {
if (cur.otyp === otyp) return cur;
cur = byNexthere ? cur.nexthere : cur.nobj;
}
return null;
}
// ── addinv_core0: the actual inventory insertion ──
// C ref: invent.c:1056
function addinv_core0(obj, other_obj, update_perm_invent) {
const g = game;
addinv_core1(obj);
// C ref: invent.c:1098-1113 — try to merge via merged() (which calls obfree for oid renaming)
// C uses pointer-to-pointer for merged(); JS uses wrapper objects to allow survivor to change.
let prev = null;
for (let otmp = g.invent; otmp; prev = otmp, otmp = otmp.nobj) {
const otmpRef = { obj: otmp };
const objRef = { obj };
if (merged(otmpRef, objRef)) {
obj = otmpRef.obj; // survivor (may have changed via obfree oid rename)
addinv_core2(obj);
return obj;
}
}
// No merge — add to inventory chain
// C ref: invent.c:1116 assigninvlet + insert at beginning + reorder
assigninvlet(obj);
obj.nobj = g.invent;
g.invent = obj;
obj.where = OBJ_INVENT;
// C ref: invent.c:1120-1121 — reorder_invent() sorts by invlet
// when flags.invlet_constant is true (default)
reorder_invent();
addinv_core2(obj);
return obj;
}
// C ref: invent.c reorder_invent() — bubble sort by inv_rank (invlet ^ 040)
// Sorts the invent linked list so items appear in invlet order.
// With invlet_constant=true (default), items are ordered A-Z then a-z.
function reorder_invent() {
const g = game;
let needMore;
do {
needMore = false;
let prev = null;
let otmp = g.invent;
while (otmp) {
const next = otmp.nobj;
if (next) {
const rankOtmp = inv_rank(otmp);
const rankNext = inv_rank(next);
if (rankNext < rankOtmp) {
needMore = true;
if (prev) prev.nobj = next;
else g.invent = next;
otmp.nobj = next.nobj;
next.nobj = otmp;
prev = next;
continue; // don't advance otmp
}
}
prev = otmp;
otmp = next;
}
} while (needMore);
}
function inv_rank(obj) {
const invlet = typeof obj.invlet === 'string' ? obj.invlet.charCodeAt(0) : (obj.invlet || 0);
return invlet ^ 0o40; // ^ 040 octal = ^ 32
}
// ── addinv: add object to hero inventory (with merge) ──
// C ref: invent.c:1148
export function addinv(obj) {
return addinv_core0(obj, null, true);
}
// ── addinv_nomerge: add without attempting merge ──
// C ref: invent.c:1155
export function addinv_nomerge(obj) {
// C ref: invent.c:addinv_nomerge() sets obj->nomerge=1 then calls addinv().
// This preserves all normal addinv side effects, including reorder_invent().
const saveNomerge = obj.nomerge;
obj.nomerge = 1;
const result = addinv(obj);
obj.nomerge = saveNomerge;
return result;
}
// Compatibility helper used by multiple modules: find first inventory
// object matching otyp.
export function carrying(otyp) {
for (let otmp = game.invent; otmp; otmp = otmp.nobj) {
if (otmp.otyp === otyp) return otmp;
}
return null;
}
// C ref: dokick.c/invent.c shared helper usage in UI text.
export function currency(amount) {
return amount === 1 ? 'zorkmid' : 'zorkmids';
}
// C ref: mkobj.c g_at() helper used by vault/shk paths.
export function g_at(x, y) {
for (const obj of (game.level?.objects || [])) {
if (!obj) continue;
if (obj.ox === x && obj.oy === y && obj.oclass === COIN_CLASS) {
return obj;
}
}
return null;
}
// C ref: invent.c:694 assigninvlet — uses lastinvnr to cycle through letters
const invlet_basic = 52; // 26 lowercase + 26 uppercase
function assigninvlet(otmp) {
if (!otmp) return;
const g = game;
if (otmp.oclass === COIN_CLASS) {
otmp.invlet = '$';
return;
}
const inuse = new Uint8Array(invlet_basic);
for (let obj = g.invent; obj; obj = obj.nobj) {
if (obj !== otmp) {
const i = obj.invlet?.charCodeAt?.(0) ?? 0;
if (i >= 97 && i <= 122) inuse[i - 97] = 1; // a-z → 0-25
else if (i >= 65 && i <= 90) inuse[i - 65 + 26] = 1; // A-Z → 26-51
if (obj.invlet === otmp.invlet) otmp.invlet = null;
}
}
// If object already has a valid invlet that's not in use, keep it
const existing = otmp.invlet?.charCodeAt?.(0) ?? 0;
if (existing && ((existing >= 97 && existing <= 122) || (existing >= 65 && existing <= 90)))
return;
// Find next available starting from lastinvnr+1, wrapping around
if (g.lastinvnr === undefined) g.lastinvnr = 51;
let i;
for (i = g.lastinvnr + 1; i !== g.lastinvnr; i++) {
if (i === invlet_basic) { i = -1; continue; }
if (!inuse[i]) break;
}
otmp.invlet = inuse[i] ? '#' : (i < 26 ? String.fromCharCode(97 + i) : String.fromCharCode(65 + i - 26));
g.lastinvnr = i;
}
// ── Callback return values for getobj ──
export const hands_obj = { _hands: true, otyp: -1 };
const HANDS_SYM = '-';
// ── getobj: select an inventory object ──
// C ref: invent.c:1752
// Prompts player to choose an inventory item for the given action.
// obj_ok callback classifies each object's suitability.
// Returns: object, null (cancel), or hands_obj (bare hands).
//
// RNG: none (input only). But this is async — reads from key queue.
export async function getobj(word, obj_ok, ctrlflags) {
const g = game;
const allowcnt = !!(ctrlflags & GETOBJ_ALLOWCNT);
let forceprompt = !!(ctrlflags & GETOBJ_PROMPT);
let allownone = false;
let cntgiven = false;
let cnt = 0;
let chosen = null;
let handsSuggested = false;
// C ref: invent.c getobj() need_more_cq block.
// If command-queue input is present, consume it directly before prompting.
while (true) {
const cmdq = cmdq_pop();
if (!cmdq) {
if (cntgiven) return null;
break;
}
if (cmdq.typ === CMDQ_USER_INPUT) {
break;
}
let picked = null;
if (cmdq.typ === CMDQ_KEY) {
const keyChar = String.fromCharCode(cmdq.key | 0);
if (keyChar === HANDS_SYM) {
const v = obj_ok ? await obj_ok(null) : GETOBJ_EXCLUDE;
if (v === GETOBJ_SUGGEST || v === GETOBJ_DOWNPLAY) {
picked = hands_obj;
}
} else {
for (let otmp = g.invent; otmp; otmp = otmp.nobj) {
if (otmp.invlet !== keyChar) continue;
const v = obj_ok ? await obj_ok(otmp) : GETOBJ_SUGGEST;
if (v === GETOBJ_SUGGEST || v === GETOBJ_DOWNPLAY) {
picked = otmp;
break;
}
}
}
} else if (cmdq.typ === CMDQ_INT) {
if (!cntgiven && allowcnt) {
cnt = cmdq.intval | 0;
cntgiven = true;
continue;
}
cmdq_clear(CQ_CANNED);
return null;
}
if (!picked) {
cmdq_clear(CQ_CANNED);
return null;
}
if (cntgiven && Number.isFinite(picked.quan) && picked.quan > cnt && cnt > 0) {
// C would split the stack here; JS getobj currently returns object refs.
// Keep C's queue-consume behavior without mutating stack shape here.
}
return picked;
}
// Check if hands/self is valid
if (obj_ok) {
const handsResult = await obj_ok(null);
if (handsResult === GETOBJ_SUGGEST || handsResult === GETOBJ_DOWNPLAY) {
allownone = true;
handsSuggested = (handsResult === GETOBJ_SUGGEST);
}
}
// Build list of valid inventory letters
let lets = '';
let suggested = 0;
for (let otmp = g.invent; otmp; otmp = otmp.nobj) {
if (!obj_ok) {
lets += otmp.invlet;
suggested++;
continue;
}
const result = await obj_ok(otmp);
if (result === GETOBJ_SUGGEST) {
lets += otmp.invlet;
suggested++;
} else if (result === GETOBJ_DOWNPLAY) {
forceprompt = true;
}
}
if (suggested === 0 && !forceprompt && !allownone) {
await You(`don't have anything to ${word}.`);
return null;
}
// Prompt for item
const compact = compact_invlet_spec(lets);
let qbuf;
if (!compact) {
qbuf = `What do you want to ${word}? [*]`;
} else if (allownone && handsSuggested) {
qbuf = `What do you want to ${word}? [- ${compact} or ?*]`;
} else {
qbuf = `What do you want to ${word}? [${compact} or ?*]`;
}
while (true) {
chosen = null;
const iletRaw = await ynFunction(qbuf, null, null);
const ilet = (typeof iletRaw === 'number')
? String.fromCharCode(iletRaw)
: iletRaw;
// Handle escape/quit
if (ilet === '\x1b' || ilet === ' ' || ilet === '\r' || ilet === '\n') {
if (g.flags?.verbose)
await pline(Never_mind);
return null;
}
// Handle bare hands
if (ilet === HANDS_SYM) {
return allownone ? hands_obj : null;
}
// C ref: invent.c:1963 — '?' shows filtered menu, '*' shows all
if (ilet === '?' || ilet === '*') {
const filtered = (ilet === '?') ? lets : null;
const picked = await getobj_pickinv(g, word, filtered, allownone, allowcnt);
if (picked === null) continue; // cancelled — reprompt
if (picked === hands_obj) return hands_obj;
if (picked) {
setBotl('getobj');
chosen = picked;
break;
}
continue;
}
// Find the matching inventory object
for (let otmp = g.invent; otmp; otmp = otmp.nobj) {
if (otmp.invlet === ilet) {
chosen = otmp;
break;
}
}
setBotl('getobj');
if (chosen && !(cnt < 0 || chosen.quan < cnt)) {
break;
}
if (chosen && (cnt < 0 || chosen.quan < cnt)) {
await You(`don't have that many! You have only ${chosen.quan}.`);
continue;
}
// No match found: tell the player and reprompt.
await pline(`You don't have that object.`);
}
if (obj_ok && chosen) {
const okv = await obj_ok(chosen);
if (okv === GETOBJ_EXCLUDE) {
await silly_thing(word, chosen);
return null;
}
}
return chosen;
}
// C ref: invent.c:1963-1999 — display_pickinv for getobj
// C ref: invent.c query_objlist with INVORDER_SORT — groups items by class
// with class name headers, matching C's inventory menu presentation.
const DEF_INV_ORDER = [12, 5, 2, 3, 7, 9, 10, 8, 4, 11, 6, 13, 14, 15, 16];
async function getobj_pickinv(g, word, allowedLetters, allownone, allowcnt) {
const candidateItems = [];
for (let otmp = g.invent; otmp; otmp = otmp.nobj) {
if (allowedLetters && !allowedLetters.includes(otmp.invlet)) continue;
candidateItems.push(otmp);
}
// C ref: invent.c display_pickinv() caches an auxiliary menu window
// (gc.cached_pickinv_win) and reuses it across getobj prompts.
const cachedWin = (Number.isInteger(g.cached_pickinv_win) && g.cached_pickinv_win >= 0)
? g.cached_pickinv_win
: null;
const win = create_nhwindow_menu(undefined, cachedWin);
if (g.cached_pickinv_win === -1) {
g.cached_pickinv_win = win.winId;
}
start_menu(win);
// C ref: invent.c display_pickinv() uses message_menu-style single-item
// flow when exactly one candidate is available and invmenu is not forced.
const useSingleItemPath = (!allownone
&& candidateItems.length === 1
&& !g.iflags?.force_invmenu
&& !g.iflags?.menu_requested);
if (useSingleItemPath) {
const otmp = candidateItems[0];
const invlet = otmp.invlet || '?';
const itemText = `${invlet} - ${doname(otmp)}.`;
await custompline(`${itemText}--More--`);
pushRngLogEntry('>more');
const ch = await nhgetch();
pushRngLogEntry('^toplin[more=0]');
pushRngLogEntry('<more');
if (game?.nhDisplay) game.nhDisplay.toplin = TOPLINE_NEED_MORE;
await tty_clear_nhwindow('message');
if (ch === 27) return null;
if (String.fromCharCode(ch || 0) === invlet) return otmp;
return null;
}
if (allownone) {
add_menu(win, HANDS_SYM, HANDS_SYM, 0, 0, 8 /* NO_COLOR */, `your hands for ${word}`, 0);
}
const inv_order = (g.flags?.inv_order?.length)
? g.flags.inv_order : DEF_INV_ORDER;
const sortpack = g.flags?.sortpack !== false; // default true
if (sortpack) {
// C ref: invent.c:3288-3299 — insert class headers before each group
for (const oclass of inv_order) {
if (!oclass) break;
let headerAdded = false;
for (let otmp = g.invent; otmp; otmp = otmp.nobj) {
if (allowedLetters && !allowedLetters.includes(otmp.invlet))
continue;
const oc = otmp.oclass ?? game.objects[otmp.otyp]?.oc_class;
if (oc !== oclass) continue;
if (!headerAdded) {
// C ref: add_menu_heading uses ATR_INVERSE for class headers.
// C ref: windows.c:1823 — suppress highlighting during gameover.
const headAttr = g.program_state?.gameover ? 0/*ATR_NONE*/ : 1/*ATR_INVERSE*/;
add_menu(win, null, 0, 0, headAttr, 8/*NO_COLOR*/,
let_to_name(oclass, false, false), 0);
headerAdded = true;
}
const desc = doname(otmp);
const invlet = otmp.invlet || '?';
add_menu(win, invlet, invlet, 0, 0, NO_COLOR, desc, 0);
}
}
} else {
// Unsorted: list items in inventory order
for (let otmp = g.invent; otmp; otmp = otmp.nobj) {
if (allowedLetters && !allowedLetters.includes(otmp.invlet))
continue;
const desc = doname(otmp);
const invlet = otmp.invlet || '?';
add_menu(win, invlet, invlet, 0, 0, NO_COLOR, desc, 0);
}
}
// C ref: query_objlist uses the prompt on the message line (via ynFunction),
// not inside the menu overlay. Pass null to avoid adding prompt rows to menu.
end_menu(win, null);
const picked = await select_menu(win, PICK_ONE);
// Keep cached pickinv window allocated for future prompts (C parity).
win.items = [];
win.prompt = null;
win.finalized = false;
if (!picked || picked.count <= 0 || !picked.items?.length)
return null; // cancelled
const invlet = picked.items[0].identifier;
if (invlet === HANDS_SYM) return hands_obj;
if (typeof invlet === 'string') {
for (let otmp = g.invent; otmp; otmp = otmp.nobj) {
if (otmp.invlet === invlet) return otmp;
}
}
return null;
}
function compact_invlet_spec(letters) {
const chars = [];
const seen = new Set();
for (const ch of (letters || '')) {
if (seen.has(ch)) continue;
seen.add(ch);
chars.push(ch);
}
if (!chars.length) return '';
if (chars.length <= 5) return chars.join('');
const out = [];
let i = 0;
while (i < chars.length) {
const start = chars[i];
let j = i;
while (j + 1 < chars.length
&& chars[j + 1].charCodeAt(0) === chars[j].charCodeAt(0) + 1) {
j++;
}
const runLen = j - i + 1;
if (runLen >= 3) out.push(`${start}-${chars[j]}`);
else if (runLen === 2) out.push(`${start}${chars[j]}`);
else out.push(start);
i = j + 1;
}
return out.join('');
}
// ── ddoinv: display inventory ──
// C ref: invent.c ddoinv() → display_pickinv()
// Full implementation with class-grouped menu is in cmd.js.
export { ddoinv };
// ── count_unpaid: count unpaid items ──
// C ref: invent.c:3350
export function count_unpaid(chain) {
let count = 0;
for (let obj = chain; obj; obj = obj.nobj) {
if (obj.unpaid) count++;
}
return count;
}
// ── count_buc: count items by bless/curse status ──
// C ref: invent.c:3378
export function count_buc(chain, type) {
let count = 0;
for (let obj = chain; obj; obj = obj.nobj) {
if (type === 1 && obj.blessed) count++; // BUC_BLESSED
else if (type === 2 && obj.cursed) count++; // BUC_CURSED
else if (type === 0 && !obj.blessed && !obj.cursed) count++; // BUC_UNCURSED
}
return count;
}
// ── is_worn: check if object is being worn/wielded ──
// C ref: invent.c:1300
export function is_worn(obj) {
return !!obj?.owornmask;
}
// ── prinv: print an inventory item with prefix ──
// ── makeknown: mark object type as discovered ──
// C ref: hack.h — #define makeknown(x) discover_object(x, TRUE, TRUE, TRUE)
export function makeknown(otyp) {
if (otyp == null || otyp < FIRST_OBJECT) return;
const od = game.objects[otyp];
if (!od) return;
const wasKnown = !!od.oc_name_known;
const wasEncountered = !!od.oc_encountered;
if (od.oc_disco_order == null) {
game.disco_counter = (game.disco_counter || 0) + 1;
od.oc_disco_order = game.disco_counter;
}
if (!wasKnown || !wasEncountered) {
od.oc_encountered = 1;
}
if (!wasKnown) {
od.oc_name_known = 1;
// C ref: discover_object(... credit_hero=TRUE)
exercise(A_WIS, true);
// C updates perm inventory during moveloop, except on gameover.
if (game.program_state?.in_moveloop && !game.program_state?.gameover) {
// C has gem_learned() side effects before update_inventory().
// Keep inventory refresh ordering faithful pending full gem pricing port.
if (od.oc_class === GEM_CLASS) {
// TODO: gem_learned(otyp)
}
update_inventory();
}
}
}
// stackobj is defined later (line ~565) — canonical version using merged()
// delobj is defined later (line ~568) — canonical version with obj_extract_self
// ── useup / useupall: consume an item ──
// C ref: invent.c:1321
export function useup(obj) {
if (!obj) return;
if (obj.quan > 1) {
obj.in_use = false;
obj.quan--;
obj.owt = weight(obj);
update_inventory();
} else {
useupall(obj);
}
}
export function useupall(obj) {
if (!obj) return;
// C ref: invent.c:1314 — setnotworn clears the worn pointer
// (uarm/uarmc/uarms/etc.) before removing the object.
setnotworn(obj);
freeinv(obj);
// C: obfree(obj, NULL)
}
// ── consume_obj_charge: decrement spe on charged item ──
// C ref: invent.c:1337
export function consume_obj_charge(obj, maybe_unpaid) {
if (maybe_unpaid) check_unpaid(obj);
obj.spe -= 1;
if (obj.known) update_inventory();
}
// ── stackobj: merge object with others at same location ──
// C ref: invent.c:4365
export function stackobj(obj) {
if (!obj) return;
if (obj.where !== OBJ_FLOOR) return;
const level = game.level;
const objects = level?.objects;
if (!Array.isArray(objects)) return;
for (const otmp of objects) {
if (!otmp || otmp === obj) continue;
if (otmp.where !== OBJ_FLOOR) continue;
if (otmp.ox !== obj.ox || otmp.oy !== obj.oy) continue;
const otmpRef = { obj: otmp };
const objRef = { obj };
if (merged(objRef, otmpRef)) {
// C: merged(&obj, &otmp) — obj (new) survives, otmp (old) extracted.
// merged() already called obj_extract_self(otmp) internally.
return;
}
}
}
// ── delobj: delete an object from the game ──
// C ref: invent.c:1428 (delobj calls delobj_core(obj, 0))
// delobj_core calls obj_resists(obj, 0, 0) — always consumes rn2(100)
export function delobj(obj) {
if (!obj) return;
if (obj_resists(obj, 0, 0)) {
obj.in_use = false;
return;
}
obj_extract_self(obj);
}
// ── dropx: drop an item from inventory to floor ──
// C ref: do.c — simplified
export function dropx(obj) {
if (!obj) return;
freeinv(obj);
obj.ox = game.u?.ux ?? 0;
obj.oy = game.u?.uy ?? 0;
const level = game.level;
if (level?.objects) level.objects.push(obj);
}
// C ref: invent.c:1196
export async function prinv(prefix, obj, quan) {
// C ref: invent.c:2875 prinv()
const totalOf = !!(quan && quan < (obj?.quan || 0));
const verbose = !!game.flags?.verbose;
const pfx = prefix || '';
const savedQuan = obj?.quan;
if (obj && Number.isInteger(quan) && quan > 0) {
obj.quan = quan;
}
const name = doname(obj);
if (obj && savedQuan !== undefined) {
obj.quan = savedQuan;
}
const tail = totalOf
? (verbose ? ` (${savedQuan} in total).` : '')
: '.';
await pline(`${pfx}${pfx ? ' ' : ''}${obj.invlet} - ${name}${tail}`);
}
// ── observe_object: mark object as seen ──
// C ref: display.c observe_object()
export function observe_object(obj) {
if (!obj) return;
obj.dknown = 1;
}
// ── not_fully_identified: check if object has unknown properties ──
// C ref: objnam.c:1785
export function not_fully_identified(otmp) {
if (!otmp) return false;
if (otmp.oclass === COIN_CLASS)
return false;
if (!otmp.known || !otmp.dknown || !otmp.bknown
|| !game.objects[otmp.otyp]?.oc_name_known)
return true;
// TODO: cknown/lknown for containers, artifact discovery
return !otmp.rknown && (otmp.oerodeproof || false);
}
// ── fully_identify_obj: make object fully identified ──
// C ref: invent.c:2637
export function fully_identify_obj(otmp) {
if (!otmp) return;
makeknown(otmp.otyp);
// C: discover_artifact — skip for now
observe_object(otmp);
otmp.known = 1;
otmp.bknown = 1;
otmp.rknown = 1;
// C: set_cknown_lknown — set if applicable
if (otmp.oclass === TOOL_CLASS || otmp.otyp === STATUE) {
otmp.cknown = 1;
}
// Boxes
if (otmp.otyp === LARGE_BOX || otmp.otyp === CHEST) {
otmp.lknown = 1;
otmp.cknown = 1;
}
}
// ── identify: callback to identify and print ──
// C ref: invent.c:2651
export async function identify(otmp) {
fully_identify_obj(otmp);
await prinv(null, otmp, 0);
return 1;
}
// ── count_unidentified: count items not fully identified ──
// C ref: invent.c:2697
export function count_unidentified(objchn) {
let unid_cnt = 0;
for (let obj = objchn; obj; obj = obj.nobj) {
if (not_fully_identified(obj))
++unid_cnt;
}
return unid_cnt;
}
// ── wearing_armor: check if hero wears any armor ──
// C ref: invent.c:2148
export function wearing_armor() {
const g = game;
return !!(g.u.uarm || g.u.uarmc || g.u.uarmf || g.u.uarmg
|| g.u.uarmh || g.u.uarms || g.u.uarmu);
}
// ── is_inuse: is object being used by hero ──
// C ref: invent.c:2166
export function is_inuse(obj) {
if (!obj) return false;
if (obj.where !== OBJ_INVENT) return false;
return is_worn(obj) || (obj.lamplit || false) || (obj.leashmon || 0) > 0;
}
// ── doprarm: show worn armor ──
// C ref: invent.c:4601
export async function doprarm() {
const g = game;
if (!wearing_armor()) {
await You('are not wearing any armor.');
} else {
const pieces = [g.u.uarm, g.u.uarmc, g.u.uarms, g.u.uarmh, g.u.uarmg, g.u.uarmf, g.u.uarmu]
.filter(Boolean);
for (const obj of pieces) {
const name = doname(obj);
await pline(`${obj.invlet || '?'} - ${name} (being worn)`);
}
}
return 0;
}
// ── dotypeinv: inventory by type ──
// C ref: invent.c:3827
// Simplified: just shows full inventory for now (complex menu system not ported)
export async function dotypeinv() {
const g = game;
if (!g.invent) {
await You('aren\'t carrying anything.');
return 0;
}
// Fall through to basic inventory display
return await ddoinv();
}
// trycall: canonical implementation in do.js
// C ref: hack.h macro carried(obj) — is object in hero inventory?
export function carried(obj) { return obj?.where === OBJ_INVENT; }
// ── any_obj_ok: trivial object filter ──
// C ref: invent.c:1710
export function any_obj_ok(obj) {
if (obj) return GETOBJ_SUGGEST;
return GETOBJ_EXCLUDE;
}
// ── o_on: find object by id in chain (recursive into containers) ──
// C ref: invent.c:1587
export function o_on(id, objchn) {
while (objchn) {
if (objchn.o_id === id) return objchn;
if (Has_contents(objchn)) {
const temp = o_on(id, objchn.cobj);
if (temp) return temp;
}
objchn = objchn.nobj;
}
return null;
}
// ── set_cknown_lknown: mark container/lock known ──
// C ref: invent.c:2624
export function set_cknown_lknown(obj) {
if (Is_container(obj) || obj.otyp === STATUE) {
obj.cknown = 1;
obj.lknown = 1;
} else if (obj.otyp === TIN) {
obj.cknown = 1;
}
}
// ── merge_choice: find first mergeable object in list ──
// C ref: invent.c:775
export function merge_choice(objlist, obj) {
if (!objlist) return null;
if (obj.otyp === SCR_SCARE_MONSTER) return null;
const save_nocharge = obj.no_charge;
// Simplified: skip shop keeper check for now
let cur = objlist;
while (cur) {
if (mergable(cur, obj)) break;
cur = cur.nobj;
}
obj.no_charge = save_nocharge;
return cur || null;
}
// ── merged: merge obj into otmp if mergeable ──
// C ref: invent.c:814
export function merged(potmp, pobj) {
const otmp = potmp.obj;
const obj = pobj.obj;
if (mergable(otmp, obj)) {
if (!obj.lamplit && !obj.globby) {
otmp.age = Math.trunc(
((otmp.age * otmp.quan) + (obj.age * obj.quan))
/ (otmp.quan + obj.quan)
);
}
if (!otmp.globby) otmp.quan += obj.quan;
// C ref: invent.c:879-883 — weight update
if (otmp.oclass === COIN_CLASS) {
otmp.owt = weight(otmp);
otmp.bknown = 0;
} else if (!otmp.globby) {
otmp.owt = weight(otmp);
}
obj_extract_self(obj);
if (obj.pickup_prev && otmp.where === OBJ_INVENT)
otmp.pickup_prev = 1;
// Identification by comparison
if (obj.known !== otmp.known) otmp.known = 1;
if (obj.rknown !== otmp.rknown) otmp.rknown = 1;
if (obj.bknown !== otmp.bknown) otmp.bknown = 1;
if (obj.bypass) otmp.bypass = 1;
obfree(obj, otmp); /* C ref: invent.c:944 — free obj, transfer bill to otmp, oid rename */
return 1;
}
return 0;
}
function extract_nobj(target) {
if (!target) return;
if (game.invent === target) {
game.invent = target.nobj;
target.nobj = null;
return;
}
let prev = null;
for (let obj = game.invent; obj; obj = obj.nobj) {
if (obj === target) {
if (prev) prev.nobj = obj.nobj;
obj.nobj = null;
return;
}
prev = obj;
}
}
function letter(ch) {
return typeof ch === 'string' && /^[A-Za-z]$/.test(ch);
}
// C ref: invent.c:4886 check_invent_gold()
function check_invent_gold() {
let goldstacks = 0;
let wrongslot = 0;
for (let otmp = game.invent; otmp; otmp = otmp.nobj) {
if (otmp.oclass !== COIN_CLASS) continue;
goldstacks++;
if (otmp.invlet !== '$') wrongslot++;
}
return (goldstacks > 1 || wrongslot > 0);
}
function adjust_ok(obj) {
if (!obj || obj.oclass === COIN_CLASS) return GETOBJ_EXCLUDE;
return GETOBJ_SUGGEST;
}
function adjust_gold_ok(obj) {
if (!obj) return GETOBJ_EXCLUDE;
return GETOBJ_SUGGEST;
}
function names_compatible_for_adjust(dst, src) {
const dstName = dst?.oname || null;
const srcName = src?.oname || null;
return !dstName || (srcName && dstName === srcName);
}
// C ref: invent.c:4981 doorganize() + 5068 doorganize_core()
export async function doorganize() {
const g = game;
if (!g.invent || (g.invent.oclass === COIN_CLASS && g.invent.invlet === '$' && !g.invent.nobj)) {
await You(`aren't carrying anything ${!g.invent ? 'to adjust' : 'adjustable'}.`);
return 0;
}
const adjust_filter = check_invent_gold() ? adjust_gold_ok : adjust_ok;
let obj = await getobj('adjust', adjust_filter, GETOBJ_PROMPT | GETOBJ_ALLOWCNT);
if (!obj) return 0;
// Detect whether getobj already split stack (C-compatible shape check).
let splitting = null;
for (let otmp = g.invent; otmp; otmp = otmp.nobj) {
if (otmp.nobj === obj && otmp.invlet === obj.invlet) {
splitting = otmp;
break;
}
}
const letsRaw = [];
if (obj.oclass === COIN_CLASS) letsRaw.push('$');
for (let c = 97; c <= 122; c++) letsRaw.push(String.fromCharCode(c));
for (let c = 65; c <= 90; c++) letsRaw.push(String.fromCharCode(c));
letsRaw.push('#');
for (let otmp = g.invent; otmp; otmp = otmp.nobj) {
if (otmp === obj) continue;
if (mergable(otmp, obj)) continue;
const idx = letsRaw.indexOf(otmp.invlet);
if (idx >= 0) letsRaw[idx] = ' ';
}
if (!g.flags?.invlet_constant) {
const n = inv_cnt(false);
if (n < 52) {
const cutoff = Math.max(0, n + (splitting ? 1 : 2));
for (let i = cutoff; i < letsRaw.length; i++) letsRaw[i] = ' ';
}
}
const lets = compact_invlet_spec(letsRaw.filter((ch) => ch !== ' ').join(''));
let qbuf = !splitting ? 'Adjust letter' : `Split ${obj.quan}`;
qbuf += ` to what [${lets}]${g.invent ? ' (? see used letters)' : ''}?`;
let letChar = '\0';
let everMind = false;
for (let trycnt = 1; ; trycnt++) {
letChar = (obj.oclass === COIN_CLASS)
? '$'
: await ynFunction(qbuf, null, '\0', null);
if (typeof letChar === 'number') letChar = String.fromCharCode(letChar);
if (letChar === '?' || letChar === '*') {
// C calls display_used_invlets(); we keep prompt ownership and reprompt.
continue;
}
if (letChar === '\x1b' || letChar === ' ' || letChar === '\r' || letChar === '\n'
|| (splitting && letChar === obj.invlet)) {
if (!everMind) await pline(Never_mind);
return 0;
}
if (letChar === '$' && obj.oclass !== COIN_CLASS) {
await pline("Only gold coins may be moved into the '$' slot.");
everMind = true;
await pline(Never_mind);
return 0;
}
if ((letter(letChar) && letChar !== '@') || (lets.includes(letChar) && letChar !== '-')) break;
if (trycnt === 5) {
await pline(Never_mind);
return 0;
}
await pline('Select an inventory slot letter.');
}
const collect = (letChar === obj.invlet);
let adjType = collect ? 'Collecting:' : (!splitting ? 'Moving:' : 'Splitting:');
extract_nobj(obj);
for (let otmp = g.invent; otmp; otmp = otmp.nobj) {
if (collect) {
const objName = obj?.oname || null;
const otmpName = otmp?.oname || null;
if ((!otmpName || (objName && objName === otmpName))) {
const otmpRef = { obj: otmp };
const objRef = { obj };
if (merged(otmpRef, objRef)) {
obj = otmpRef.obj;
extract_nobj(obj);
continue;
}
}
} else if (otmp.invlet === letChar) {
if (names_compatible_for_adjust(otmp, obj)) {
const otmpRef = { obj: otmp };
const objRef = { obj };
if (merged(otmpRef, objRef)) {
adjType = 'Merging:';
obj = otmpRef.obj;
extract_nobj(obj);
break;
}
}
if (!splitting) {
adjType = 'Swapping:';
otmp.invlet = obj.invlet;
}
break;
}
}
obj.invlet = letChar;
obj.nobj = g.invent;
obj.where = OBJ_INVENT;
g.invent = obj;
reorder_invent();
await prinv(adjType, obj, 0);
update_inventory();
return 0;
}
// ── silly_thing: message for incorrect action on object ──
// C ref: invent.c:2094
export async function silly_thing(word, otmp) {
if (word === 'call'
&& (otmp.otyp === AMULET_OF_YENDOR
|| (otmp.otyp === FAKE_AMULET_OF_YENDOR && !otmp.known))) {
await pline("The Amulet doesn't like being called names.");
} else {
await pline("That is a silly thing to %s.", word);
}
}
// ── let_to_name: convert inventory letter to class name ──
// C ref: invent.c:4800
// ── let_to_name: exact port of C invent.c:4800 ──
// C ref: names[] at invent.c:4789, oth_symbols at 4794
import { def_oc_syms } from './symbols.js';
import { obj_resists } from './zap.js';
const let_to_name_names = [
null, 'Illegal objects', 'Weapons', 'Armor', 'Rings', 'Amulets', 'Tools',
'Comestibles', 'Potions', 'Scrolls', 'Spellbooks', 'Wands', 'Coins',
'Gems/Stones', 'Boulders/Statues', 'Iron balls', 'Chains', 'Venoms'
];
export function let_to_name(let_char, unpaid, showsym) {
const ocsymfmt = " ('%s')";
const invbuf_sympadding = 8;
const let_code = typeof let_char === 'string' ? let_char.charCodeAt(0) : let_char;
const oclass = (let_code >= 1 && let_code < MAXOCLASSES) ? let_code : 0;
let class_name;
if (oclass) {
class_name = let_to_name_names[oclass];
} else {
// check oth_symbols (just CONTAINED_SYM)
if (let_code === CONTAINED_SYM.charCodeAt(0)) {
class_name = 'Bagged/Boxed items';
} else {
class_name = let_to_name_names[ILLOBJ_CLASS];
}
}
let result;
if (unpaid) {
result = 'Unpaid ' + class_name;
} else {
result = class_name;
}
if (oclass !== 0 && showsym) {
let mlen = invbuf_sympadding - class_name.length;
let pad = '';
while (--mlen > 0) {
pad += ' ';
}
result += pad;
const sym = def_oc_syms[oclass]?.sym || '?';
result += ` ('${sym}')`;
}
return result;
}
// ── sobj_at: find object of given type at location ──
// C ref: invent.c:1466
// sobj_at: imported from mkobj.js
// ── display_inventory: show inventory ──
// C ref: invent.c:3428 display_inventory() -> display_pickinv()
export async function display_inventory(lets, want_reply) {
const g = game;
const allowedClassSyms = (typeof lets === 'string' && lets.length > 0) ? lets : null;
// C ref: invent.c:3430-3447 — command queue fast path.
const cmdq = cmdq_pop();
if (cmdq) {
if (cmdq.typ === CMDQ_KEY) {
const keyChar = String.fromCharCode(cmdq.key | 0);
for (let otmp = g.invent; otmp; otmp = otmp.nobj) {
if (otmp.invlet !== keyChar) continue;
if (!allowedClassSyms) return otmp.invlet;
const sym = def_oc_syms[otmp.oclass]?.sym;
if (sym && allowedClassSyms.includes(sym)) {
return otmp.invlet;
}
}
}
cmdq_clear(CQ_CANNED);
return '\0';
}
if (!g.invent) {
await pline('Not carrying anything.');
return '\0';
}
const usePermInvWindow = (!lets && !want_reply
&& Number.isInteger(g.WIN_INVEN) && g.WIN_INVEN >= 0);
const existingWinId = usePermInvWindow
? g.WIN_INVEN
: ((Number.isInteger(g.cached_pickinv_win) && g.cached_pickinv_win >= 0)
? g.cached_pickinv_win
: null);
const win = create_nhwindow_menu(undefined, existingWinId);
if (!usePermInvWindow && g.cached_pickinv_win === -1) {
g.cached_pickinv_win = win.winId;
}
start_menu(win);
const inv_order = (g.flags?.inv_order?.length)
? g.flags.inv_order : DEF_INV_ORDER;
for (const oclass of inv_order) {
if (!oclass) break;
const classSym = def_oc_syms[oclass]?.sym;
if (allowedClassSyms && (!classSym || !allowedClassSyms.includes(classSym))) {
continue;
}
let classHasItems = false;
for (let obj = g.invent; obj; obj = obj.nobj) {
if ((obj.oclass ?? game.objects[obj.otyp]?.oc_class) === oclass) {
classHasItems = true;
break;
}
}
if (!classHasItems) continue;
// C ref: add_menu_heading uses inverse attribute unless gameover.
const headAttr = g.program_state?.gameover ? 0/* ATR_NONE */ : 1/* ATR_INVERSE */;
add_menu(win, null, 0, 0, headAttr, NO_COLOR, let_to_name(oclass), 0);
for (let obj = g.invent; obj; obj = obj.nobj) {
if ((obj.oclass ?? game.objects[obj.otyp]?.oc_class) !== oclass) continue;
const desc = doname(obj);
const invlet = obj.invlet || '?';
add_menu(win, invlet, invlet, 0, 0, NO_COLOR, desc, 0);
}
}
end_menu(win, null);
const selected = await select_menu(win, want_reply ? PICK_ONE : PICK_NONE);
// C keeps cached pickinv window for reuse; clear menu content but keep slot.
win.items = [];
win.prompt = null;
win.finalized = false;
if (!want_reply) {
return '\0';
}
if (!selected || selected.count === 0) {
return '\0';
}
if (selected.count < 0) {
return '\x1b';
}
return selected.items?.[0]?.identifier || '\0';
}
// ── hold_another_object: try to add obj to inventory, drop if can't ──
// C ref: invent.c:1208 (simplified)
export async function hold_another_object(obj, drop_fmt, drop_arg, hold_msg) {
// C ref: invent.c:1208-1292 hold_another_object() + merged() discovered message.
// Detect comparison discovery against a potential merge target before addinv().
let discoveredByComparing = false;
for (let inv = game.invent; inv; inv = inv.nobj) {
if (!mergable(inv, obj)) continue;
if (inv.known !== obj.known) discoveredByComparing = true;
if (inv.rknown !== obj.rknown && !!inv.oerodeproof) discoveredByComparing = true;
if (inv.bknown !== obj.bknown && !Role_if(PM_CLERIC)) discoveredByComparing = true;
if (inv.how_lost === LOST_THROWN || obj.how_lost === LOST_THROWN) {
discoveredByComparing = false;
}
break;
}
if (!Blind()) {
observe_object(obj);
}
// C ref: invent.c:1218-1244 — artifact pickup probe uses temporary floor
// placement so touch_artifact() runs with the same object state as C.
if (obj.oartifact) {
const crysknife = (obj.otyp === CRYSKNIFE);
const oerode = obj.oerodeproof;
const wasUpolyd = Upolyd();
place_object(obj, game.u?.ux ?? 0, game.u?.uy ?? 0);
if (!(await touch_artifact(obj, game.youmonst))) {
obj_extract_self(obj);
await dropy(obj);
return obj;
} else if (wasUpolyd && !Upolyd()) {
if (drop_fmt) {
await pline(drop_fmt, drop_arg);
}
obj_extract_self(obj);
await dropy(obj);
return obj;
}
obj_extract_self(obj);
if (crysknife) {
obj.otyp = CRYSKNIFE;
obj.oerodeproof = oerode;
}
}
const oquan = obj?.quan || 0;
obj = addinv(obj);
if (discoveredByComparing) {
await pline('You learn more about your items by comparing them.');
}
if (hold_msg || drop_fmt) {
await prinv(hold_msg, obj, oquan);
}
update_inventory();
return obj;
}
// ── identify_pack: identify inventory items ──
// C ref: invent.c:2711 (simplified)
export async function identify_pack(id_limit, learning_id) {
let unid_cnt = 0;
for (let obj = game.invent; obj; obj = obj.nobj) {
if (not_fully_identified(obj)) unid_cnt++;
}
if (!unid_cnt) {
await You('have already identified %s of your possessions.',
!learning_id ? 'all' : 'the rest');
} else if (!id_limit || id_limit >= unid_cnt) {
for (let obj = game.invent; obj; obj = obj.nobj) {
if (not_fully_identified(obj)) {
await identify(obj);
if (--unid_cnt < 1) break;
}
}
} else {
// C ref: invent.c:2660 menu_identify() partial-ID flow.
let first = true;
let tryct = 5;
while (id_limit > 0) {
const prompt = `What would you like to identify ${first ? 'first' : 'next'}?`;
const win = create_nhwindow_menu();
start_menu(win);
const invOrder = (game.flags?.inv_order?.length)
? game.flags.inv_order : DEF_INV_ORDER;
for (const oclass of invOrder) {
if (!oclass) break;
let headerAdded = false;
for (let obj = game.invent; obj; obj = obj.nobj) {
if (!not_fully_identified(obj)) continue;
const oc = obj.oclass ?? game.objects[obj.otyp]?.oc_class;
if (oc !== oclass) continue;
if (!headerAdded) {
add_menu_heading(win, let_to_name(oclass, false, false));
headerAdded = true;
}
add_menu(win, obj, obj.invlet || 0, 0, 0, NO_COLOR, doname(obj), 0);
}
}
end_menu(win, prompt);
// C ref: query_objlist/menu display path clears message row before
// painting this prompt window; do this locally for identify menus.
const display = game?.nhDisplay || null;
if (display && game.level && display.toplin === 0) {
display.toplin = TOPLINE_NEED_MORE;
await tty_clear_nhwindow('message');
}
const picked = await select_menu(win, PICK_ANY);
destroy_nhwindow_menu(win);
const count = picked?.count || 0;
if (count > 0 && Array.isArray(picked.items)) {
const n = Math.min(count, id_limit, picked.items.length);
for (let i = 0; i < n; i++) {
const obj = picked.items[i]?.identifier;
if (!obj) continue;
await identify(obj);
id_limit--;
if (id_limit <= 0) break;
}
first = false;
} else if (count < 0) {
break;
} else if (!--tryct) {
await pline("That's enough tries.");
break;
} else {
await pline('Choose an item; use ESC to decline.');
}
}
}
update_inventory();
}
// ── learn_unseen_invent: mark inventory as seen when regaining sight ──
// C ref: invent.c:2750 (simplified)
export function learn_unseen_invent() {
const u = game.u;
if (u.uprops?.[BLINDED]?.intrinsic) return;
let invupdated = false;
for (let otmp = game.invent; otmp; otmp = otmp.nobj) {
if (otmp.dknown) continue;
otmp.dknown = 1;
invupdated = true;
}
if (invupdated) update_inventory();
}
|