All files / js dungeon.js

94.31% Statements 945/1002
82.44% Branches 202/245
92.5% Functions 37/40
94.31% Lines 945/1002

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 100373x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 71x 71x 71x 71x 71x 73x 73x 73x 71x 71x 71x 355x 355x 71x 71x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 71x 71x 71x 71x 71x 71x 71x 71x 71x 71x 71x 73x 73x 73x 73x 73x 3188x 3188x 3188x 3188x 369x 369x 369x 3188x 2819x 2819x 3188x 3188x 3188x 3188x 3188x 3188x 3188x 3188x   3188x 1328x 3188x 1860x 1860x 3188x 3188x 73x 73x 73x 73x 73x 2691x 2691x 2691x 2691x 2691x 2691x 2691x 2691x 2691x 2691x 2691x 2691x 5731x 5731x 2691x 2691x 2691x 7499x 727x 727x 7499x 2691x 2691x 2691x 2691x 86112x 86112x 2691x 2691x 2691x 73x 73x 73x 73x 73x 2691x 2691x 21054x 21054x   2691x 73x 73x 73x 73x 73x 3366x 3366x 2727x 2727x 3366x 2691x 2691x 2691x 2691x 2691x 2691x 100x 100x 100x 3366x 73x 73x 73x 73x 73x 2627x 2627x 2627x 2627x 2627x 2627x 2591x 2591x 2591x 2627x 2627x 2627x 2627x 2627x 2627x 2627x 2627x 2627x 2627x 2627x 2627x 2627x 2627x 73x 73x 73x 73x 73x 497x 497x 497x 497x 497x 497x 497x   497x 73x 1988x 1988x 7952x 7952x   1988x 73x 994x 994x 994x 1136x 1136x 1136x   994x 73x 497x 497x 497x 497x 497x 497x 497x 497x 497x 497x 497x 497x 497x 497x 497x 497x 497x 497x 497x 497x 497x 497x 497x 497x 497x 1491x 1491x       1491x 497x 497x 497x 497x 497x 73x 3266x 3266x 3266x 3266x 3266x 73x 497x 497x 497x 497x 497x 497x 497x 497x 1065x 1065x 355x 355x 355x 1065x 497x 497x 73x 497x 497x 497x 497x 497x 497x 497x 497x 497x 497x 497x 497x 497x 497x 497x 497x 73x 73x 73x 73x 73x 2591x 2591x 71x 71x 71x 71x 2520x 2520x 2520x 2591x 45651x 45651x 45651x 44906x 44906x 44906x 2591x 71x 71x 2591x 2449x 2449x 2449x 2591x 73x 73x 73x 73x 73x 639x 639x 639x 213x 213x 213x 213x 639x       426x 426x 426x 639x 73x 73x 73x 73x 73x 497x 497x 497x 497x 497x     497x 497x 497x 497x 497x 497x 497x 497x 497x 73x 12475x 12475x 12475x 73x 73x 73x 73x 73x 73x 639x 639x 639x 639x 639x 639x       639x 639x 639x 639x 2627x 2627x 2627x 2627x 2627x 2627x 2627x 2627x 2627x 2627x 2627x 2627x 2627x 2627x 2627x 142x 1562x 142x 142x 142x 1562x 2627x     2627x 639x 639x 639x 639x 497x 497x 497x 497x 497x 497x 497x 497x 497x 497x 497x 497x 213x 639x 213x 213x 213x 639x 497x     497x 639x 639x 639x 639x 639x 639x 639x 639x 639x 639x 639x 639x 639x 639x 639x 639x 639x 639x 639x 639x 639x 639x 639x 639x 639x 639x 639x 639x 639x 639x 284x 639x 355x 355x 639x 639x 639x 71x 71x 71x 639x 568x 568x 568x 568x 639x 639x 639x 639x 639x 639x 639x 639x 639x 639x 639x 639x 639x 639x 639x 639x 639x 71x 639x 497x 497x 639x 639x 639x 639x 639x 73x 73x 73x 73x 73x 71x 71x 71x 71x 71x 71x 71x 71x 71x 71x 71x 71x 71x 71x 71x 71x 71x 71x 71x 71x 71x 71x 71x 71x 71x 71x 71x 71x 71x 1846x 1846x 1846x 1810x 1810x 213x 213x 213x 1810x 1810x 1810x 1810x 1810x 71x 71x 355x 71x 71x 71x 71x 71x 71x 1810x 1846x 71x 71x 71x 355x 1988x 1988x   355x 71x 71x 71x 71x 71x 71x 71x 71x 71x 71x 71x 71x 71x 71x 71x 71x 71x 73x 73x 7897x 7897x 241134x 233327x 233327x 90x 7897x 73x 834x 834x 834x 834x 834x 834x 834x 834x 834x 73x 5989x 5989x 5989x 73x 73x 73x 5989x 5989x 5989x 5850x 5989x 5709x 5989x 73x 73x 5989x 5989x 5989x 5989x 5989x 5989x 5989x 5989x 280x 5989x 5709x 5709x 5989x 5989x 5989x 5989x 5989x 5989x 73x 6408x 6408x 44856x 44856x 44856x 834x 834x 834x 834x 834x 44856x 6408x 73x 73x 73x 141x 141x 141x 141x 141x 141x 141x 1269x 1269x 1269x 1253x 1253x 1269x 1269x 1269x 1269x 1114x 1269x 139x 139x 1269x 419x 278x 419x 141x 141x 419x 1253x 1253x 1253x 1269x 46361x 5155x 5155x 5155x 46361x 139x 139x 5155x 5155x 5155x 1253x 1253x 141x 141x 141x 141x 141x 140x 140x 140x 140x 140x 1x 141x 73x 73x 73x 73x 73x 73x 71x 71x 71x 71x 71x 71x 71x 71x 71x 71x 71x 71x 71x 71x 71x 71x 71x 639x 639x 639x 639x 639x 2627x 2627x 639x 639x 639x     639x 639x 639x 2627x 2591x 2591x 2627x 639x 639x 639x 639x 71x 71x 71x 71x 71x 71x 71x 73x 73x 73x 73x 73x 17x 17x 17x 17x 73x 73x 73x 73x 475x 475x 73x 73x 73x 82x 82x 73x 73x 73x     73x 73x 314x 314x 73x 73x 73x 197x 197x 197x 73x 73x 73x 73x                                             73x 73x 73x 61x 61x 61x 61x 61x 61x 61x     61x 61x 61x 61x 61x 25x 25x 25x 25x 25x 25x 36x 36x 36x 44x 2x 2x 2x 2x         2x 2x         2x 2x 2x 36x 36x 61x 61x 36x 36x 36x 61x 73x 73x 72x 72x  
// dungeon.js — Dungeon tree initialization (port of dungeon.c)
// Faithful port of init_dungeons() and all helper functions to match
// the exact RNG call sequence of the C version.
 
import { game } from './gstate.js';
import { rn2, rn1 } from './rng.js';
import { add_menu, add_menu_heading, create_nhwindow_menu, destroy_nhwindow_menu, end_menu, select_menu, start_menu } from './menu.js';
import { ATR_INVERSE, NO_COLOR } from './terminal.js';
import { Align2amask, BR_NO_END1, BR_NO_END2, BR_PORTAL, BR_STAIR, D_ALIGN_CHAOTIC, D_ALIGN_LAWFUL, D_ALIGN_MASK, D_ALIGN_NEUTRAL, HELLISH, MAZELIKE, ROGUELIKE, TBR_NO_DOWN, TBR_NO_UP, TBR_PORTAL, TBR_STAIR, TOWN, UNCONNECTED } from './const.js';
import { on_level } from './hacklib.js';
import { In_endgame as in_endgame } from './macros.js';
 
const MAXLEVEL = 32;
const MAXDUNGEON = 16;
 
// C ref: nhlib.lua — When dungeon.lua is loaded, nhlib.lua runs first.
// nhlib.lua overrides math.random to use nh.rn2(), then shuffles
// align = { "law", "neutral", "chaos" } — consuming 2 core RNG calls.
function nhlib_init_shuffle() {
    // Fisher-Yates shuffle of 3-element array: rn2(3), rn2(2)
    rn2(3); // math.random(3) => 1 + rn2(3)
    rn2(2); // math.random(2) => 1 + rn2(2)
}
 
// C ref: dungeon.c:1110 init_castle_tune()
// 5 RNG calls: rn2(7) each
function init_castle_tune() {
    const tune = [];
    for (let i = 0; i < 5; i++) {
        tune.push(String.fromCharCode('A'.charCodeAt(0) + rn2(7)));
    }
    game.castle_tune = tune.join('');
}
 
// ============================================================
// Dungeon definition data — hardcoded from dat/dungeon.lua
// ============================================================
 
// Each dungeon definition, in the order they appear in dungeon.lua.
// chance defaults to 100 if not specified (matches C get_table_int_opt).
// entry defaults to 0 if not specified.
// range defaults to 0 if not specified.
const DUNGEON_DEFS = [
    {
        name: 'The Dungeons of Doom', bonetag: 'D', base: 25, range: 5,
        align: 0, entry: 0, chance: 100, flags: 0,
        protoname: '', fill: '', themerms: 'themerms.lua',
        levels: [
            { name: 'rogue', bonetag: 'R', base: 15, range: 4, chance: 100, nlevels: 0, chain: -1 },
            { name: 'oracle', bonetag: 'O', base: 5, range: 5, chance: 100, nlevels: 0, chain: -1, align: D_ALIGN_NEUTRAL },
            { name: 'bigrm', bonetag: 'B', base: 10, range: 3, chance: 40, nlevels: 13, chain: -1 },
            { name: 'medusa', bonetag: '', base: -5, range: 4, chance: 100, nlevels: 4, chain: -1, align: D_ALIGN_CHAOTIC },
            { name: 'castle', bonetag: '', base: -1, range: 0, chance: 100, nlevels: 0, chain: -1 },
        ],
        branches: [
            { name: 'The Gnomish Mines', base: 2, range: 3, type: TBR_STAIR, up: false, chain: -1 },
            { name: 'Sokoban', base: 1, range: 0, type: TBR_STAIR, up: true, chainlevel: 'oracle' },
            { name: 'The Quest', base: 6, range: 2, type: TBR_PORTAL, up: false, chainlevel: 'oracle' },
            { name: 'Fort Ludios', base: 18, range: 4, type: TBR_PORTAL, up: false, chain: -1 },
            { name: 'Gehennom', base: 0, range: 0, type: TBR_NO_DOWN, up: false, chainlevel: 'castle' },
            { name: 'The Elemental Planes', base: 1, range: 0, type: TBR_NO_DOWN, up: true, chain: -1 },
        ],
    },
    {
        name: 'Gehennom', bonetag: 'G', base: 20, range: 5,
        align: 0, entry: 0, chance: 100, flags: MAZELIKE | HELLISH,
        protoname: '', fill: 'hellfill', themerms: '',
        levels: [
            { name: 'valley', bonetag: 'V', base: 1, range: 0, chance: 100, nlevels: 0, chain: -1 },
            { name: 'sanctum', bonetag: '', base: -1, range: 0, chance: 100, nlevels: 0, chain: -1 },
            { name: 'juiblex', bonetag: 'J', base: 4, range: 4, chance: 100, nlevels: 0, chain: -1 },
            { name: 'baalz', bonetag: 'B', base: 6, range: 4, chance: 100, nlevels: 0, chain: -1 },
            { name: 'asmodeus', bonetag: 'A', base: 2, range: 6, chance: 100, nlevels: 0, chain: -1 },
            { name: 'wizard1', bonetag: '', base: 11, range: 6, chance: 100, nlevels: 0, chain: -1 },
            { name: 'wizard2', bonetag: 'X', base: 1, range: 0, chance: 100, nlevels: 0, chainlevel: 'wizard1' },
            { name: 'wizard3', bonetag: 'Y', base: 2, range: 0, chance: 100, nlevels: 0, chainlevel: 'wizard1' },
            { name: 'orcus', bonetag: 'O', base: 10, range: 6, chance: 100, nlevels: 0, chain: -1 },
            { name: 'fakewiz1', bonetag: 'F', base: -6, range: 4, chance: 100, nlevels: 0, chain: -1 },
            { name: 'fakewiz2', bonetag: 'G', base: -6, range: 4, chance: 100, nlevels: 0, chain: -1 },
        ],
        branches: [
            { name: "Vlad's Tower", base: 9, range: 5, type: TBR_STAIR, up: true, chain: -1 },
        ],
    },
    {
        name: 'The Gnomish Mines', bonetag: 'M', base: 8, range: 2,
        align: D_ALIGN_LAWFUL, entry: 0, chance: 100, flags: MAZELIKE,
        protoname: '', fill: 'minefill', themerms: '',
        levels: [
            { name: 'minetn', bonetag: 'T', base: 3, range: 2, chance: 100, nlevels: 7, chain: -1, flags: TOWN },
            { name: 'minend', bonetag: '', base: -1, range: 0, chance: 100, nlevels: 3, chain: -1 },
        ],
        branches: [],
    },
    {
        name: 'The Quest', bonetag: 'Q', base: 5, range: 2,
        align: 0, entry: 0, chance: 100, flags: 0,
        protoname: '', fill: '', themerms: '',
        levels: [
            { name: 'x-strt', bonetag: '', base: 1, range: 1, chance: 100, nlevels: 0, chain: -1 },
            { name: 'x-loca', bonetag: 'L', base: 3, range: 1, chance: 100, nlevels: 0, chain: -1 },
            { name: 'x-goal', bonetag: '', base: -1, range: 0, chance: 100, nlevels: 0, chain: -1 },
        ],
        branches: [],
    },
    {
        name: 'Sokoban', bonetag: '', base: 4, range: 0,
        align: D_ALIGN_NEUTRAL, entry: -1, chance: 100, flags: MAZELIKE,
        protoname: '', fill: '', themerms: '',
        levels: [
            { name: 'soko1', bonetag: '', base: 1, range: 0, chance: 100, nlevels: 2, chain: -1 },
            { name: 'soko2', bonetag: '', base: 2, range: 0, chance: 100, nlevels: 2, chain: -1 },
            { name: 'soko3', bonetag: '', base: 3, range: 0, chance: 100, nlevels: 2, chain: -1 },
            { name: 'soko4', bonetag: '', base: 4, range: 0, chance: 100, nlevels: 2, chain: -1 },
        ],
        branches: [],
    },
    {
        name: 'Fort Ludios', bonetag: 'K', base: 1, range: 0,
        align: 0, entry: 0, chance: 100, flags: MAZELIKE,
        protoname: '', fill: '', themerms: '',
        levels: [
            { name: 'knox', bonetag: 'K', base: -1, range: 0, chance: 100, nlevels: 0, chain: -1 },
        ],
        branches: [],
    },
    {
        name: "Vlad's Tower", bonetag: 'T', base: 3, range: 0,
        align: D_ALIGN_CHAOTIC, entry: -1, chance: 100, flags: MAZELIKE,
        protoname: 'tower', fill: '', themerms: '',
        levels: [
            { name: 'tower1', bonetag: '', base: 1, range: 0, chance: 100, nlevels: 0, chain: -1 },
            { name: 'tower2', bonetag: '', base: 2, range: 0, chance: 100, nlevels: 0, chain: -1 },
            { name: 'tower3', bonetag: '', base: 3, range: 0, chance: 100, nlevels: 0, chain: -1 },
        ],
        branches: [],
    },
    {
        name: 'The Elemental Planes', bonetag: 'E', base: 6, range: 0,
        align: 0, entry: -2, chance: 100, flags: MAZELIKE,
        protoname: '', fill: '', themerms: '',
        levels: [
            { name: 'astral', bonetag: '', base: 1, range: 0, chance: 100, nlevels: 0, chain: -1 },
            { name: 'water', bonetag: '', base: 2, range: 0, chance: 100, nlevels: 0, chain: -1 },
            { name: 'fire', bonetag: '', base: 3, range: 0, chance: 100, nlevels: 0, chain: -1 },
            { name: 'air', bonetag: '', base: 4, range: 0, chance: 100, nlevels: 0, chain: -1 },
            { name: 'earth', bonetag: '', base: 5, range: 0, chance: 100, nlevels: 0, chain: -1 },
            { name: 'dummy', bonetag: '', base: 6, range: 0, chance: 100, nlevels: 0, chain: -1 },
        ],
        branches: [],
    },
    {
        name: 'The Tutorial', bonetag: '', base: 2, range: 0,
        align: 0, entry: 0, chance: 100, flags: MAZELIKE | UNCONNECTED,
        protoname: '', fill: '', themerms: '',
        levels: [
            { name: 'tut-1', bonetag: '', base: 1, range: 0, chance: 100, nlevels: 0, chain: -1 },
            { name: 'tut-2', bonetag: '', base: 2, range: 0, chance: 100, nlevels: 0, chain: -1 },
        ],
        branches: [],
    },
];
 
// ============================================================
// Proto dungeon state — mirrors C struct proto_dungeon
// ============================================================
 
function make_proto_dungeon() {
    return {
        tmpdungeon: [],  // per-dungeon info
        tmplevel: [],    // all levels across all dungeons
        final_lev: [],   // placed s_level objects (or null)
        tmpbranch: [],   // all branches across all dungeons
        start: 0,        // starting index of current dungeon's levels
        n_levs: 0,       // total level count so far
        n_brs: 0,        // total branch count so far
    };
}
 
// ============================================================
// Helper: level_range (dungeon.c:380)
// ============================================================
 
function level_range(dgn, base, randc, chain, pd) {
    const lmax = game.dungeons[dgn].num_dunlevs;
 
    if (chain >= 0) {
        const levtmp = pd.final_lev[chain];
        if (!levtmp) throw new Error('level_range: empty chain level!');
        base += levtmp.dlevel.dlevel;
    } else {
        if (base < 0) base = lmax + base + 1;
    }
 
    if (base < 1 || base > lmax)
        throw new Error(`level_range: base value out of range (base=${base}, lmax=${lmax})`);
 
    const adjusted_base = base;
 
    let count;
    if (randc === -1) {
        count = lmax - base + 1;
    } else if (randc) {
        count = ((base + randc - 1) > lmax) ? lmax - base + 1 : randc;
    } else {
        count = 1;
    }
    return { count, adjusted_base };
}
 
// ============================================================
// Helper: possible_places (dungeon.c:597)
// ============================================================
 
function possible_places(idx, pd) {
    const lev = pd.final_lev[idx];
    const map = new Array(MAXLEVEL + 1).fill(false);
 
    const { count, adjusted_base: start } = level_range(
        lev.dlevel.dnum,
        pd.tmplevel[idx].lev_base,
        pd.tmplevel[idx].lev_rand,
        pd.tmplevel[idx].chain,
        pd
    );
    for (let i = start; i < start + count; i++) {
        map[i] = true;
    }
 
    // mark off already placed levels
    for (let i = pd.start; i < idx; i++) {
        if (pd.final_lev[i] && map[pd.final_lev[i].dlevel.dlevel]) {
            map[pd.final_lev[i].dlevel.dlevel] = false;
        }
    }
 
    // recount
    let npossible = 0;
    for (let i = 1; i <= MAXLEVEL; i++) {
        if (map[i]) npossible++;
    }
 
    return { npossible, map };
}
 
// ============================================================
// Helper: pick_level (dungeon.c:631)
// ============================================================
 
function pick_level(map, nth) {
    for (let i = 1; i <= MAXLEVEL; i++) {
        if (map[i] && nth-- === 0) return i;
    }
    throw new Error('pick_level: ran out of valid levels');
}
 
// ============================================================
// place_level (dungeon.c:666) — recursive
// ============================================================
 
function place_level(proto_index, pd) {
    if (proto_index === pd.n_levs) return true;
 
    const lev = pd.final_lev[proto_index];
    if (!lev) return place_level(proto_index + 1, pd);
 
    let { npossible, map } = possible_places(proto_index, pd);
 
    for (; npossible; --npossible) {
        lev.dlevel.dlevel = pick_level(map, rn2(npossible));
        if (place_level(proto_index + 1, pd)) return true;
        map[lev.dlevel.dlevel] = false;
    }
    return false;
}
 
// ============================================================
// init_level (dungeon.c:566)
// ============================================================
 
function init_level(dgn, proto_index, pd) {
    const tlevel = pd.tmplevel[proto_index];
 
    pd.final_lev[proto_index] = null;
    // C: if (!wizard && tlevel->chance <= rn2(100)) return;
    if (!game.flags.debug && tlevel.chance <= rn2(100)) return;
 
    pd.final_lev[proto_index] = {
        proto: tlevel.name,
        boneid: tlevel.bonetag || '\0',
        dlevel: { dnum: dgn, dlevel: 0 },
        flags: {
            town: !!(tlevel.flags & TOWN),
            hellish: !!(tlevel.flags & HELLISH),
            maze_like: !!(tlevel.flags & MAZELIKE),
            rogue_like: !!(tlevel.flags & ROGUELIKE),
            align: ((tlevel.flags & D_ALIGN_MASK) >> 4)
                || ((pd.tmpdungeon[dgn].flags & D_ALIGN_MASK) >> 4),
        },
        rndlevs: tlevel.nlevels || 0,
        next: null,
    };
}
 
// ============================================================
// Branch list management (dungeon.c:463, 514)
// ============================================================
 
function correct_branch_type(tbr) {
    switch (tbr.type) {
        case TBR_STAIR: return BR_STAIR;
        case TBR_NO_UP: return tbr.up ? BR_NO_END1 : BR_NO_END2;
        case TBR_NO_DOWN: return tbr.up ? BR_NO_END2 : BR_NO_END1;
        case TBR_PORTAL: return BR_PORTAL;
    }
    return BR_STAIR;
}
 
function find_branch(name, pd) {
    for (let i = 0; i < pd.n_brs; i++) {
        if (pd.tmpbranch[i].name === name) return i;
    }
    throw new Error(`find_branch: can't find ${name}`);
}
 
function parent_dnum(name, pd) {
    let i = find_branch(name, pd);
    for (let pdnum = 0; pd.tmpdungeon[pdnum].name !== name; pdnum++) {
        i -= pd.tmpdungeon[pdnum].branches_count;
        if (i < 0) return pdnum;
    }
    throw new Error("parent_dnum: couldn't resolve branch.");
}
 
function parent_dlevel(name, pd) {
    const i = find_branch(name, pd);
    const dnum = parent_dnum(name, pd);
    const { count: num, adjusted_base: base } = level_range(
        dnum,
        pd.tmpbranch[i].lev_base,
        pd.tmpbranch[i].lev_rand,
        pd.tmpbranch[i].chain,
        pd
    );
 
    // C: i = j = rn2(num); do { if(++i >= num) i = 0; check branches } while(...)
    // The rn2(num) call is the key RNG consumption here.
    // We simplify the loop since we don't have existing branches to conflict with
    // during initial dungeon creation (branches are inserted in order).
    const j = rn2(num);
 
    // Try to find a level without an existing branch
    let idx = j;
    let found = false;
    do {
        if (++idx >= num) idx = 0;
        // Check if any existing branch already uses this level
        let conflict = false;
        for (const br of game.branches) {
            if ((br.end1.dnum === dnum && br.end1.dlevel === base + idx)
                || (br.end2.dnum === dnum && br.end2.dlevel === base + idx)) {
                conflict = true;
                break;
            }
        }
        if (!conflict) { found = true; break; }
    } while (idx !== j);
 
    return base + idx;
}
 
function branch_val(bp) {
    return (((bp.end1.dnum * (MAXLEVEL + 1) + bp.end1.dlevel)
        * (MAXDUNGEON + 1) * (MAXLEVEL + 1))
        + (bp.end2.dnum * (MAXLEVEL + 1) + bp.end2.dlevel));
}
 
function insert_branch(new_branch) {
    new_branch.next = null;
    const branches = game.branches;
 
    // Find insertion point — sorted by branch_val
    const new_val = branch_val(new_branch);
    let insert_idx = branches.length; // default: append
    for (let i = 0; i < branches.length; i++) {
        const curr_val = branch_val(branches[i]);
        if (new_val <= curr_val) {
            insert_idx = i;
            break;
        }
    }
    branches.splice(insert_idx, 0, new_branch);
}
 
function add_branch(dgn, child_entry_level, pd) {
    const branch_num = find_branch(game.dungeons[dgn].dname, pd);
    const new_branch = {
        id: game.branch_id_counter++,
        type: correct_branch_type(pd.tmpbranch[branch_num]),
        end1: {
            dnum: parent_dnum(game.dungeons[dgn].dname, pd),
            dlevel: parent_dlevel(game.dungeons[dgn].dname, pd),
        },
        end2: { dnum: dgn, dlevel: child_entry_level },
        end1_up: pd.tmpbranch[branch_num].up ? true : false,
        next: null,
    };
    insert_branch(new_branch);
    return new_branch;
}
 
// ============================================================
// add_level — add s_level to special level chain (dungeon.c:544)
// ============================================================
 
function add_level(new_lev) {
    if (!game.sp_levchn) {
        game.sp_levchn = new_lev;
        new_lev.next = null;
        return;
    }
 
    let prev = null;
    let curr = game.sp_levchn;
    while (curr) {
        if (curr.dlevel.dnum === new_lev.dlevel.dnum
            && curr.dlevel.dlevel > new_lev.dlevel.dlevel)
            break;
        prev = curr;
        curr = curr.next;
    }
    if (!prev) {
        new_lev.next = game.sp_levchn;
        game.sp_levchn = new_lev;
    } else {
        new_lev.next = curr;
        prev.next = new_lev;
    }
}
 
// ============================================================
// init_dungeon_set_entry (dungeon.c:932)
// ============================================================
 
function init_dungeon_set_entry(pd, dngidx) {
    const dgn_entry = pd.tmpdungeon[dngidx].entry;
    if (dgn_entry < 0) {
        game.dungeons[dngidx].entry_lev =
            game.dungeons[dngidx].num_dunlevs + dgn_entry + 1;
        if (game.dungeons[dngidx].entry_lev <= 0)
            game.dungeons[dngidx].entry_lev = 1;
    } else if (dgn_entry > 0) {
        game.dungeons[dngidx].entry_lev = dgn_entry;
        if (game.dungeons[dngidx].entry_lev > game.dungeons[dngidx].num_dunlevs)
            game.dungeons[dngidx].entry_lev = game.dungeons[dngidx].num_dunlevs;
    } else {
        game.dungeons[dngidx].entry_lev = 1;
    }
}
 
// ============================================================
// init_dungeon_set_depth (dungeon.c:959)
// ============================================================
 
function init_dungeon_set_depth(pd, dngidx) {
    const br = add_branch(dngidx, game.dungeons[dngidx].entry_lev, pd);
 
    let from_depth, from_up;
    if (br.end1.dnum === dngidx) {
        from_depth = depth_of(br.end2);
        from_up = !br.end1_up;
    } else {
        from_depth = depth_of(br.end1);
        from_up = br.end1_up;
    }
 
    game.dungeons[dngidx].depth_start =
        from_depth + (br.type === BR_PORTAL ? 0 : (from_up ? -1 : 1))
        - (game.dungeons[dngidx].entry_lev - 1);
}
 
function depth_of(dlevel) {
    return game.dungeons[dlevel.dnum].depth_start + dlevel.dlevel - 1;
}
 
// ============================================================
// init_dungeon_dungeons (dungeon.c:997) — process one dungeon
// Returns true if dungeon was created, false if skipped by chance.
// ============================================================
 
function init_dungeon_dungeons(pd, dngidx, def) {
    const dgn_chance = def.chance;
 
    // C ref: dungeon.c:1022 — chance check
    // C: if (!wizard && dgn_chance && (dgn_chance <= rn2(100)))
    if (!game.flags.debug && dgn_chance && (dgn_chance <= rn2(100))) {
        game.n_dgns--;
        return false;
    }
 
    // Parse levels into pd.tmplevel (no RNG consumed here)
    const level_start = pd.n_levs;
    for (let f = 0; f < def.levels.length; f++) {
        const lvl = def.levels[f];
        const tmpl_idx = pd.n_levs + f;
        pd.tmplevel[tmpl_idx] = {
            name: lvl.name,
            bonetag: lvl.bonetag || '',
            lev_base: lvl.base,
            lev_rand: lvl.range || 0,
            chance: lvl.chance,
            nlevels: lvl.nlevels || 0,
            align: lvl.align || 0,
            flags: (lvl.flags || 0) | (lvl.align || 0),
            chain: -1,
        };
        // Resolve chain references
        if (lvl.chainlevel) {
            for (let bi = 0; bi < pd.n_levs + f; bi++) {
                if (pd.tmplevel[bi].name === lvl.chainlevel) {
                    pd.tmplevel[tmpl_idx].chain = bi;
                    break;
                }
            }
        } else if (lvl.chain !== undefined && lvl.chain >= 0) {
            pd.tmplevel[tmpl_idx].chain = lvl.chain;
        }
    }
    pd.n_levs += def.levels.length;
 
    // Parse branches into pd.tmpbranch (no RNG consumed here)
    for (let f = 0; f < def.branches.length; f++) {
        const br = def.branches[f];
        const tmpb_idx = pd.n_brs + f;
        pd.tmpbranch[tmpb_idx] = {
            name: br.name,
            lev_base: br.base,
            lev_rand: br.range || 0,
            type: br.type,
            up: br.up,
            chain: -1,
        };
        // Resolve chain references
        if (br.chainlevel) {
            for (let bi = 0; bi < pd.n_levs; bi++) {
                if (pd.tmplevel[bi].name === br.chainlevel) {
                    pd.tmpbranch[tmpb_idx].chain = bi;
                    break;
                }
            }
        } else if (br.chain !== undefined && br.chain >= 0) {
            pd.tmpbranch[tmpb_idx].chain = br.chain;
        }
    }
    pd.n_brs += def.branches.length;
 
    // Store dungeon metadata in pd
    pd.tmpdungeon[dngidx] = {
        name: def.name,
        protoname: def.protoname || '',
        bonetag: def.bonetag || '',
        lev_base: def.base,
        lev_rand: def.range || 0,
        flags: def.flags,
        align: def.align || 0,
        chance: dgn_chance,
        entry: def.entry || 0,
        levels_count: def.levels.length,
        branches_count: def.branches.length,
    };
 
    // Set dungeon name and metadata
    if (!game.dungeons[dngidx]) {
        game.dungeons[dngidx] = {};
    }
    const dgn = game.dungeons[dngidx];
    dgn.dname = def.name;
    dgn.proto = def.protoname || '';
    dgn.boneid = def.bonetag ? def.bonetag.charCodeAt(0) : 0;
    dgn.fill_lvl = def.fill || '';
    dgn.themerms = def.themerms || '';
 
    // C ref: dungeon.c:1073 — level count
    if (def.range) {
        dgn.num_dunlevs = rn1(def.range, def.base);
    } else {
        dgn.num_dunlevs = def.base;
    }
 
    // Ledger and depth
    if (!dngidx) {
        dgn.ledger_start = 0;
        dgn.depth_start = 1;
        dgn.dunlev_ureached = 1;
    } else {
        const prev = game.dungeons[dngidx - 1];
        dgn.ledger_start = prev.ledger_start + prev.num_dunlevs;
        dgn.dunlev_ureached = 0;
    }
 
    // Flags
    dgn.flags = {
        hellish: !!(def.flags & HELLISH),
        maze_like: !!(def.flags & MAZELIKE),
        rogue_like: !!(def.flags & ROGUELIKE),
        // C stores dungeon align in a 3-bit field; assigning D_ALIGN_* (0x10/0x20/0x40)
        // truncates to 0, and induced_align() falls through to rn2(3) unless on special level.
        align: (def.align || 0) & 0x7,
        unconnected: !!(def.flags & UNCONNECTED),
    };
 
    // Entry level
    init_dungeon_set_entry(pd, dngidx);
 
    // Depth
    if (dgn.flags.unconnected) {
        dgn.depth_start = 1;
    } else if (dngidx) {
        init_dungeon_set_depth(pd, dngidx);
    }
 
    if (dgn.num_dunlevs > MAXLEVEL) dgn.num_dunlevs = MAXLEVEL;
 
    return true;
}
 
// ============================================================
// fixup_level_locations (dungeon.c:1121) — set named level pointers
// ============================================================
 
function fixup_level_locations() {
    // Walk the special level chain and set up named level references.
    // In C this sets global level pointers; in JS we store them on game.
    const level_map_names = [
        'air', 'asmodeus', 'astral', 'baalz', 'bigrm', 'castle',
        'earth', 'fakewiz1', 'fire', 'juiblex', 'knox', 'medusa',
        'oracle', 'orcus', 'rogue', 'sanctum', 'valley', 'water',
        'wizard1', 'wizard2', 'wizard3', 'minend', 'soko1',
        'x-strt', 'x-loca', 'x-goal',
    ];
 
    const level_map_keys = {
        'air': 'air_level', 'asmodeus': 'asmodeus_level',
        'astral': 'astral_level', 'baalz': 'baalzebub_level',
        'bigrm': 'bigroom_level', 'castle': 'stronghold_level',
        'earth': 'earth_level', 'fakewiz1': 'portal_level',
        'fire': 'fire_level', 'juiblex': 'juiblex_level',
        'knox': 'knox_level', 'medusa': 'medusa_level',
        'oracle': 'oracle_level', 'orcus': 'orcus_level',
        'rogue': 'rogue_level', 'sanctum': 'sanctum_level',
        'valley': 'valley_level', 'water': 'water_level',
        'wizard1': 'wiz1_level', 'wizard2': 'wiz2_level',
        'wizard3': 'wiz3_level', 'minend': 'mineend_level',
        'soko1': 'sokoend_level',
        'x-strt': 'qstart_level', 'x-loca': 'qlocate_level',
        'x-goal': 'nemesis_level',
    };
 
    for (const name of level_map_names) {
        const key = level_map_keys[name];
        const x = find_level(name);
        if (x) {
            game[key] = { dnum: x.dlevel.dnum, dlevel: x.dlevel.dlevel };
            if (name.startsWith('x-')) {
                const filecode = game.urole?.filecode;
                if (filecode) x.proto = `${filecode}${name.slice(1)}`;
            }
            // C ref: dungeon.c:1142-1157 — Knox (Fort Ludios) branch kludge.
            // Mark the parent endpoint as floating (dnum = n_dgns) so it does
            // not appear as a regular parent branch in print_dungeon(), then
            // re-sort the branch list by branch_val ordering.
            if (key === 'knox_level') {
                const branches = game.branches || [];
                const br = branches.find((b) => b?.end2
                    && b.end2.dnum === x.dlevel.dnum
                    && b.end2.dlevel === x.dlevel.dlevel);
                if (br) {
                    br.end1.dnum = game.n_dgns;
                    branches.sort((a, b) => branch_val(a) - branch_val(b));
                }
            }
        }
    }
 
    // Named dungeon numbers
    function dname_to_dnum(dname) {
        for (let i = 0; i < game.n_dgns; i++) {
            if (game.dungeons[i] && game.dungeons[i].dname === dname) return i;
        }
        return -1;
    }
 
    game.quest_dnum = dname_to_dnum('The Quest');
    game.sokoban_dnum = dname_to_dnum('Sokoban');
    game.mines_dnum = dname_to_dnum('The Gnomish Mines');
    game.tower_dnum = dname_to_dnum("Vlad's Tower");
    game.tutorial_dnum = dname_to_dnum('The Tutorial');
 
    // Dummy level fixup for Elemental Planes depth
    const dummy = find_level('dummy');
    if (dummy) {
        const i = dummy.dlevel.dnum;
        const dunlevs = game.dungeons[i].num_dunlevs;
        if (dunlevs > 1 - game.dungeons[i].depth_start) {
            game.dungeons[i].depth_start -= 1;
        }
    }
}
 
export function find_level(name) {
    let lev = game.sp_levchn;
    while (lev) {
        if (lev.proto === name) return lev;
        lev = lev.next;
    }
    return null;
}
 
function br_string(type) {
    switch (type) {
    case BR_PORTAL: return 'Portal';
    case BR_NO_END1: return 'Connection';
    case BR_NO_END2: return 'One way stair';
    case BR_STAIR: return 'Stair';
    default: return ' (unknown)';
    }
}
 
function chr_u_on_lvl(dlev) {
    return on_level(game.u?.uz, dlev) ? '*' : ' ';
}
 
// in_endgame: imported from macros.js as In_endgame
 
function unreachable_level(lvl, unplaced = false) {
    if (unplaced) return true;
    if (in_endgame(game.u?.uz) && !in_endgame(lvl)) return true;
    const dummy = find_level('dummy');
    if (dummy && on_level(lvl, dummy.dlevel)) return true;
    return false;
}
 
 
function tport_menu(menu, entry, lchoices, lvl, cannotreach) {
    const idx = lchoices.idx;
    lchoices.lev[idx] = lvl.dlevel;
    lchoices.dgn[idx] = lvl.dnum;
    lchoices.playerlev[idx] = depth_of(lvl);
 
    const accelerator = lchoices.menuletter;
    if (cannotreach) {
        add_menu(menu, null, 0, 0, 0, NO_COLOR, `    ${entry}`, 0);
    } else {
        add_menu(menu, idx + 1, accelerator, 0, 0, NO_COLOR, entry, 0);
    }
 
    lchoices.menuletter = (lchoices.menuletter === 'z')
        ? 'A'
        : String.fromCharCode(lchoices.menuletter.charCodeAt(0) + 1);
    lchoices.idx++;
}
 
function print_branch(menu, dnum, lowerBound, upperBound, bymenu, lchoices) {
    for (const br of game.branches || []) {
        if (br.end1.dnum === dnum
            && lowerBound < br.end1.dlevel
            && br.end1.dlevel <= upperBound) {
            const entry = `${bymenu ? chr_u_on_lvl(br.end1) : ' '} ${br_string(br.type)} to ${game.dungeons[br.end2.dnum].dname}: ${depth_of(br.end1)}`;
            if (bymenu) {
                tport_menu(menu, entry, lchoices, br.end1, unreachable_level(br.end1, false));
            }
        }
    }
}
 
// C ref: dungeon.c:2284 print_dungeon()
export async function print_dungeon(bymenu, rlev = null, rdgn = null) {
    if (!bymenu) return 0;
 
    const menu = create_nhwindow_menu();
    start_menu(menu);
    const lchoices = { idx: 0, menuletter: 'a', lev: [], dgn: [], playerlev: [] };
 
    for (let i = 0; i < game.n_dgns; i++) {
        const dptr = game.dungeons[i];
        if (!dptr) continue;
        if (in_endgame(game.u?.uz) && i !== game.astral_level?.dnum) continue;
 
        const unplaced = dptr.dname === 'Fort Ludios';
        const descr = unplaced ? 'depth' : 'level';
        const nlev = dptr.num_dunlevs;
        let buf;
        if (nlev > 1) {
            buf = `${dptr.dname}: ${descr}s ${dptr.depth_start} to ${dptr.depth_start + nlev - 1}`;
        } else {
            buf = `${dptr.dname}: ${descr} ${dptr.depth_start}`;
        }
        if (dptr.entry_lev !== 1) {
            if (dptr.entry_lev === nlev) {
                buf += ', entrance from below';
            } else {
                buf += `, entrance on ${dptr.depth_start + dptr.entry_lev - 1}`;
            }
        }
        add_menu_heading(menu, buf);
 
        let lastLevel = 0;
        for (let slev = game.sp_levchn; slev; slev = slev.next) {
            if (slev.dlevel.dnum !== i) continue;
            print_branch(menu, i, lastLevel, slev.dlevel.dlevel, true, lchoices);
            const lev = depth_of(slev.dlevel);
            let entry = `${chr_u_on_lvl(slev.dlevel)} ${slev.proto}: ${lev}`;
            if (game.stronghold_level && on_level(slev.dlevel, game.stronghold_level) && game.castle_tune) {
                entry += ` (tune ${game.castle_tune})`;
            }
            tport_menu(menu, entry, lchoices, slev.dlevel, unreachable_level(slev.dlevel, unplaced));
            lastLevel = slev.dlevel.dlevel;
        }
        print_branch(menu, i, lastLevel, MAXLEVEL, true, lchoices);
    }
 
    end_menu(menu, 'Level teleport to where:');
    const result = await select_menu(menu, 1);
    destroy_nhwindow_menu(menu);
    if (result.count > 0) {
        const idx = result.items[0].identifier - 1;
        if (rlev) rlev.value = lchoices.lev[idx];
        if (rdgn) rdgn.value = lchoices.dgn[idx];
        return lchoices.playerlev[idx];
    }
    return 0;
}
 
// ============================================================
// init_dungeons (dungeon.c:1205) — main entry point
// ============================================================
 
export async function init_dungeons() {
    const g = game;
 
    // C: Lua state init runs nhlib.lua which shuffles align[] — 2 core RNG calls
    nhlib_init_shuffle();
 
    // Initialize game state
    g.dungeons = [];
    g.branches = [];
    g.sp_levchn = null;
    g.n_dgns = DUNGEON_DEFS.length;
    game.branch_id_counter = 0;
 
    const pd = make_proto_dungeon();
    let cl = 0; // cumulative level index
    let i = 0;  // dungeon index (incremented only for created dungeons)
 
    for (let defIdx = 0; defIdx < DUNGEON_DEFS.length; defIdx++) {
        const def = DUNGEON_DEFS[defIdx];
 
        if (init_dungeon_dungeons(pd, i, def)) {
            // C: for (; cl < pd.n_levs; cl++) init_level(i, cl, &pd);
            for (; cl < pd.n_levs; cl++) {
                init_level(i, cl, pd);
            }
 
            // C: place_level(pd.start, &pd)
            if (!place_level(pd.start, pd)) {
                throw new Error("init_dungeons: couldn't place levels");
            }
 
            // C: add placed levels to sp_levchn
            for (; pd.start < pd.n_levs; pd.start++) {
                if (pd.final_lev[pd.start]) {
                    add_level(pd.final_lev[pd.start]);
                }
            }
 
            i++;
        }
    }
 
    // C ref: dungeon.c:1312
    init_castle_tune();
 
    // C ref: dungeon.c:1313
    fixup_level_locations();
}
export function setMtInitialized() {}
 
// C ref: dungeon.c:1376 ledger_no()
// Returns the bookkeeping ledger number for a dungeon level.
export function ledger_no(lev) {
    if (!lev) return 0;
    const start = game.dungeons?.[lev.dnum]?.ledger_start ?? 0;
    return lev.dlevel + start;
}
 
// C ref: dungeon.c:1370 dunlev()
// Returns the dungeon-relative level number.
export function dunlev(lev) {
    return lev?.dlevel ?? 1;
}
 
// C ref: dungeon.c:1390 dunlevs_in_dungeon()
export function dunlevs_in_dungeon(lev) {
    return game.dungeons?.[lev?.dnum]?.num_dunlevs ?? 1;
}
 
// C ref: global d_level records populated during dungeon init.
export function valley_level() {
    return game.valley_level || { dnum: 0, dlevel: 0 };
}
 
export function qstart_level() {
    return game.qstart_level || { dnum: -1, dlevel: 0 };
}
 
// C ref: dungeon.c:1405 assign_level()
export function assign_level(dst, src) {
    dst.dnum = src.dnum;
    dst.dlevel = src.dlevel;
}
 
// C ref: dungeon.c:1993 induced_align — pick altar alignment
// Returns an alignment mask (AM_LAWFUL, AM_NEUTRAL, AM_CHAOTIC, etc.)
export function induced_align(pct) {
    const g = game;
    const uz = g.u?.uz;
    // C: check special level alignment first
    let slev = g.sp_levchn;
    while (slev) {
        const d = slev.dlevel;
        if (d && d.dnum === uz?.dnum && d.dlevel === uz?.dlevel) {
            if (slev.flags?.align && rn2(100) < pct)
                return slev.flags.align;
            break;
        }
        slev = slev.next;
    }
    // C: check dungeon alignment
    const dng = g.dungeons?.[uz?.dnum ?? 0];
    if (dng?.flags?.align) {
        if (rn2(100) < pct) return dng.flags.align;
    }
    // random alignment
    const al = rn2(3) - 1; // -1=chaotic, 0=neutral, 1=lawful
    return Align2amask(al);
}
 
// C ref: dungeon.c — get_level(flev, nlev) helper
export function get_level(flev, nlev) {
    if (!flev) return;
 
    let levnum = nlev;
    let dgn = game.u?.uz?.dnum ?? 0;
    const dungeons = game.dungeons || [];
 
    if (levnum <= 0) {
        // C: can currently happen only in endgame; keep current relative level.
        levnum = game.u?.uz?.dlevel ?? 1;
    } else {
        const cur = dungeons[dgn] || {};
        const curStart = cur.depth_start ?? 1;
        const curNum = cur.num_dunlevs ?? 1;
        if (levnum > (curStart + curNum - 1)) {
            // C: beyond end of current dungeon => jump to last level in it.
            levnum = curNum;
            flev.dnum = dgn;
            flev.dlevel = levnum;
            return;
        }
 
        // If target absolute depth is above current dungeon start,
        // walk up parent branches until containing dungeon is found.
        if (levnum < curStart) {
            let safetyCount = 0;
            while (levnum < (dungeons[dgn]?.depth_start ?? 1)) {
                const parentBr = (game.branches || []).find(br => br?.end2?.dnum === dgn);
                if (!parentBr) {
                    // Can't find parent — clamp to current dungeon
                    levnum = 1;
                    break;
                }
                const newDgn = parentBr.end1.dnum;
                if (newDgn === dgn || ++safetyCount > 20) {
                    // Self-referencing or too deep — clamp
                    levnum = dungeons[dgn]?.depth_start ?? 1;
                    break;
                }
                dgn = newDgn;
            }
        }
 
        // Convert absolute depth to dungeon-relative dlevel.
        levnum = levnum - (dungeons[dgn]?.depth_start ?? 1) + 1;
    }
 
    flev.dnum = dgn;
    flev.dlevel = levnum;
}
 
export function init_dungeon_globals() {
    game.branch_id_counter = 0;
}