All files / js do.js

45.85% Statements 553/1206
61.07% Branches 102/167
40.29% Functions 27/67
45.85% Lines 553/1206

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 120773x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x   73x 73x   73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x   73x   73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x             73x 73x 73x 73x 73x 73x 73x 73x 73x     73x   73x 73x 73x 73x 73x 73x 73x 73x 3x 4x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x   73x 73x 73x         73x   146x 146x 146x 146x 73x 153x 153x 153x 153x 153x 26x 153x 73x   73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 16x   16x                                                                                                                                             16x 16x 73x 73x 73x 73x 73x 108x 108x 108x 108x 108x 108x   108x                                                           108x   108x                         108x 44x 44x 1x 1x     1x 1x 1x 1x 1x       1x 108x 64x 64x                                         108x 108x 73x 73x 73x                                         73x 73x 73x 9x 9x 9x 9x 73x 73x 73x                                                                                           73x 73x                                               73x 73x                                                                                                                                                                                                                                                                                                   73x 73x 73x 75x 1x 1x 1x 1x 75x                 75x                 75x         75x         74x 75x 73x 73x 35x 35x 35x 35x 35x 29x 31x 35x 35x 31x 4x       4x 4x 31x     31x 1x 1x 31x       31x 29x 29x       29x 1x 1x 1x 1x 1x 1x 1x 1x 1x 29x 29x 29x 28x 28x 28x 35x 73x 73x                 73x 73x                           73x 73x 73x 36x 36x 36x 36x 36x 36x 36x 36x 36x 73x 73x 73x 36x 36x 73x 73x 73x 37x 37x 37x 37x 37x 37x 37x             37x 37x 37x 37x 37x 37x 37x 37x 37x 37x 37x 37x 37x 37x 37x 37x 37x 37x 73x 73x                                                                   73x 73x 73x 80x   80x               80x 80x               80x 80x 73x 73x 73x 35x 35x 35x 35x 35x 35x 35x 35x 35x 73x 73x 73x                                 73x 73x                                   73x 73x 73x 73x 73x         73x 73x 73x 73x         73x 73x 73x 9x       9x 9x 9x 9x 3x 9x 6x 6x 9x 9x 9x         9x 73x 73x 73x                                                 73x 73x 73x 189x 19x 19x 9x 9x 9x     19x 189x 73x 73x 73x 10x 10x 10x 10x 10x 10x 10x 73x 73x 73x 134x 134x 134x 134x 134x 134x 73x 73x 73x 189x 189x 189x 189x 189x 189x 189x 73x 73x 73x             73x 73x 73x                                                                                     73x 73x 73x             73x 73x 73x                         73x 73x 339x 339x 339x 73x 73x 73x 73x 692x 692x 473x 473x 473x 473x 473x 473x 473x 473x 473x 473x 473x 134x 134x 134x 472x         473x 692x 558x 558x 692x 73x 73x 73x 73x 73x 73x           73x 73x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x   1x 73x 73x 73x 2x 1x 1x 1x 1x 1x 2x 73x 73x 73x 1x   1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 73x 73x 73x 4x 4x 4x 3x 3x 3x 3x 4x 4x 4x 4x 4x 4x 4x 4x 4x 73x 73x 73x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x  
// do.js — Port of do.c
// Contains drop commands, floor effects, sink ring effects, corpse revival,
// level transition helpers, wounded legs, and miscellaneous commands.
import { game } from './gstate.js';
import { rn2, rnd, rn1, rnl, c_d, pushRngLogEntry } from './rng.js';
import { There, You, You_cant, You_feel, You_hear, You_see, Your, custompline, pline, pline_The, verbalize } from './pline.js';
import { impossible } from './pline.js';
import { Blind, BlindedTimeout, Confusion, Deaf, Fire_resistance, Flying, Hallucination, Inhell, Is_waterlevel, Levitation, Maybe_Half_Phys, Passes_walls, Punished, Role_if, Sick, Slimed, Stoned, Strangled, Stunned, Underwater, Upolyd, Wounded_legs } from './macros.js';
import { Amonnam, DEADMONSTER, Monnam, a_monnam, healmon, impact_disturbs_zombies, m_in_air, mnexto, mon_nam, mondied, mongone, newcham, pm_to_cham, revive, set_ustuck, wake_nearto, y_monnam, zombie_form } from './mon.js';
import { levl, m_at, t_at } from './map_access.js';
import { ABASE, AMAX, ACURR } from './attrib.js';
import { canseemon, canspotmon, docrt, map_background, map_object, newsym, recalc_block_point, setBotl } from './display.js';
import {
    mons,
    PM_WRAITH, PM_NURSE, PM_GREEN_SLIME,
    PM_DEATH, PM_PESTILENCE, PM_FAMINE,
    PM_CROESUS, PM_ROGUE,
    S_ZOMBIE,
    AD_POLY,
} from './monsters.js';
import { A_DEX, Align2amask, ALTAR, AM_NONE, BLINDED, BOTH_SIDES, COLNO, CORR, DB_FLOOR, DB_UNDER, DOOR, DRAWBRIDGE_UP, ECMD_FAIL, ECMD_OK, ECMD_TIME, FACE, FOUNTAIN, FROMOUTSIDE, GETOBJ_ALLOWCNT, GETOBJ_PROMPT, HAND, HOLE, I_SPECIAL, INTRINSIC, IS_ALTAR, is_hole, is_pit, IS_SINK, IS_WATERWALL, isok, KILLED_BY, KILLED_BY_AN, LADDER, LAVAPOOL, LEFT_SIDE, LEG, LOST_DROPPED, LOW_PM, MOAT, NO_KILLER_PREFIX, NO_TRAP, NON_PM, OBJ_BURIED, OBJ_CONTAINED, OBJ_FLOOR, OBJ_FREE, OBJ_INVENT, OBJ_MINVENT, PIT, POOL, RIGHT_SIDE, ROOM, ROWNO, SCORR, SDOOR, SELL_DELIBERATE, SELL_NORMAL, SINK, SPIKED_PIT, STAIRS, STOMACH, THRONE, TIMEOUT, TRAPDOOR, u_at, UTOTYPE_ATSTAIRS, UTOTYPE_DEFERRED, UTOTYPE_FALLING, UTOTYPE_NONE, UTOTYPE_PORTAL, UTOTYPE_RMPORTAL, W_ACCESSORY, W_ARMOR, W_ART, W_ARTI, W_SADDLE, WATER, WOUNDED_LEGS } from './const.js';
import { AMULET_OF_YENDOR, BELL_OF_OPENING, BOULDER, CANDELABRUM_OF_INVOCATION, COIN_CLASS, CORPSE, CRYSKNIFE, EGG, ENORMOUS_MEATBALL, FOOD_CLASS, GLOB_OF_GREEN_SLIME, has_omonst, LEASH, LOADSTONE, LUCKSTONE, MEAT_RING, MEAT_STICK, MEATBALL, POT_OIL, POTION_CLASS, RIN_ADORNMENT, RIN_AGGRAVATE_MONSTER, RIN_COLD_RESISTANCE, RIN_CONFLICT, RIN_FIRE_RESISTANCE, RIN_FREE_ACTION, RIN_GAIN_CONSTITUTION, RIN_GAIN_STRENGTH, RIN_HUNGER, RIN_INCREASE_ACCURACY, RIN_INCREASE_DAMAGE, RIN_INVISIBILITY, RIN_LEVITATION, RIN_POISON_RESISTANCE, RIN_POLYMORPH, RIN_POLYMORPH_CONTROL, RIN_PROTECTION, RIN_PROTECTION_FROM_SHAPE_CHAN, RIN_REGENERATION, RIN_SEARCHING, RIN_SEE_INVISIBLE, RIN_SHOCK_RESISTANCE, RIN_SLOW_DIGESTION, RIN_STEALTH, RIN_SUSTAIN_ABILITY, RIN_TELEPORT_CONTROL, RIN_TELEPORTATION, RIN_WARNING, RING_CLASS, SPE_BOOK_OF_THE_DEAD, TIN, WORM_TOOTH } from './objects.js';
import { mpickobj } from './steal.js';
import { setuwep, setuqwep, setuswapwep, welded, weldmsg } from './wield.js';
import { Doname2, The, Tobjnam, an, corpse_xname, doname, makeplural, otense, the, vtense, xname, yname, yobjnam } from './objnam.js';
import { docall, hcolor, hliquid, rndmonnam } from './do_name.js';
import { bimanual, dist2, distu, plur, s_suffix, set_levltyp, sgn, Something, upstart, visctrl } from './hacklib.js';
import { cansee, couldsee } from './vision.js';
import { encumber_msg } from './pickup.js';
import { digests, dmgtype, is_reviver, is_rider, is_vampshifter, is_whirly, locomotion, mdistu, olfaction, passes_walls, sticks, throws_rocks, touch_petrifies } from './mondata.js';
import { delfloortrap, fill_pit, float_down, minstapetrify, reset_utrap, uescaped_shaft, uteetering_at_seen_pit, water_damage } from './trap.js';
import { getobj, delobj, stackobj, freeinv, useup } from './invent.js';
import { place_object, set_bknown, set_corpsenm, splitobj, weight } from './mkobj.js';
import { costly_alteration, is_unpaid, sellobj, sellobj_state, stolen_value } from './shk.js';
import { set_itimeout, incr_itimeout, start_timer, obj_has_timer } from './timeout.js';
import { make_blinded, gulp_blnd_check } from './mhitu.js';
import { Can_fall_thru, is_lava, is_pool, is_pool_or_lava } from './dbridge.js';
import { can_reach_floor, losehp, monster_nearby, next2u, reset_occupations } from './hack.js';
import { body_part, mbodypart } from './polyself.js';
import { bury_objs, rot_corpse } from './dig.js';
import { container_impact_dmg, ship_object } from './dokick.js';
import { breakobj, hitfloor } from './dothrow.js';
import { grow_up } from './monmove.js';
import { mcureblindness } from './muse.js';
import { hmon } from './uhitm.js';
import { engr_at, make_grave } from './engrave.js';
import { Soundeffect } from './sounds.js';
import { drop_ball } from './ball.js';
import { set_occupation } from './cmd.js';
import { enexto_core } from './makemon.js';
import { u_on_newpos } from './stairs.js';
import { rloc } from './teleport.js';
import { waterbody_name } from './pager.js';
import { u_safe_from_fatal_corpse, useupf } from './eat.js';
import { assign_level } from './dungeon.js';
// ── Non-RNG stubs for functions not yet ported ──
// body_part imported from polyself.js
// mbodypart imported from polyself.js
// canseemon imported from display.js
// canspotmon imported from display.js
// is_pool imported from dbridge.js
// is_lava imported from dbridge.js
// is_pit imported from const.js
// is_hole imported from const.js
// NOTE: differs from canonical hacklib.js — local takes (x, y) and uses game.u directly;
// canonical takes (player, x, y). Changing signature would require updating all call sites.
// mdistu imported from display.js
// yobjnam imported from objnam.js
// doname_with_price: canonical in objnam.js
function an_str(s) { return an(s); }
function fruitname(_full) { return 'fruit'; } // simplified
// Something imported from hacklib.js
function obj_pmname(obj) { return obj?.corpsenm != null ? (mons[obj.corpsenm]?.mname || 'monster') : 'monster'; }
// get_obj_location: import from mkobj.js if needed
function get_container_location(_container, _ploctype, _p) { return null; } // TODO
// Non-RNG stubs for unported functions
// getobj imported from invent.js
// freeinv imported from invent.js
// stackobj imported from invent.js
// encumber_msg imported from pickup.js
// sellobj imported from shk.js
// ship_object imported from dokick.js
// impact_disturbs_zombies: imported from mon.js
// useupf: imported from eat.js
// useup imported from invent.js
// delobj imported from invent.js
// bury_objs imported from dig.js
function add_to_buried(_obj) { /* TODO */ }
// wake_nearto imported from mon.js
function set_uinwater(val) { game.u.uinwater = val; }
// docrt imported from display.js
// delfloortrap imported from trap.js
// mondied imported from bottleneck.js
// recalc_block_point imported from display.js
function lava_damage(_obj, _x, _y) { return false; } // TODO
// water_damage imported from hack.js
// weldmsg imported from wield.js
function finesse_ahriman(_obj) { return false; } // TODO
// stolen_value imported from shk.js
// costly_alteration imported from shk.js
// flooreffects: ported below (line ~300)
// weight imported from mkobj.js
// u_safe_from_fatal_corpse: imported from eat.js
function paranoid_ynq(_flag, _str, _flag2) { return 'y'; } // TODO
// splitobj imported from mkobj.js
import { add_valid_menu_class } from './pickup.js';
import { obj_resists } from './zap.js';
function menu_drop_stub(_retry) { return ECMD_OK; } // TODO
// obj_resists imported from mon.js
// breakobj imported from dothrow.js
// is_reviver: imported from mondata.js
// is_rider imported from mondata.js
// C ref: mondata.h — is_displacer: monster displaces others
function is_displacer(ptr) { return !!((ptr?.mflags3 ?? 0) & 0x0400); } // M3_DISPLACES
// touch_petrifies imported from mondata.js
// C ref: obj.h:321 polyfood — corpse/egg/tin that causes polymorph when eaten
function polyfood(obj) {
    if (!obj || (obj.otyp !== CORPSE && obj.otyp !== EGG && obj.otyp !== TIN))
        return false;
    if (obj.corpsenm < LOW_PM) return false;
    return pm_to_cham(obj.corpsenm) !== NON_PM || dmgtype(mons[obj.corpsenm], AD_POLY);
}
// minstapetrify imported from trap.js
// grow_up imported from monmove.js
// mcureblindness imported from muse.js
// revive: imported from mon.js
// start_timer imported from timeout.js
// obj_has_timer imported from timeout.js
// rot_corpse imported from dig.js
// zombie_form: imported from mon.js
// set_corpsenm imported from mkobj.js
function has_omid(_obj) { return false; }
function free_omid(_obj) { /* TODO */ }
// has_omonst imported from objects.js
function free_omonst(_obj) { /* TODO */ }
// dist2 imported from hacklib.js
// gulp_blnd_check imported from mhitu.js
// make_blinded imported from mhitu.js
// Slimed imported from macros.js
// Strangled imported from macros.js
// Sick imported from macros.js
// Wounded_legs imported from macros.js
// Punished imported from macros.js
function EWounded_legs_val() { return game.u?.uprops?.[WOUNDED_LEGS]?.extrinsic || 0; }
function HWounded_legs_val() { return game.u?.uprops?.[WOUNDED_LEGS]?.intrinsic || 0; }
// BlindedTimeout imported from macros.js
// digests imported from mondata.js
// sticks imported from mondata.js
// set_ustuck imported from mon.js
// passes_walls imported from mondata.js
// throws_rocks imported from mondata.js
// is_whirly imported from mondata.js
// is_vampshifter imported from mondata.js
// Maybe_Half_Phys imported from macros.js
// losehp imported from hack.js
// hmon imported from uhitm.js
// Passes_walls imported from macros.js
// obj_nexto_xy: not called in this file
function pudding_merge_message(_obj1, _obj2) { /* TODO */ }
// obj_meld: not called in this file
// Can_fall_thru: imported from dbridge.js
// Inhell: imported from macros.js (handles both Inhell() and Inhell(uz))
function NH_AMBER() { return 'amber'; }
function NH_BLACK() { return 'black'; }
function NH_SILVER() { return 'silver'; }
function NH_WHITE() { return 'white'; }
// assign_level: imported from dungeon.js
function the_your() { return ['the', 'your']; }
function norepFormat(fmt, ...args) {
    let i = 0;
    return String(fmt).replace(/%[sd]/g, () => (i < args.length ? String(args[i++]) : ''));
}
export async function Norep(msg, ...args) {
    const formatted = args.length ? norepFormat(msg, ...args) : String(msg);
    // C ref: Norep() is a pline flag, not a global "ever seen" cache.
    // Suppress only immediate repeats of the current/last topline message.
    if (formatted === game._pending_message || formatted === game._last_toplines)
        return;
    await custompline(formatted);
}
// locomotion imported from mondata.js
function SET_FOUNTAIN_LOOTED(x, y) { const loc = levl(x, y); if (loc) loc.looted = 1; }
// set_levltyp imported from hacklib.js
// make_grave imported from engrave.js
// Align2amask imported from const.js
// olfaction imported from mondata.js
// visctrl imported from hacklib.js
function cmd_from_func(_fn) { return 0; } // TODO
function do_reqmenu() { return 0; } // TODO
// ── Exported functions ──
// ── boulder_hits_pool ──
// C ref: do.c:49 — boulder_hits_pool()
export async function boulder_hits_pool(otmp, rx, ry, pushing) {
    if (!otmp || otmp.otyp !== BOULDER) {
        impossible('Not a boulder?');
    } else if (is_pool_or_lava(rx, ry)) {
        const lava = is_lava(rx, ry);
        let fills_up;
        const what = waterbody_name(rx, ry);
        const ltyp = levl(rx, ry).typ;
        const chance = rn2(10);
        fills_up = Is_waterlevel(game.u?.uz) ? false
                   : IS_WATERWALL(ltyp) ? (chance < 5)
                     : lava ? (chance === 0) : (chance !== 0);
        if (fills_up) {
            const ttmp = t_at(rx, ry);
            if (ltyp === DRAWBRIDGE_UP) {
                levl(rx, ry).drawbridgemask &= ~DB_UNDER;
                levl(rx, ry).drawbridgemask |= DB_FLOOR;
            } else {
                levl(rx, ry).typ = ROOM;
                levl(rx, ry).flags = 0;
                recalc_block_point(rx, ry);
            }
            const mtmp = m_at(rx, ry);
            if (mtmp && !DEADMONSTER(mtmp) && !m_in_air(mtmp))
                await mondied(mtmp);
            if (ttmp)
                delfloortrap(ttmp);
            await bury_objs(rx, ry);
            newsym(rx, ry);
            if (pushing) {
                let whobuf = 'you';
                if (game.u?.usteed)
                    whobuf = y_monnam(game.u.usteed);
                await pline('%s %s %s into the %s.', upstart(whobuf),
                    vtense(whobuf, 'push'), the(xname(otmp)), what);
                if (game.flags?.verbose && !Blind())
                    await pline('Now you can cross it!');
            }
        }
        if (!fills_up || !pushing) {
            if (!game.u?.uinwater) {
                if (pushing ? !Blind() : cansee(rx, ry)) {
                    await There('There is a large splash as %s %s the %s.',
                        the(xname(otmp)), fills_up ? 'fills' : 'falls into', what);
                } else if (!Deaf()) {
                    if (lava) {
                        Soundeffect('se_sizzling', 100);
                    } else {
                        Soundeffect('se_splash', 100);
                    }
                    await You_hear('a%s splash.', lava ? ' sizzling' : '');
                }
                wake_nearto(rx, ry, 40);
            }
            if (fills_up && game.u?.uinwater && distu(rx, ry) === 0) {
                set_uinwater(0);
                await docrt();
                await You('find yourself on dry land again!');
            } else if (lava && next2u(rx, ry)) {
                const dmg = c_d(Fire_resistance() ? 1 : 3, 6);
                await You('are hit by molten %s%s',
                    hliquid('lava'), Fire_resistance() ? '.' : '!');
                await losehp(Maybe_Half_Phys(dmg), 'molten lava', KILLED_BY);
            } else if (!fills_up && game.flags?.verbose
                       && (pushing ? !Blind() : cansee(rx, ry))) {
                await pline('It sinks without a trace!');
            }
        }
        if (pushing)
            useupf(otmp, otmp.quan);
        else
            delobj(otmp);
        return true;
    }
    return false;
}
// ── flooreffects ──
// C ref: do.c:162 — flooreffects()
// Simplified: handles boulder-in-pool and basic pool/lava cases.
// Many sub-cases stubbed for now.
export async function flooreffects(obj, x, y, verb) {
    if (obj.where !== OBJ_FREE)
        impossible('flooreffects: obj not free');
    obj.nobj = null;
    obj.nexthere = null;
    let res = false;
    if (obj.otyp === BOULDER && await boulder_hits_pool(obj, x, y, false)) {
        res = true;
    } else if (obj.otyp === BOULDER) {
        const t = t_at(x, y);
        if (t && (is_pit(t.ttyp) || is_hole(t.ttyp))) {
            // Boulder falls into pit/hole — simplified
            if (verb) {
                if (Blind() && u_at(x, y)) {
                    Soundeffect('se_crashing_boulder', 100);
                    await You_hear('a CRASH! beneath you.');
                } else if (!Blind() && cansee(x, y)) {
                    await pline_The('boulder %s%s.',
                        (t.ttyp === TRAPDOOR && !t.tseen) ? 'triggers and ' : '',
                        (t.ttyp === TRAPDOOR) ? 'plugs a trap door'
                        : (t.ttyp === HOLE) ? 'plugs a hole'
                          : 'fills a pit');
                } else {
                    Soundeffect('se_boulder_drop', 100);
                    await You_hear('a boulder %s.', verb);
                }
            }
            const t2 = t_at(x, y);
            if (t2) {
                delfloortrap(t2);
                if (game.u?.utrap && u_at(x, y))
                    await reset_utrap(false);
            }
            useupf(obj, 1);
            await bury_objs(x, y);
            newsym(x, y);
            res = true;
        }
    } else if (is_lava(x, y)) {
        res = lava_damage(obj, x, y);
    } else if (is_pool(x, y)) {
        if ((Blind() || (Levitation() || Flying())) && !Deaf() && u_at(x, y)) {
            if (!Underwater()) {
                if (weight(obj) > 200) { // WT_SPLASH_THRESHOLD
                    await pline('Splash!');
                } else if (Levitation() || Flying()) {
                    await pline('Plop!');
                }
            }
            map_background(x, y, 0);
            newsym(x, y);
        }
        res = await water_damage(obj, null, false) === 3; // ER_DESTROYED
    } else if (u_at(x, y)) {
        const t = t_at(x, y);
        if (t && (uteetering_at_seen_pit(t) || uescaped_shaft(t))) {
            if (is_pit(t.ttyp)) {
                if (Blind() && !Deaf()) {
                    Soundeffect('se_item_tumble_downwards', 50);
                    await You_hear('%s tumble downwards.', the(xname(obj)));
                } else {
                    await pline('%s into %s pit.', Tobjnam(obj, 'tumble'),
                        t.madeby_u ? 'your' : 'the');
                }
            } else if (await ship_object(obj, x, y, false)) {
                // ship_object() prints its own message.
                res = true;
            }
        }
    } else if (obj.oclass === POTION_CLASS
               && (game.level?.flags?.temperature || 0) > 0
               && (levl(x, y)?.typ === ROOM || levl(x, y)?.typ === CORR)) {
        // C ref: do.c flooreffects() hot-ground potion branch.
        if (cansee(x, y)) {
            await pline('%s up as %s the hot ground.', Tobjnam(obj, 'heat'),
                (obj.quan || 1) > 1 ? 'they hit' : 'it hits');
        }
        let survival_chance = obj.blessed ? 70 : 50;
        if (obj.invlet)
            survival_chance += (((game.u?.uluck || 0) + (game.u?.moreluck || 0)) * 2);
        if (obj.otyp === POT_OIL)
            survival_chance = 100;
        if (!obj_resists(obj, survival_chance, 100)) {
            if (cansee(x, y)) {
                await pline('%s from the heat!', (obj.quan || 1) > 1 ? 'They shatter' : 'It shatters');
            } else {
                await You_hear('a shattering noise.');
            }
            await breakobj(obj, x, y, false, false);
            res = true;
        }
    }
    return res;
}
// ── doaltarobj ──
// C ref: do.c:363 — doaltarobj()
export async function doaltarobj(obj) {
    if (Blind())
        return;
    if (obj.oclass !== COIN_CLASS) {
        // conduct tracking skipped
    } else {
        obj.blessed = 0;
        obj.cursed = 0;
    }
    if (obj.blessed || obj.cursed) {
        await There('There is %s flash as %s %s the altar.',
            an(hcolor(obj.blessed ? 'amber' : 'black')), doname(obj),
            otense(obj, 'hit'));
        if (!Hallucination())
            obj.bknown = 1;
    } else {
        await pline('%s %s on the altar.', Doname2(obj), otense(obj, 'land'));
        if (obj.oclass !== COIN_CLASS)
            obj.bknown = 1;
    }
}
// ── trycall ──
// C ref: do.c:395 — trycall()
export async function trycall(obj) {
    const od = game.objects[obj.otyp];
    if (od && !od.oc_name_known && !od.oc_uname)
        await docall(obj);
}
// ── polymorph_sink ──
// C ref: do.c:403 — polymorph_sink()
export async function polymorph_sink() {
    const g = game;
    const u = g.u;
    let sym = 'S_sink';
    if (levl(u.ux, u.uy).typ !== SINK)
        return;
    const sinklooted = levl(u.ux, u.uy).looted !== 0;
    levl(u.ux, u.uy).flags = 0;
    const roll = rn2(4);
    switch (roll) {
    default:
    case 0:
        sym = 'fountain';
        set_levltyp(u.ux, u.uy, FOUNTAIN);
        levl(u.ux, u.uy).blessedftn = 0;
        if (sinklooted)
            SET_FOUNTAIN_LOOTED(u.ux, u.uy);
        break;
    case 1:
        sym = 'throne';
        set_levltyp(u.ux, u.uy, THRONE);
        if (sinklooted)
            levl(u.ux, u.uy).looted = 1; // T_LOOTED
        break;
    case 2: {
        sym = 'altar';
        set_levltyp(u.ux, u.uy, ALTAR);
        const algn = rn2(3) - 1;
        levl(u.ux, u.uy).altarmask = ((Inhell() && rn2(3)) ? AM_NONE
                                      : Align2amask(algn));
        break;
    }
    case 3:
        sym = 'room';
        set_levltyp(u.ux, u.uy, ROOM);
        make_grave(u.ux, u.uy, null);
        if (levl(u.ux, u.uy).typ === 31) // GRAVE
            sym = 'grave';
        break;
    }
    if (levl(u.ux, u.uy).typ !== ROOM)
        await pline_The('sink transforms into %s!', an(sym));
    else
        await pline_The('sink vanishes.');
    newsym(u.ux, u.uy);
}
// ── teleport_sink ──
// C ref: do.c:459 — teleport_sink()
function teleport_sink() {
    const g = game;
    const u = g.u;
    let trycnt = 0;
    do {
        const cx = 1 + rnd((COLNO - 1) - 2);
        const cy = 1 + rn2(ROWNO - 2);
        const loc = levl(cx, cy);
        if (loc && loc.typ === ROOM
            && !t_at(cx, cy) && !engr_at(cx, cy)
            && (!cansee(cx, cy) || distu(cx, cy) > 9)) {
            const alreadylooted = levl(u.ux, u.uy).looted;
            set_levltyp(u.ux, u.uy, ROOM);
            levl(u.ux, u.uy).looted = 0;
            newsym(u.ux, u.uy);
            set_levltyp(cx, cy, SINK);
            levl(cx, cy).looted = alreadylooted ? 1 : 0;
            newsym(cx, cy);
            return true;
        }
    } while (++trycnt < 200);
    return false;
}
// ── dosinkring ──
// C ref: do.c:497 — dosinkring()
async function dosinkring(obj) {
    let ideed = true;
    let nosink = false;
    await You('drop %s down the drain.', doname(obj));
    obj.in_use = true;
    switch (obj.otyp) {
    case RIN_SEARCHING:
        await You('thought %s got lost in the sink, but there it is!', yname(obj));
        obj.in_use = false;
        await dropx(obj);
        await trycall(obj);
        return;
    case RIN_SLOW_DIGESTION:
        await pline_The('ring is regurgitated!');
        obj.in_use = false;
        await dropx(obj);
        await trycall(obj);
        return;
    case RIN_LEVITATION:
        await pline_The('sink quivers upward for a moment.');
        break;
    case RIN_POISON_RESISTANCE:
        await You('smell rotten %s.', makeplural(fruitname(false)));
        break;
    case RIN_AGGRAVATE_MONSTER:
        await pline('Several %s buzz angrily around the sink.',
            Hallucination() ? makeplural(rndmonnam(null)) : 'flies');
        break;
    case RIN_SHOCK_RESISTANCE:
        await pline('Static electricity surrounds the sink.');
        break;
    case RIN_CONFLICT:
        Soundeffect('se_drain_noises', 50);
        await You_hear('loud noises coming from the drain.');
        break;
    case RIN_SUSTAIN_ABILITY:
        await pline_The('%s flow seems fixed.', hliquid('water'));
        break;
    case RIN_GAIN_STRENGTH:
        await pline_The('%s flow seems %ser now.',
            hliquid('water'), (obj.spe < 0) ? 'weak' : 'strong');
        break;
    case RIN_GAIN_CONSTITUTION:
        await pline_The('%s flow seems %ser now.',
            hliquid('water'), (obj.spe < 0) ? 'less' : 'great');
        break;
    case RIN_INCREASE_ACCURACY:
        await pline_The('%s flow %s the drain.',
            hliquid('water'), (obj.spe < 0) ? 'misses' : 'hits');
        break;
    case RIN_INCREASE_DAMAGE:
        await pline("The water's force seems %ser now.",
            (obj.spe < 0) ? 'small' : 'great');
        break;
    case RIN_HUNGER:
        ideed = false;
        // Simplified: objects vanish from sink location
        break;
    case MEAT_RING:
        await pline('Several flies buzz around the sink.');
        break;
    case RIN_TELEPORTATION:
        nosink = teleport_sink();
        await pline_The('sink %svanishes.', nosink ? '' : 'momentarily ');
        ideed = false;
        break;
    case RIN_POLYMORPH:
        await polymorph_sink();
        nosink = true;
        ideed = (levl(game.u.ux, game.u.uy).typ !== ROOM);
        break;
    default:
        ideed = false;
        break;
    }
    if (!Blind() && !ideed) {
        ideed = true;
        switch (obj.otyp) {
        case RIN_ADORNMENT:
            await pline_The('faucets flash brightly for a moment.');
            break;
        case RIN_REGENERATION:
            await pline_The('sink looks as good as new.');
            break;
        case RIN_INVISIBILITY:
            await You("don't see anything happen to the sink.");
            break;
        case RIN_FREE_ACTION:
            await You_see('the ring slide right down the drain!');
            break;
        case RIN_SEE_INVISIBLE:
            await You_see('some %s in the sink.',
                Hallucination() ? 'oxygen molecules' : 'air');
            break;
        case RIN_STEALTH:
            await pline_The('sink seems to blend into the floor for a moment.');
            break;
        case RIN_FIRE_RESISTANCE:
            await pline_The('hot %s faucet flashes brightly for a moment.',
                hliquid('water'));
            break;
        case RIN_COLD_RESISTANCE:
            await pline_The('cold %s faucet flashes brightly for a moment.',
                hliquid('water'));
            break;
        case RIN_PROTECTION_FROM_SHAPE_CHAN:
            await pline_The('sink looks nothing like a fountain.');
            break;
        case RIN_PROTECTION:
            await pline_The('sink glows %s for a moment.',
                hcolor((obj.spe < 0) ? 'black' : 'silver'));
            break;
        case RIN_WARNING:
            await pline_The('sink glows %s for a moment.', hcolor('white'));
            break;
        case RIN_TELEPORT_CONTROL:
            await pline_The('sink looks like it is being beamed aboard somewhere.');
            break;
        case RIN_POLYMORPH_CONTROL:
            await pline_The('sink momentarily looks like a regularly erupting geyser.');
            break;
        default:
            break;
        }
    }
    if (ideed) {
        await trycall(obj);
    } else if (!nosink) {
        Soundeffect('se_ring_in_drain', 50);
        await You_hear('the ring bouncing down the drainpipe.');
    }
    if (!rn2(20) && !nosink) {
        await pline_The('sink backs up, leaving %s.', doname(obj));
        obj.in_use = false;
        await dropx(obj);
    } else if (!rn2(5)) {
        freeinv(obj);
        obj.in_use = false;
        obj.ox = game.u.ux;
        obj.oy = game.u.uy;
        add_to_buried(obj);
    } else {
        useup(obj);
    }
}
// ── canletgo ──
// C ref: do.c:665 — canletgo()
export async function canletgo(obj, word) {
    if (obj.owornmask & (W_ARMOR | W_ACCESSORY)) {
        if (word)
            await Norep('You cannot %s %s you are wearing.', word, 'something');
        return false;
    }
    if (obj === game.u.uwep && welded(game.u.uwep)) {
        if (word) {
            let hand = body_part(HAND);
            if (bimanual(game.u.uwep))
                hand = makeplural(hand);
            await Norep('You cannot %s %s welded to your %s.', word, 'something', hand);
        }
        return false;
    }
    if (obj.otyp === LOADSTONE && obj.cursed) {
        if (word) {
            await pline('For some reason, you cannot %s%s the stone%s!', word,
                obj.corpsenm ? ' any of' : '', plur(obj.quan));
        }
        obj.corpsenm = 0;
        set_bknown(obj, 1);
        return false;
    }
    if (obj.otyp === LEASH && obj.leashmon !== 0) {
        if (word)
            await pline_The('leash is tied around your %s.', body_part(HAND));
        return false;
    }
    if (obj.owornmask & W_SADDLE) {
        if (word)
            await You('cannot %s %s you are sitting on.', word, 'something');
        return false;
    }
    return true;
}
// ── drop (static) ──
// C ref: do.c:714 — drop()
async function drop(obj) {
    const g = game;
    const u = g.u;
    if (!obj)
        return ECMD_FAIL;
    if (!(await canletgo(obj, 'drop')))
        return ECMD_FAIL;
    if (obj.otyp === CORPSE && better_not_try_to_drop_that(obj))
        return ECMD_FAIL;
    if (obj === g.u.uwep) {
        if (welded(g.u.uwep)) {
            await weldmsg(obj);
            return ECMD_FAIL;
        }
        await setuwep(null);
    }
    if (obj === g.u.uquiver) {
        setuqwep(null);
    }
    if (obj === g.u.uswapwep) {
        setuswapwep(null);
    }
    if (u.uswallow) {
        if (g.flags?.verbose) {
            await You('drop %s into %s.', doname(obj), mon_nam(u.ustuck));
        }
    } else {
        if ((obj.oclass === RING_CLASS || obj.otyp === MEAT_RING)
            && IS_SINK(levl(u.ux, u.uy)?.typ)) {
            await dosinkring(obj);
            return ECMD_TIME;
        }
        if (!can_reach_floor(true)) {
            const levhack = finesse_ahriman(obj);
            if (g.flags?.verbose)
                await You('drop %s.', doname(obj));
            freeinv(obj);
            await hitfloor(obj, true);
            if (levhack)
                await float_down(I_SPECIAL | TIMEOUT, W_ARTI | W_ART);
            return ECMD_TIME;
        }
        if (!IS_ALTAR(levl(u.ux, u.uy)?.typ) && g.flags?.verbose)
            await You('drop %s.', doname(obj));
    }
    obj.how_lost = LOST_DROPPED;
    await dropx(obj);
    return ECMD_TIME;
}
// ── better_not_try_to_drop_that ──
// C ref: do.c:947 — better_not_try_to_drop_that()
function better_not_try_to_drop_that(otmp) {
    if (otmp.otyp === CORPSE && !u_safe_from_fatal_corpse(otmp, 0)) {
        return (paranoid_ynq(true,
            `Drop the ${obj_pmname(otmp)} corpse without ${body_part(HAND)} protection on?`,
            false) !== 'y');
    }
    return false;
}
// ── menudrop_split ──
// C ref: do.c:963 — menudrop_split()
async function menudrop_split(otmp, cnt) {
    if (cnt && cnt < otmp.quan) {
        if (welded(otmp)) {
            ; // don't split
        } else if (otmp.otyp === LOADSTONE && otmp.cursed) {
            // same kludge as getobj(), for canletgo()'s use
            otmp.corpsenm = cnt;
        } else {
            otmp = splitobj(otmp, cnt);
        }
    }
    return await drop(otmp);
}
// ── dropx ──
// C ref: do.c:786 — dropx()
export async function dropx(obj) {
    freeinv(obj);
    if (!game.u?.uswallow) {
        if (await ship_object(obj, game.u.ux, game.u.uy, false))
            return;
        if (IS_ALTAR(levl(game.u.ux, game.u.uy)?.typ))
            await doaltarobj(obj);
    }
    await dropy(obj);
}
// ── dropy ──
// C ref: do.c:800 — dropy()
export async function dropy(obj) {
    await dropz(obj, false);
}
// ── dropz ──
// C ref: do.c:806 — dropz()
export async function dropz(obj, with_impact) {
    if (obj === game.u.uwep)
        await setuwep(null);
    if (obj === game.u.uquiver)
        setuqwep(null);
    if (obj === game.u.uswapwep)
        setuswapwep(null);
    if (game.u?.uswallow) {
        if (obj !== game.u.uball) {
            if (is_unpaid(obj))
                await stolen_value(obj, game.u.ux, game.u.uy, true, false);
            if (!(await engulfer_digests_food(obj)))
                await mpickobj(game.u.ustuck, obj);
        }
    } else {
        if (await flooreffects(obj, game.u.ux, game.u.uy, 'drop'))
            return;
        place_object(obj, game.u.ux, game.u.uy);
        if (with_impact)
            await container_impact_dmg(obj, game.u.ux, game.u.uy);
        impact_disturbs_zombies(obj, with_impact);
        if (obj === game.u.uball)
            await drop_ball(game.u.ux, game.u.uy);
        else if (game.level?.flags?.has_shop)
            sellobj(obj, game.u.ux, game.u.uy);
        stackobj(obj);
        if (Blind() && Levitation())
            map_object(obj, 0);
        newsym(game.u.ux, game.u.uy);
    }
    await encumber_msg();
}
// ── engulfer_digests_food ──
// C ref: do.c:849 — engulfer_digests_food()
async function engulfer_digests_food(obj) {
    if (digests(game.u?.ustuck?.data)
        && (obj.otyp === CORPSE || obj.globby
            || obj.otyp === MEATBALL || obj.otyp === ENORMOUS_MEATBALL
            || obj.otyp === MEAT_RING || obj.otyp === MEAT_STICK)) {
        let could_petrify = false,
            could_poly = false, could_slime = false,
            could_grow = false, could_heal = false;
        if (obj.otyp === CORPSE) {
            could_petrify = touch_petrifies(mons[obj.corpsenm]);
            could_poly = polyfood(obj);
            could_grow = (obj.corpsenm === PM_WRAITH);
            could_heal = (obj.corpsenm === PM_NURSE);
        } else if (obj.otyp === GLOB_OF_GREEN_SLIME) {
            could_slime = true;
        }
        // C: pline("%s instantly digested!", Tobjnam(obj, "are"));
        await pline('%s instantly digested!', Tobjnam(obj, 'are'));
        if (could_poly || could_slime) {
            await newcham(game.u.ustuck, could_slime ? mons[PM_GREEN_SLIME] : 0, 0);
        } else if (could_petrify) {
            minstapetrify(game.u.ustuck, true);
        } else if (could_grow) {
            grow_up(game.u.ustuck, null);
        } else if (could_heal) {
            healmon(game.u.ustuck, game.u.ustuck.mhpmax, 0);
            await mcureblindness(game.u.ustuck, false);
        }
        delobj(obj);
        return true;
    }
    return false;
}
// ── obj_no_longer_held ──
// C ref: do.c:892 — obj_no_longer_held()
export function obj_no_longer_held(obj) {
    if (!obj) {
        return;
    } else if (obj.cobj) {
        // Has_contents — recurse into container
        let contents = obj.cobj;
        while (contents) {
            obj_no_longer_held(contents);
            contents = contents.nobj;
        }
    }
    switch (obj.otyp) {
    case CRYSKNIFE:
        if (!obj.oerodeproof || !rn2(10)) {
            if (!game.context?.mon_moving)
                costly_alteration(obj, 18); // COST_DEGRD
            obj.otyp = WORM_TOOTH;
            obj.oerodeproof = 0;
        }
        break;
    }
}
// ── dodrop ──
// C ref: do.c:28 — dodrop() — the #drop command
export async function dodrop() {
    if (game.u?.ushops)
        sellobj_state(SELL_DELIBERATE);
    const result = await drop(await getobj('drop', null, GETOBJ_PROMPT | GETOBJ_ALLOWCNT));
    if (game.u?.ushops)
        sellobj_state(SELL_NORMAL);
    if (result)
        reset_occupations();
    return result;
}
// ── doddrop ──
// C ref: do.c:922 — doddrop() — the #droptype command
export async function doddrop() {
    let result = ECMD_OK;
    if (!game.invent) {
        await You('have nothing to drop.');
        return ECMD_OK;
    }
    add_valid_menu_class(0);
    if (game.u?.ushops)
        sellobj_state(SELL_DELIBERATE);
    // Simplified: menu_drop not yet fully ported
    result = menu_drop_stub(result);
    if (game.u?.ushops)
        sellobj_state(SELL_NORMAL);
    if (result)
        reset_occupations();
    return result;
}
// ── u_stuck_cannot_go ──
// C ref: do.c:1109 — u_stuck_cannot_go()
async function u_stuck_cannot_go(updn) {
    const u = game.u;
    if (u.ustuck) {
        if (u.uswallow || !sticks(game.player?.data)) {
            await You('are %s, and cannot go %s.',
                !u.uswallow ? 'being held'
                : digests(u.ustuck.data) ? 'swallowed'
                : 'engulfed', updn);
            return true;
        } else {
            const mtmp = u.ustuck;
            set_ustuck(null);
            await You('release %s.', mon_nam(mtmp));
        }
    }
    return false;
}
// ── dodown ──
// C ref: do.c:1129 — dodown()
// The full implementation is complex and involves goto_level.
// Stub: most logic handled in cmd.js dispatch.
export async function dodown() {
    // Stub — full dodown logic is in cmd.js dispatch
    await You_cant('go down here.');
    return ECMD_OK;
}
// ── doup ──
// C ref: do.c:1296 — doup()
// Stub: most logic handled in cmd.js dispatch.
export async function doup() {
    // Stub — full doup logic is in cmd.js dispatch
    await You_cant('go up here.');
    return ECMD_OK;
}
// ── u_collide_m ──
// C ref: do.c:1412-1445 — handle hero/monster collision on level arrival
export async function u_collide_m(mtmp) {
    if (!mtmp || mtmp === game.u?.usteed || mtmp !== m_at(game.u.ux, game.u.uy)) {
        impossible('level arrival collision');
        return;
    }
    // C ref: do.c:1429 — randomly move hero to adjacent spot or monster elsewhere
    const cc = {};
    if (!rn2(2) && enexto_core(cc, game.u.ux, game.u.uy, game.youmonst?.data, 0)
        && Math.abs(cc.x - game.u.ux) <= 1 && Math.abs(cc.y - game.u.uy) <= 1) {
        u_on_newpos(cc.x, cc.y);
    } else {
        await mnexto(mtmp, 0 /* RLOC_NOMSG */);
    }
    // C ref: do.c:1435 — if monster still on hero, try harder
    mtmp = m_at(game.u.ux, game.u.uy);
    if (mtmp) {
        if (!(await rloc(mtmp, 0)) || m_at(game.u.ux, game.u.uy)) {
            mongone(m_at(game.u.ux, game.u.uy)); // m_into_limbo
        }
    }
}
// ── familiar_level_msg ──
// C ref: do.c:1447 — familiar_level_msg()
export async function familiar_level_msg() {
    const fam_msgs = [
        'You have a sense of deja vu.',
        'You feel like you\'ve been here before.',
        'This place %s familiar...',
        null,
    ];
    const halu_fam_msgs = [
        'Whoa!  Everything %s different.',
        'You are surrounded by twisty little passages, all alike.',
        'Gee, this %s like uncle Conan\'s place...',
        null,
    ];
    const which = rn2(4);
    let mesg;
    if (Hallucination())
        mesg = halu_fam_msgs[which];
    else
        mesg = fam_msgs[which];
    if (mesg && mesg.includes('%s')) {
        mesg = mesg.replace('%s', !Blind() ? 'looks' : 'seems');
    }
    if (mesg)
        await pline(mesg);
}
// ── temperature_change_msg ──
// C ref: do.c:2015 — temperature_change_msg()
export async function temperature_change_msg(prev_temperature) {
    if (prev_temperature !== (game.level?.flags?.temperature || 0)) {
        if (game.level?.flags?.temperature)
            await hellish_smoke_mesg();
        else if (prev_temperature > 0)
            await pline_The('heat %s gone.',
                Inhell(game.u?.uz0) ? 'and smoke are' : 'is');
        else if (prev_temperature < 0)
            await You('are out of the cold.');
    }
}
// ── hellish_smoke_mesg ──
// C ref: do.c:2003 — hellish_smoke_mesg()
export async function hellish_smoke_mesg() {
    if (game.level?.flags?.temperature)
        await pline('It is %s here.',
            game.level.flags.temperature > 0 ? 'hot' : 'cold');
    if (Inhell(game.u?.uz) && (game.level?.flags?.temperature || 0) > 0)
        await You('%s smoke...',
            olfaction(game.player?.data) ? 'smell' : 'sense');
}
// ── maybe_lvltport_feedback ──
// C ref: do.c:2032 — maybe_lvltport_feedback()
export async function maybe_lvltport_feedback() {
    if (game.dfr_post_msg
        && game.dfr_post_msg.toLowerCase().startsWith('you materialize')) {
        await pline(game.dfr_post_msg);
        game.dfr_post_msg = null;
    }
}
// ── schedule_goto ──
// C ref: do.c:2056 — schedule_goto()
export function schedule_goto(tolev, utotype_flags, pre_msg, post_msg) {
    game.u.utotype = utotype_flags | UTOTYPE_DEFERRED;
    assign_level(game.u.utolev, tolev);
    if (pre_msg)
        game.dfr_pre_msg = pre_msg;
    if (post_msg)
        game.dfr_post_msg = post_msg;
}
// ── deferred_goto ──
// C ref: do.c:2074 — deferred_goto()
export async function deferred_goto() {
    // Canonical implementation lives in allmain.js (ports do.c:2075 path).
    // Delegate dynamically to avoid stale duplicate behavior and keep callers
    // of do.js/deferred_goto safe if this symbol is referenced.
    const mod = await import('./allmain.js');
    return await mod.deferred_goto();
}
// ── revive_corpse ──
// C ref: do.c:2110 — revive_corpse()
export async function revive_corpse(corpse) {
    const where = corpse.where;
    const montype = corpse.corpsenm;
    const is_zomb = (mons[montype]?.mlet === S_ZOMBIE);
    const chewed = (corpse.oeaten !== 0);
    const cname = corpse_xname(corpse, chewed ? 'bite-covered' : null, 0);
    const mtmp = await revive(corpse, false);
    if (mtmp) {
        switch (where) {
        case OBJ_INVENT:
            await You_feel('squirming in your backpack!');
            break;
        case OBJ_FLOOR:
            if (canseemon(mtmp)) {
                await pline('%s rises from the dead!', Monnam(mtmp));
            }
            break;
        case OBJ_MINVENT:
            if (canspotmon(mtmp))
                await pline('%s suddenly appears!', Monnam(mtmp));
            break;
        case OBJ_BURIED:
            if (is_zomb) {
                // zombie digs itself out
                if (cansee(mtmp.mx, mtmp.my)) {
                    await pline('%s claws itself out of the ground!',
                        canspotmon(mtmp) ? Amonnam(mtmp) : 'Something');
                    newsym(mtmp.mx, mtmp.my);
                } else if (mdistu(mtmp) < 25) {
                    Soundeffect('se_scratching', 50);
                    await You_hear('scratching noises.');
                }
                fill_pit(mtmp.mx, mtmp.my);
            }
            break;
        default:
            impossible(`revive_corpse: lost corpse @ ${where}`);
            break;
        }
        return true;
    }
    return false;
}
// ── revive_mon ──
// C ref: do.c:2250 — revive_mon()
export async function revive_mon(arg, _timeout) {
    const body = arg.a_obj;
    if (!body) return;
    if (!(await revive_corpse(body))) {
        // Failed to revive — set up rot timer (simplified)
    }
}
// ── zombify_mon ──
// C ref: do.c:2298 — zombify_mon()
export async function zombify_mon(arg, timeout) {
    const body = arg.a_obj;
    if (!body) return;
    const zmon = zombie_form(mons[body.corpsenm]);
    if (zmon !== NON_PM) {
        if (has_omid(body)) free_omid(body);
        if (has_omonst(body)) free_omonst(body);
        set_corpsenm(body, zmon);
        await revive_mon(arg, timeout);
    } else {
        await rot_corpse(arg, timeout);
    }
}
// ── danger_uprops ──
// C ref: do.c:2318 — danger_uprops()
function danger_uprops() {
    return (Stoned() || Slimed() || Strangled() || Sick());
}
// ── cmd_safety_prevention ──
// C ref: do.c:2342 — cmd_safety_prevention(ucverb, cmddesc, act, flagcounter)
// Checks for nearby monsters or dangerous intrinsics before wait/search.
export async function cmd_safety_prevention(ucverb, cmddesc, act, flagcounter) {
    pushRngLogEntry('>cmd_safety_prevention');
    if ((game.flags?.safe_wait !== false) && !game.iflags?.menu_requested && !game.multi) {
        let suffix = '';
        if (flagcounter) {
            if (!game._cmd_safety_flags) game._cmd_safety_flags = Object.create(null);
            const counter = game._cmd_safety_flags[flagcounter] || 0;
            // C builds this from visctrl(cmd_from_func(do_reqmenu)); command key is 'm'.
            if (game.iflags?.cmdassist || counter === 0) {
                suffix = `  Use 'm' prefix to force ${cmddesc}.`;
            }
            game._cmd_safety_flags[flagcounter] = counter + 1;
        }
        if (monster_nearby()) {
            await Norep('%s%s', act, suffix);
            pushRngLogEntry('<cmd_safety_prevention=1');
            return true;
        } else if (danger_uprops()) {
            await Norep("%s doesn't feel like a good idea right now.", ucverb);
            pushRngLogEntry('<cmd_safety_prevention=1');
            return true;
        }
    }
    if (flagcounter && game._cmd_safety_flags) game._cmd_safety_flags[flagcounter] = 0;
    pushRngLogEntry('<cmd_safety_prevention=0');
    return false;
}
// ── monster_nearby ──
// C ref: mon.c — monster_nearby()
// Check if any hostile monster is adjacent to the player.
// ── donull ──
// C ref: do.c:2349 — donull()
export async function donull() {
    if (await cmd_safety_prevention('Waiting', 'a no-op (to rest)',
                              'Are you waiting to get hit?', 'did_nothing_flag'))
        return ECMD_OK;
    return ECMD_TIME;
}
// ── wipeoff ──
// C ref: do.c:2360 — wipeoff()
function wipeoff() {
    const u = game.u;
    let udelta = u.ucreamed || 0;
    // Simplified: just reduce ucreamed
    if (udelta > 4) udelta = 4;
    u.ucreamed = (u.ucreamed || 0) - udelta;
    // HBlinded timeout reduction skipped (needs property system)
    if (!u.ucreamed) {
        return 0; // done
    }
    return 1; // still busy
}
// ── dowipe ──
// C ref: do.c:2389 — dowipe()
export async function dowipe() {
    if (game.u?.ucreamed) {
        set_occupation(wipeoff, `wiping off your ${body_part(FACE)}`, 0);
        return ECMD_TIME;
    }
    await Your('%s is already clean.', body_part(FACE));
    return ECMD_TIME;
}
// ── legs_in_no_shape ──
// C ref: do.c:2431 — legs_in_no_shape()
export async function legs_in_no_shape(for_what, by_steed) {
    if (by_steed && game.u?.usteed) {
        await pline('%s is in no shape for %s.', Monnam(game.u.usteed), for_what);
    } else {
        const wl = (EWounded_legs_val() & BOTH_SIDES);
        let bp = body_part(LEG);
        if (wl === BOTH_SIDES)
            bp = makeplural(bp);
        await Your('%s%s %s in no shape for %s.',
            (wl === LEFT_SIDE) ? 'left ' : (wl === RIGHT_SIDE) ? 'right ' : '',
            bp, (wl === BOTH_SIDES) ? 'are' : 'is', for_what);
    }
}
// ── set_wounded_legs ──
// C ref: do.c:2449 — set_wounded_legs()
export async function set_wounded_legs(side, timex) {
    const u = game.u;
    setBotl('set_wounded_legs');
    if (!Wounded_legs()) {
        // ATEMP(A_DEX)--
        if (!u.atemp) u.atemp = { a: [0,0,0,0,0,0] };
        u.atemp.a[A_DEX]--;
    }
    const curTimeout = HWounded_legs_val() & TIMEOUT;
    if (!Wounded_legs() || curTimeout < timex)
        set_itimeout(WOUNDED_LEGS, timex);
    // EWounded_legs |= side
    if (!u.uprops) u.uprops = {};
    if (!u.uprops[WOUNDED_LEGS]) u.uprops[WOUNDED_LEGS] = { intrinsic: 0, extrinsic: 0 };
    u.uprops[WOUNDED_LEGS].extrinsic = (u.uprops[WOUNDED_LEGS].extrinsic || 0) | side;
    await encumber_msg();
}
// ── heal_legs ──
// C ref: do.c:2472 — heal_legs()
export async function heal_legs(how) {
    const u = game.u;
    if (Wounded_legs()) {
        setBotl('heal_legs');
        // ATEMP(A_DEX)++ if negative
        if (!u.atemp) u.atemp = { a: [0,0,0,0,0,0] };
        if (u.atemp.a[A_DEX] < 0)
            u.atemp.a[A_DEX]++;
        if (!u.usteed && how !== 2) {
            let legs = body_part(LEG);
            if ((EWounded_legs_val() & BOTH_SIDES) === BOTH_SIDES)
                legs = makeplural(legs);
            await Your('%s %s better.', legs, vtense(legs, 'feel'));
        }
        // HWounded_legs = EWounded_legs = 0
        if (u.uprops && u.uprops[WOUNDED_LEGS]) {
            u.uprops[WOUNDED_LEGS].intrinsic = 0;
            u.uprops[WOUNDED_LEGS].extrinsic = 0;
        }
        if (how === 0)
            await encumber_msg();
    }
}