All files / js hacklib.js

64.63% Statements 636/984
76.83% Branches 136/177
44.44% Functions 36/81
64.63% Lines 636/984

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 98573x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x     73x 73x 73x 73x     73x 73x 73x     73x 73x 73x 2261x 2261x 2261x 2261x 73x 73x 73x         73x 73x 73x 73x 73x 73x 73x 73x 73x     73x 73x 73x     73x 73x 73x 55x 55x 55x 73x 73x 73x                                   73x 73x 73x 73x 73x 73x 73x 73x 73x 9006x 9006x 9006x 403954x 403954x 403954x 403954x 403954x 403954x 9006x 9006x 9006x 9006x 73x 73x 73x     73x 73x 73x     73x 73x 73x 73x 73x 73x 73x 73x 73x 73x     73x 73x 73x     73x 73x 73x 73x             73x 73x 73x 73x 73x 73x 73x 73x 690x 690x   690x 73x 73x 73x     73x 73x 73x               73x 73x 73x 73x 73x 73x 73x 73x 73x     73x 73x 73x 73x             73x 73x 73x               73x 73x 73x 73x                     73x 73x 73x 73x 73x 73x 73x 73x 19x 19x 19x 19x 15x 19x 73x 73x 73x 17x 17x 17x 17x 17x 17x   17x   17x     17x 17x 17x   17x 17x 17x 17x 15x 15x 17x     2x       17x 17x 17x 73x 73x 73x 73x 73x 73x 73x 73x           73x 73x 73x 73x                           73x 73x 73x 73x                                 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x             73x 73x 73x     73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 2x 2x 2x 2x 73x 73x 73x 73x 73x                                                     73x 73x 73x 73x 73x                                       73x 73x 73x 73x 73x 73x 73x 73x 73x 564163x 634708x 634708x 632998x 632998x 634708x 634708x 634708x 2130x 564163x 73x 73x 73x 4021x 4021x 73x 73x 73x 73x 73x 7x 7x 7x 7x 73x 73x 73x 3564x 3564x 4030x 4030x 4030x 4030x 4030x 4030x 4030x 4030x 4030x 3564x 73x 73x 73x 73x 73x 73x 73x 73x 73x           73x 73x 73x     73x 73x 73x 62573x 62573x 73x 73x 73x 73x 73x 73x 73x 73x 73x 1228559x 1228559x 1228559x 1228559x 1228559x 73x 73x 73x 73x 551912x 551912x 551912x 73x 73x 73x 73x 682x 682x 73x 73x 73x 73x 19x 19x 1764x 1764x 1764x 1764x 19x 19x 73x 73x 73x 73x 62891x 62891x 62891x 73x 73x 73x 73x 73x 73x 73x 73x 73x       73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x                 73x 73x 73x 73x 73x 73x 73x 73x 73x 59714x 59714x 59714x 3775049x 3775049x 3775049x 3775049x 3775049x 59714x 59714x 73x 73x 73x 73x 59714x 59714x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 3431x 3431x 3431x 3431x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 56283x 56283x 56283x 27302x 27302x 56283x 28981x 28981x 28981x 56283x 56283x 73x 73x 73x 73x 73x 73x                         73x 73x 73x 73x                           73x 73x 73x 73x 73x 73x 73x 73x                       73x 73x 73x       73x 73x 73x                   73x                         73x                 73x                         73x 73x 73x 73x                                                   73x 73x 73x 73x                                                             73x 73x 73x             73x 73x 73x 73x 73x 73x 73x 73x 73x 79x 79x 79x 79x 79x 79x 79x 79x 79x 79x 79x 78x 79x 79x 76x 79x   79x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 24484x 24484x 24484x 24484x 24484x 24479x 24484x 73x 73x 73x 73x 81x 81x 81x 81x 81x 81x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 3x 3x 10x 10x 10x 3x 3x 73x 73x 73x 73x 73x 424x             424x   424x 424x 73x 73x 73x         73x 73x 73x 73x 73x 73x 73x 73x 15x 15x 73x 73x 73x 202105x 202105x 202105x 202105x 51302x 51302x 51302x 185935x 51302x 51302x 202105x 35x 35x 35x 735x 58065x 58065x 58065x 58065x 58065x 58065x 735x 35x 35x 35x 35x 35x 51302x 202105x  
import { game } from './gstate.js';
// hacklib.js — String and character utility functions
// Faithful port of hacklib.c from NetHack 3.7.
//
// Note on JS semantics: C functions that modify strings in-place (lcase, ucase,
// upstart, upwords, mungspaces, trimspaces, strip_newline, strkitten, copynchars,
// strcasecpy, tabexpand) return new strings in JS because JS strings are immutable.
 
import { pushAnimFrameEvent, pushRngLogEntry, rn2 } from './rng.js';
import {
    ALTAR,
    COLNO,
    CORR,
    DRAWBRIDGE_DOWN,
    FOUNTAIN,
    GRAVE,
    ICE,
    IS_DOOR,
    IS_FOUNTAIN,
    IS_LAVA,
    IS_POOL,
    IS_ROOM,
    IS_SINK,
    IS_WALL,
    ROOM,
    ROWNO,
    LADDER,
    STAIRS,
    SDOOR,
} from './const.js';
 
// ============================================================================
// Character predicates and case conversion
// C ref: hacklib.c:125-150
// ============================================================================
 
// C: Strcat(dst, src) — string concatenation
export function Strcat(dst, src) {
    return (dst || '') + (src || '');
}
 
// hacklib.c:125 — is 'c' a digit?
// Autotranslated from hacklib.c:125
export function digit(c) {
  return ('0' <= c && c <= '9');
}
 
// hacklib.c:132 — is 'c' a letter? note: '@' classed as letter
export function letter(c) {
    return ('@' <= c && c <= 'Z') || ('a' <= c && c <= 'z');
}
 
// hacklib.c:139 — force 'c' into uppercase
export function highc(c) {
    return (c >= 'a' && c <= 'z')
        ? String.fromCharCode(c.charCodeAt(0) & ~0x20)
        : c;
}
 
// hacklib.c:146 — force 'c' into lowercase
export function lowc(c) {
    return (c >= 'A' && c <= 'Z')
        ? String.fromCharCode(c.charCodeAt(0) | 0x20)
        : c;
}
 
// ============================================================================
// String case conversion
// C ref: hacklib.c:153-203
// Note: JS versions return new strings (C modifies in-place).
// ============================================================================
 
// hacklib.c:153 — convert a string into all lowercase
export function lcase(s) {
    return s.toLowerCase();
}
 
// hacklib.c:166 — convert a string into all uppercase
export function ucase(s) {
    return s.toUpperCase();
}
 
// hacklib.c:177 — convert first character of a string to uppercase
export function upstart(s) {
    if (!s) return s;
    return highc(s[0]) + s.slice(1);
}
 
// hacklib.c:186 — capitalize first letter of every word in a string
export function upwords(s) {
    let result = '';
    let space = true;
    for (let i = 0; i < s.length; i++) {
        const c = s[i];
        if (c === ' ') {
            space = true;
            result += c;
        } else if (space && letter(c)) {
            result += highc(c);
            space = false;
        } else {
            result += c;
            space = false;
        }
    }
    return result;
}
 
// ============================================================================
// String whitespace and newline handling
// C ref: hacklib.c:205-255
// Note: JS versions return new strings (C modifies in-place).
// ============================================================================
 
// hacklib.c:205 — remove excess whitespace (collapse runs, trim ends, stop at \n)
export function mungspaces(bp) {
    let result = '';
    let was_space = true;
    for (let i = 0; i < bp.length; i++) {
        let c = bp[i];
        if (c === '\n') break;
        if (c === '\t') c = ' ';
        if (c !== ' ' || !was_space) result += c;
        was_space = (c === ' ');
    }
    if (was_space && result.length > 0)
        result = result.slice(0, -1);
    return result;
}
 
// hacklib.c:227 — skip leading whitespace; remove trailing whitespace
export function trimspaces(txt) {
    return txt.replace(/^[ \t]+/, '').replace(/[ \t]+$/, '');
}
 
// hacklib.c:243 — remove \n from end of line (and \r if present)
export function strip_newline(str) {
    return str.replace(/\r?\n$/, '');
}
 
// ============================================================================
// String end/length utilities
// C ref: hacklib.c:257-274
// Note: C eos() returns a char* pointer to '\0'. JS returns the string length
// (the index where '\0' would be), which is the natural JS equivalent.
// ============================================================================
 
// hacklib.c:257 — return the index of the end of a string (= length)
export function eos(s) {
    return s.length;
}
 
// hacklib.c:266 — const version of eos()
export function c_eos(s) {
    return s.length;
}
 
// C stdlib: strchr(s, ch) — find first occurrence of ch in s
// Returns the substring starting at ch, or null if not found.
export function strchr(s, ch) {
    if (s == null || ch == null) return null;
    const text = String(s);
    const needle = typeof ch === 'number' ? String.fromCharCode(ch) : String(ch)[0] || '';
    const idx = text.indexOf(needle);
    return idx >= 0 ? text.slice(idx) : null;
}
 
// ============================================================================
// String comparison utilities
// C ref: hacklib.c:277-337
// ============================================================================
 
// hacklib.c:277 — determine whether 'str' starts with 'chkstr', optionally case-blind
export function str_start_is(str, chkstr, caseblind) {
    if (caseblind)
        return str.toLowerCase().startsWith(chkstr.toLowerCase());
    return str.startsWith(chkstr);
}
 
// hacklib.c:305 — determine whether 'str' ends with 'chkstr'
export function str_end_is(str, chkstr) {
    return str.endsWith(chkstr);
}
 
// hacklib.c:316 — return max line length from newline-separated string
export function str_lines_maxlen(str) {
    let max_len = 0;
    const lines = str.split('\n');
    for (const line of lines) {
        if (line.length > max_len) max_len = line.length;
    }
    return max_len;
}
 
// ============================================================================
// String building utilities
// C ref: hacklib.c:340-408
// Note: strkitten/copynchars/strcasecpy return new strings in JS.
// ============================================================================
 
// hacklib.c:340 — append a character to a string: strcat(s, {c,'\0'})
export function strkitten(s, c) {
    return s + c;
}
 
// hacklib.c:350 — truncating string copy (stops at n chars or '\n')
// Returns the copied string (JS: no separate dst buffer needed).
export function copynchars(src, n) {
    let result = '';
    for (let i = 0; i < n && i < src.length && src[i] !== '\n'; i++) {
        result += src[i];
    }
    return result;
}
 
// hacklib.c:365 — convert char nc into oc's case
export function chrcasecpy(oc, nc) {
    if ('a' <= oc && oc <= 'z') {
        if ('A' <= nc && nc <= 'Z') nc = String.fromCharCode(nc.charCodeAt(0) + ('a'.charCodeAt(0) - 'A'.charCodeAt(0)));
    } else if ('A' <= oc && oc <= 'Z') {
        if ('a' <= nc && nc <= 'z') nc = String.fromCharCode(nc.charCodeAt(0) + ('A'.charCodeAt(0) - 'a'.charCodeAt(0)));
    }
    return nc;
}
 
// hacklib.c:387 — overwrite string, preserving old chars' case
// In JS: applies old string's case pattern to new string src.
export function strcasecpy(dst, src) {
    let result = '';
    let dst_exhausted = false;
    let dstIdx = 0;
    for (let i = 0; i < src.length; i++) {
        if (!dst_exhausted && dstIdx >= dst.length) dst_exhausted = true;
        const oc = dst_exhausted ? dst[dst.length - 1] : dst[dstIdx++];
        result += chrcasecpy(oc || '', src[i]);
    }
    return result;
}
 
// ============================================================================
// English suffix helpers (used by message formatting)
// C ref: hacklib.c:409-494
// ============================================================================
 
// hacklib.c:409 — return a name converted to possessive
export function s_suffix(s) {
    const lower = s.toLowerCase();
    if (lower === 'it') return s + 's';       // it -> its
    if (lower === 'you') return s + 'r';      // you -> your
    if (s[s.length - 1] === 's') return s + "'";  // Xs -> Xs'
    return s + "'s";                           // X -> X's
}
 
// hacklib.c:427 — construct a gerund (verb + "ing")
export function ing_suffix(s) {
    const vowel = 'aeiouwy';
    let buf = s;
    let onoff = '';
 
    // Extract trailing " on", " off", " with"
    if (buf.length >= 3 && buf.slice(-3).toLowerCase() === ' on') {
        onoff = ' on'; buf = buf.slice(0, -3);
    } else if (buf.length >= 4 && buf.slice(-4).toLowerCase() === ' off') {
        onoff = ' off'; buf = buf.slice(0, -4);
    } else if (buf.length >= 5 && buf.slice(-5).toLowerCase() === ' with') {
        onoff = ' with'; buf = buf.slice(0, -5);
    }
 
    const p = buf.length;
    if (p >= 2 && buf.slice(-2).toLowerCase() === 'er') {
        // slither + ing — nothing
    } else if (p >= 3
        && !vowel.includes(buf[p - 1].toLowerCase())
        && vowel.includes(buf[p - 2].toLowerCase())
        && !vowel.includes(buf[p - 3].toLowerCase())) {
        // tip -> tipp + ing
        buf = buf + buf[p - 1];
    } else if (p >= 2 && buf.slice(-2).toLowerCase() === 'ie') {
        // vie -> vy + ing
        buf = buf.slice(0, -2) + 'y';
    } else if (p >= 1 && buf[p - 1] === 'e') {
        // grease -> greas + ing
        buf = buf.slice(0, -1);
    }
 
    return buf + 'ing' + onoff;
}
 
// ============================================================================
// Miscellaneous utilities
// C ref: hacklib.c:482-575
// ============================================================================
 
// hacklib.c:483 — is a string entirely whitespace?
export function onlyspace(s) {
    for (let i = 0; i < s.length; i++) {
        if (s[i] !== ' ' && s[i] !== '\t') return false;
    }
    return true;
}
 
// hacklib.c:493 — expand tabs into proper number of spaces (8-column tabs)
// JS returns a new string (C modifies in-place).
export function tabexpand(sbuf) {
    let result = '';
    let idx = 0;
    for (let i = 0; i < sbuf.length; i++) {
        if (sbuf[i] === '\t') {
            do { result += ' '; } while (++idx % 8);
        } else {
            result += sbuf[i];
            idx++;
        }
        if (idx >= 512) break; // BUFSZ safety limit
    }
    return result;
}
 
// hacklib.c:533 — make a displayable string from a character
// In C this returns one of 5 rotating static buffers; in JS just returns a string.
export function visctrl(c) {
    const code = typeof c === 'string' ? c.charCodeAt(0) : c;
    let result = '';
    let ch = code;
    if (ch & 0x80) {
        result += 'M-';
        ch &= 0x7f;
    }
    if (ch < 0x20) {
        result += '^' + String.fromCharCode(ch | 0x40); // letter
    } else if (ch === 0x7f) {
        result += '^?';
    } else {
        result += String.fromCharCode(ch);
    }
    return result;
}
 
// ============================================================================
// String strip utilities
// C ref: hacklib.c:560-595
// Note: C functions modify strings in-place; JS versions return new strings.
// ============================================================================
 
// hacklib.c:560 — strip all chars in stuff_to_strip from orig
// C signature: stripchars(char *bp, const char *stuff_to_strip, const char *orig)
// JS: bp output buffer dropped; takes (orig, stuff_to_strip), returns new string.
export function stripchars(orig, stuff_to_strip) {
    let result = '';
    for (let i = 0; i < orig.length; i++) {
        if (!stuff_to_strip.includes(orig[i])) result += orig[i];
    }
    return result;
}
 
// hacklib.c:584 — remove digits from string
export function stripdigits(s) {
    return s.replace(/[0-9]/g, '');
}
 
// ============================================================================
// String substitution utilities
// C ref: hacklib.c:599-684
// Note: C functions modify strings in-place; JS versions return new strings.
// Note: strNsubst C return value is substitution count; JS returns new string.
// ============================================================================
 
// hacklib.c:599 — substitute first occurrence of orig with replacement in bp
export function strsubst(bp, orig, replacement) {
    const idx = bp.indexOf(orig);
    if (idx < 0) return bp;
    return bp.slice(0, idx) + replacement + bp.slice(idx + orig.length);
}
 
// hacklib.c:619 — substitute Nth occurrence of orig with replacement (n=0: all)
// C: modifies inoutbuf in place, returns substitution count.
// JS: returns the modified string.
export function strNsubst(inoutbuf, orig, replacement, n) {
    const len = orig.length;
    if (len === 0) {
        // Special case: empty orig — insert replacement before Nth char (n>0)
        // or before every char (n=0), or append if n==strlen+1.
        let result = '';
        let ocount = 0;
        for (let i = 0; i < inoutbuf.length; i++) {
            if (++ocount === n || n === 0) result += replacement;
            result += inoutbuf[i];
        }
        if (inoutbuf.length + 1 === n) result += replacement;
        return result;
    }
    let result = '';
    let ocount = 0;
    let bp = 0;
    while (bp < inoutbuf.length) {
        if (inoutbuf.startsWith(orig, bp) && (++ocount === n || n === 0)) {
            result += replacement;
            bp += len;
        } else {
            result += inoutbuf[bp++];
        }
    }
    return result;
}
 
// hacklib.c:663 — search for word in space-separated list
// C: returns pointer into list at start of found word, or NULL.
// JS: returns the slice of list starting at the found word, or null.
export function findword(list, word, wordlen, ignorecase) {
    const w = word.slice(0, wordlen);
    let p = 0;
    while (p < list.length) {
        while (p < list.length && list[p] === ' ') p++;
        if (p >= list.length) break;
        const candidate = list.slice(p, p + wordlen);
        const afterWord = p + wordlen;
        const atWordEnd = afterWord >= list.length || list[afterWord] === ' ';
        const matches = ignorecase
            ? candidate.toLowerCase() === w.toLowerCase()
            : candidate === w;
        if (matches && atWordEnd) return list.slice(p);
        // advance to next space (C: strchr(p + 1, ' '))
        const spaceIdx = list.indexOf(' ', p + 1);
        if (spaceIdx < 0) break;
        p = spaceIdx;
    }
    return null;
}
 
// ============================================================================
// Case-insensitive string comparison
// C ref: hacklib.c:781-843
// ============================================================================
 
// hacklib.c:781 — case-insensitive counted string comparison
// Returns negative if s1 < s2, 0 if equal, positive if s1 > s2.
export function strncmpi(s1, s2, n) {
    for (let i = 0; i < n; i++) {
        if (i >= s2.length) return s1.length > i ? 1 : 0;
        if (i >= s1.length) return -1;
        const t1 = s1[i].toLowerCase();
        const t2 = s2[i].toLowerCase();
        if (t1 < t2) return -1;
        if (t1 > t2) return 1;
    }
    return 0;
}
 
// C strcmpi — case-insensitive full string comparison, returns 0 for match
export function strcmpi(s1, s2) {
    return strncmpi(s1, s2, Math.max(s1.length, s2.length));
}
 
// hacklib.c:803 — case-insensitive substring search
// C: returns pointer to match in str, or NULL.
// JS: returns the slice of str starting at the match, or null.
export function strstri(str, sub) {
    if (sub === '') return str;
    const idx = str.toLowerCase().indexOf(sub.toLowerCase());
    return idx < 0 ? null : str.slice(idx);
}
 
// hacklib.c:848 — compare two strings ignoring specified chars, optionally case-blind
export function fuzzymatch(s1, s2, ignore_chars, caseblind) {
    let i1 = 0, i2 = 0;
    for (;;) {
        while (i1 < s1.length && ignore_chars.includes(s1[i1])) i1++;
        while (i2 < s2.length && ignore_chars.includes(s2[i2])) i2++;
        const c1 = i1 < s1.length ? s1[i1++] : null;
        const c2 = i2 < s2.length ? s2[i2++] : null;
        if (!c1 || !c2) return c1 === c2;
        const cmp1 = caseblind ? c1.toLowerCase() : c1;
        const cmp2 = caseblind ? c2.toLowerCase() : c2;
        if (cmp1 !== cmp2) return false;
    }
}
 
// ============================================================================
// Number formatting utilities
// C ref: hacklib.c:689-717
// ============================================================================
 
// hacklib.c:689 — return the ordinal suffix of a number (1st, 2nd, 3rd, 4th...)
// Autotranslated from hacklib.c:688
export function ordin(n) {
  let dd = n % 10;
  return (dd === 0 || dd > 3 || Math.trunc((n % 100) / 10) === 1)
    ? "th"
    : (dd === 1) ? "st" : (dd === 2) ? "nd" : "rd";
}
 
// hacklib.c:701 — make a signed digit string from a number ("+3" or "-2")
export function sitoa(n) {
    return n < 0 ? String(n) : '+' + String(n);
}
 
// hacklib.c:714 — return the sign of a number: -1, 0, or 1
export function sgn(n) {
    return n < 0 ? -1 : (n !== 0 ? 1 : 0);
}
 
// ============================================================================
// Geometry utilities
// C ref: hacklib.c:720-774
// ============================================================================
 
// hacklib.c:720 — distance between two points in moves (Chebyshev distance)
// Autotranslated from hacklib.c:720
export function distmin(x0, y0, x1, y1) {
  let dx = x0 - x1, dy = y0 - y1;
  if (dx < 0) dx = -dx;
  if (dy < 0) dy = -dy;
  return (dx < dy) ? dy : dx;
}
 
// hacklib.c:737 — square of Euclidean distance between pair of points
// Autotranslated from hacklib.c:736
export function dist2(x0, y0, x1, y1) {
  let dx = x0 - x1, dy = y0 - y1;
  return dx * dx + dy * dy;
}
 
// C ref: hack.h distu(x,y) — squared distance from hero to (x,y)
// C macro: #define distu(x,y) dist2((int)u.ux,(int)u.uy,x,y)
export function distu(x, y) {
  return dist2(game.u.ux, game.u.uy, x, y);
}
 
// hacklib.c:746 — integer square root (floor(sqrt(val))); not in C comment block
// Autotranslated from hacklib.c:745
export function isqrt(val) {
  let rt = 0, odd = 1;
  while (val >= odd) {
    val = val - odd;
    odd = odd + 2;
    rt = rt + 1;
  }
  return rt;
}
 
// hacklib.c:768 — are two points lined up (orthogonal or diagonal)?
// Autotranslated from hacklib.c:767
export function online2(x0, y0, x1, y1) {
  let dx = x0 - x1, dy = y0 - y1;
  return (!dy || !dx || dy === dx || dy === -dx);
}
 
// ============================================================================
// Bit manipulation
// C ref: hacklib.c:894-900
// ============================================================================
 
// hacklib.c:894 — swap bit at position bita with bit at position bitb in val
// Autotranslated from hacklib.c:895
export function swapbits(val, bita, bitb) {
  let tmp = ((val >> bita) & 1) ^ ((val >> bitb) & 1);
  return (val ^ ((tmp << bita) | (tmp << bitb)));
}
 
// ============================================================================
// Deterministic sort (stable, index-tiebreaking)
// C ref: hacklib.c:36-122 nh_deterministic_qsort()
//
// JS version: sorts array in place using comparator, with original-index
// tiebreaking to ensure deterministic order across platforms.
// Unlike C which operates on raw bytes, this takes a JS array directly.
// ============================================================================
 
// hacklib.c:36 — deterministic replacement for qsort(), stable across platforms
export function nh_deterministic_qsort(arr, comparator) {
    if (!arr || arr.length < 2) return;
    const indexed = arr.map((item, i) => ({ item, i }));
    indexed.sort((a, b) => {
        const c = comparator(a.item, b.item);
        return c !== 0 ? c : a.i - b.i;
    });
    for (let i = 0; i < arr.length; i++) arr[i] = indexed[i].item;
}
 
// ============================================================================
// Data file utilities (JS-only, no C counterpart)
// ============================================================================
 
// xcrypt: XOR each char that has bit 5 or 6 set with a rotating bitmask
// (1,2,4,8,16). C ref: hacklib.c:464 xcrypt().
// JS version: takes str only (no output buffer needed), returns new string.
export function xcrypt(str) {
    let result = '';
    let bitmask = 1;
    for (let i = 0; i < str.length; i++) {
        let ch = str.charCodeAt(i);
        if (ch & (32 | 64)) ch ^= bitmask;
        if ((bitmask <<= 1) >= 32) bitmask = 1;
        result += String.fromCharCode(ch);
    }
    return result;
}
 
// Strip trailing underscores added by makedefs padding.
// C ref: rumors.c unpadline() — strips trailing '_' characters.
export function unpadline(str) {
    return str.replace(/_+$/, '');
}
 
// Parse a makedefs-compiled encrypted data file (epitaph, engrave, etc.).
// Format: 1 header line (skipped) + N encrypted+padded data lines.
// Returns { texts: string[], lineBytes: number[], chunksize: number }
export function parseEncryptedDataFile(fileText) {
    const allLines = fileText.split('\n');
    // Skip header line ("# This data file is generated by makedefs...")
    // and trailing empty string from final newline
    const dataLines = allLines.slice(1).filter(l => l.length > 0);
    const texts = [];
    const lineBytes = [];
    for (const line of dataLines) {
        const decrypted = unpadline(xcrypt(line));
        texts.push(decrypted);
        lineBytes.push(line.length + 1); // +1 for newline byte in file
    }
    const chunksize = lineBytes.reduce((a, b) => a + b, 0);
    return { texts, lineBytes, chunksize };
}
 
// Parse the makedefs-compiled rumors file which has two sections (true + false).
// Format: header line, index line, then true rumors followed by false rumors.
// Index line: "%04d,%06ld,%06lx;%04d,%06ld,%06lx;0,0,%06lx"
//   = trueCount(dec), trueSize(dec), trueOffset(hex);
//     falseCount(dec), falseSize(dec), falseOffset(hex); 0,0,eofOffset(hex)
// Returns { trueTexts, trueLineBytes, trueSize, falseTexts, falseLineBytes, falseSize }
export function parseRumorsFile(fileText) {
    const allLines = fileText.split('\n');
    // Line 0: "# This data file..." header (skipped)
    // Line 1: index line with section sizes and offsets
    const indexLine = allLines[1];
    const [truePart, falsePart] = indexLine.split(';');
    const trueParts = truePart.split(',');
    const falseParts = falsePart.split(',');
    const trueSize = parseInt(trueParts[1], 10);
    const falseSize = parseInt(falseParts[1], 10);
 
    // Data lines start at line 2
    const dataLines = allLines.slice(2).filter(l => l.length > 0);
 
    const trueTexts = [];
    const trueLineBytes = [];
    const falseTexts = [];
    const falseLineBytes = [];
    let cumBytes = 0;
 
    for (const line of dataLines) {
        const bytes = line.length + 1; // +1 for newline
        const decrypted = unpadline(xcrypt(line));
        if (cumBytes < trueSize) {
            trueTexts.push(decrypted);
            trueLineBytes.push(bytes);
        } else {
            falseTexts.push(decrypted);
            falseLineBytes.push(bytes);
        }
        cumBytes += bytes;
    }
 
    return { trueTexts, trueLineBytes, trueSize, falseTexts, falseLineBytes, falseSize };
}
 
// hacklib.c:985 — case-insensitive compare. Returns signed difference.
export function case_insensitive_comp(s1, s2) {
    const a = String(s1 ?? '');
    const b = String(s2 ?? '');
    let i = 0;
    for (;;) {
        let u1 = i < a.length ? a.charCodeAt(i) : 0;
        let u2 = i < b.length ? b.charCodeAt(i) : 0;
        if (u1 >= 65 && u1 <= 90) u1 += 32;
        if (u2 >= 65 && u2 <= 90) u2 += 32;
        if (u1 === 0 || u1 !== u2) return u1 - u2;
        i++;
    }
}
 
// hacklib.c:1003 — byte-copy helper used by native file descriptors.
// JS runtime variant supports stream-like objects exposing read()/write().
export function copy_bytes(ifd, ofd) {
    const BUFSIZ = 8192;
    if (!ifd || !ofd || typeof ifd.read !== 'function' || typeof ofd.write !== 'function') {
        return false;
    }
    const buf = new Uint8Array(BUFSIZ);
    for (;;) {
        const nfrom = ifd.read(buf, 0, BUFSIZ);
        if (typeof nfrom !== 'number' || nfrom < 0) return false;
        const nto = nfrom > 0 ? ofd.write(buf, 0, nfrom) : 0;
        if (nto !== nfrom) return false;
        if (nfrom !== BUFSIZ) return true;
    }
}
 
const DATA_MODELS = [
    { sz: [2, 4, 4, 8, 4], datamodel: 'ILP32LL64', dmplatform: 'x86 32-bit' },
    { sz: [2, 4, 4, 8, 8], datamodel: 'IL32LLP64', dmplatform: 'Windows x64 64-bit' },
    { sz: [2, 4, 8, 8, 8], datamodel: 'I32LP64', dmplatform: 'Unix 64-bit' },
    { sz: [2, 8, 8, 8, 8], datamodel: 'ILP64', dmplatform: 'Unix ILP64' },
];
 
function detectedDataModelSizes() {
    if (typeof process !== 'undefined') {
        const arch = String(process.arch || '');
        if (arch === 'ia32') return [2, 4, 4, 8, 4];
        if (arch === 'x64' || arch === 'arm64' || arch === 'ppc64'
            || arch === 's390x' || arch === 'riscv64' || arch === 'loong64') {
            return [2, 4, 8, 8, 8];
        }
    }
    return [2, 4, 8, 8, 8];
}
 
// hacklib.c:974 — detect this runtime's data model label.
export function datamodel(retidx) {
    const [szshort, szint, szlong, szll, szptr] = detectedDataModelSizes();
    return what_datamodel_is_this(retidx, szshort, szint, szlong, szll, szptr);
}
 
// hacklib.c:992 — map explicit size tuple to data model label/platform.
export function what_datamodel_is_this(retidx, szshort, szint, szlong, szll, szptr) {
    for (let i = 0; i < DATA_MODELS.length; i++) {
        const sz = DATA_MODELS[i].sz;
        if (szshort === sz[0] && szint === sz[1] && szlong === sz[2]
            && szll === sz[3] && szptr === sz[4]) {
            return retidx === 0 ? DATA_MODELS[i].datamodel : DATA_MODELS[i].dmplatform;
        }
    }
    return 'Unknown';
}
 
function fmtValue(spec, arg) {
    switch (spec) {
    case 's': return String(arg ?? '');
    case 'd':
    case 'i': return String(Number(arg) | 0);
    case 'u': return String((Number(arg) >>> 0));
    case 'x': return (Number(arg) >>> 0).toString(16);
    case 'X': return (Number(arg) >>> 0).toString(16).toUpperCase();
    case 'c': return String.fromCharCode(Number(arg) | 0);
    default: return `%${spec}`;
    }
}
 
function simplePrintf(fmt, args) {
    let ai = 0;
    return String(fmt).replace(/%([%sdixXuc])/g, (m, spec) => {
        if (spec === '%') return '%';
        const arg = ai < args.length ? args[ai++] : undefined;
        return fmtValue(spec, arg);
    });
}
 
function assignIntoOutString(target, text) {
    if (target && typeof target === 'object') {
        if ('value' in target) {
            target.value = text;
            return;
        }
        if ('text' in target) {
            target.text = text;
            return;
        }
    }
}
 
// hacklib.c:854 — snprintf wrapper used for bounded formatting.
// JS returns the bounded string and also writes into out.value/out.text if passed.
export function nh_snprintf(func, line, out, size, fmt, ...args) {
    let _func = func;
    let _line = line;
    let _out = out;
    let _size = size;
    let _fmt = fmt;
    let _args = args;

    // Support both signatures:
    // 1) nh_snprintf(func, line, out, size, fmt, ...)
    // 2) nh_snprintf(out, size, fmt, ...)
    if (typeof line !== 'number') {
        _func = '';
        _line = 0;
        _out = func;
        _size = line;
        _fmt = out;
        _args = [size, fmt, ...args].filter((x) => x !== undefined);
    }

    const rendered = simplePrintf(_fmt, _args);
    const limit = Math.max(0, Number(_size) - 1);
    const bounded = rendered.slice(0, limit);
    assignIntoOutString(_out, bounded);
    return bounded;
}
 
// hacklib.c:882 — convert Unicode code point to UTF-8 bytes into caller buffer.
// Returns 1 on success, 0 on invalid input or insufficient buffer.
export function unicodeval_to_utf8str(uval, buffer, bufsz) {
    const cap = Number.isInteger(bufsz) ? bufsz : (buffer?.length ?? 0);
    if (!buffer || cap < 5) return 0;
    if (!Number.isInteger(uval) || uval < 0) return 0;

    const bytes = [];
    if (uval < 0x80) {
        bytes.push(uval);
    } else if (uval < 0x800) {
        bytes.push(0xc0 + Math.floor(uval / 64));
        bytes.push(0x80 + (uval % 64));
    } else if ((uval - 0xd800) >>> 0 < 0x800) {
        return 0;
    } else if (uval < 0x10000) {
        bytes.push(0xe0 + Math.floor(uval / 4096));
        bytes.push(0x80 + Math.floor(uval / 64) % 64);
        bytes.push(0x80 + (uval % 64));
    } else if (uval < 0x110000) {
        bytes.push(0xf0 + Math.floor(uval / 262144));
        bytes.push(0x80 + Math.floor(uval / 4096) % 64);
        bytes.push(0x80 + Math.floor(uval / 64) % 64);
        bytes.push(0x80 + (uval % 64));
    } else {
        return 0;
    }

    if (bytes.length + 1 > cap) return 0;
    for (let i = 0; i < bytes.length; i++) buffer[i] = bytes[i];
    buffer[bytes.length] = 0;
    return 1;
}
 
// hacklib.c:18 — qsort index comparator helper, with index tiebreak.
export function nh_qsort_idx_cmp(va, vb) {
    const a = Number(va?.idx ?? 0);
    const b = Number(vb?.idx ?? 0);
    if (a < b) return -1;
    if (a > b) return 1;
    return 0;
}
 
// C ref: hacklib.c plur — plural suffix
export function plur(n) { return n === 1 ? '' : 's'; }
 
// C ref: monst.h ROLL_FROM(arr) — pick a random element from an array
export function ROLL_FROM(arr) { return arr[rn2(arr.length)]; }
 
// C ref: hacklib.c surface() — name of the surface at a position
export function surface(x, y) {
    const g = (typeof game !== 'undefined' ? game : null);
    const loc = g?.level?.at?.(x, y);
    if (!loc) return 'ground';
    const typ = loc.typ;
    // C ref: dungeon.c:1744-1781 surface()
    if (IS_POOL(typ)) return 'water';
    if (typ === ICE) return 'ice';
    if (IS_LAVA(typ)) return 'lava';
    if (typ === DRAWBRIDGE_DOWN) return 'bridge';
    if (typ === ALTAR) return 'altar';
    if (typ === GRAVE) return 'headstone';
    if (typ === FOUNTAIN) return 'fountain';
    if (typ === STAIRS || typ === LADDER) return 'stairs';
    if (IS_WALL(typ) || typ === SDOOR) return 'wall';
    if (IS_DOOR(typ)) return 'doorway';
    if (typ === ROOM || typ === CORR || IS_ROOM(typ)) return 'floor';
    return 'ground';
}
 
// ── Shared utility functions (extracted from 8-12 files each) ──
 
 
export function on_level(a, b) { return a?.dlevel === b?.dlevel && a?.dnum === b?.dnum; }
export function depth(uz) { const dnum = uz?.dnum ?? 0; const dlevel = uz?.dlevel ?? 1; return (game.dungeons?.[dnum]?.depth_start ?? 1) + dlevel - 1; }
export function Mgender(mon) { return mon?.female ? 1 : 0; }
export function MON_WEP(mtmp) { return mtmp?.mw || null; }
export function unsolid(data) { return !!(data?.mflags1 & 0x00100000); }
export function is_art(obj, artid) { return obj?.oartifact === artid; }
 
// C ref: hack.h monnear(mon,x,y) — is monster within 1 square?
// Some files call monnear(mtmp) with 1 arg to check near hero.
// Handle both: monnear(mtmp) and monnear(mtmp, x, y).
// C ref: mon.c:2455 monnear() — is monster within 1 step of (x,y)?
// NODIAG monsters (grid bugs) can't move diagonally, so distance 2
// (diagonal adjacency) doesn't count as "near" for them.
export function monnear(mtmp, x, y) {
    if (x === undefined) { x = game.u?.ux ?? 0; y = game.u?.uy ?? 0; }
    const dx = mtmp.mx - x, dy = mtmp.my - y;
    const dist2 = dx * dx + dy * dy;
    // C ref: NODIAG(monsndx(mon->data)) — only PM_GRID_BUG (116)
    if (dist2 === 2 && mtmp.mnum === 116 /* PM_GRID_BUG */) return false;
    return dist2 < 3;
}
 
// C ref: obj.h bimanual(otmp) — is weapon two-handed?
// C checks (WEAPON_CLASS || TOOL_CLASS) && oc_bimanual
export function bimanual(obj) {
    if (!obj) return false;
    const od = game.objects[obj.otyp];
    if (!od) return false;
    return (od.oc_class === 2 /* WEAPON_CLASS */ || od.oc_class === 6 /* TOOL_CLASS */)
        && !!od.oc_bimanual;
}
 
// C compat: Strcpy/Sprintf — JS strings are immutable; these are no-ops
export function Strcpy(_dst, src) { return src; }
export function Sprintf(_buf, fmt, ...args) { return fmt; }
 
// C ref: hack.h — Something() returns "Something" for invisible monster
export function Something() { return 'Something'; }
 
// Iterate fmon list with async callback; returns first match or null
export async function get_iter_mons(callback) {
    let mtmp = game.fmon;
    while (mtmp) {
        if (await callback(mtmp)) return mtmp;
        mtmp = mtmp.nmon;
    }
    return null;
}
 
// C ref: mtrapped_in_pit — check if monster is trapped in a pit
import { t_at } from './map_access.js';
import { is_pit } from './const.js';
export function mtrapped_in_pit(mtmp) {
    if (mtmp === game.youmonst) {
        if (game.u.utrap && game.u.utraptype === 1) {
            const tt = t_at(game.u.ux, game.u.uy);
            return !!tt && is_pit(tt.ttyp);
        }
        return false;
    }
    if (!mtmp?.mtrapped) return false;
    const tt = t_at(mtmp.mx, mtmp.my);
    return !!tt && is_pit(tt.ttyp);
}
 
// C ref: monverbself — "monster verbs himself/herself"
export function monverbself(mtmp, mnam, verb, extra) {
    const pron = mtmp?.female ? 'her' : 'him';
    if (extra) return `${mnam} ${verb}s ${extra} ${pron}self`;
    return `${mnam} ${verb}s ${pron}self`;
}
 
// C ref: anything union wrapper — JS uses {a_obj: obj} pattern
export function obj_to_any(obj) { return { a_obj: obj }; }
 
// C ref: nh_delay_output — display pause between animation frames.
// Parity: C instrumentation logs this as ^anim_frame[id=N] where N is
// a monotonic counter (matching C's nomux_anim_id).
export function nh_delay_output() {
    pushAnimFrameEvent();
}
 
// C ref: set level tile type
export function set_levltyp(x, y, typ) {
    const loc = game.level?.at(x, y);
    if (!loc) return false;
    const oldtyp = loc.typ;
    if (oldtyp === typ) return true;
 
    loc.typ = typ;
    if (IS_LAVA(typ))
        loc.lit = 1;
 
    if ((IS_FOUNTAIN(oldtyp) !== IS_FOUNTAIN(typ))
        || (IS_SINK(oldtyp) !== IS_SINK(typ))) {
        let nfountains = 0;
        let nsinks = 0;
        for (let yy = 0; yy < ROWNO; yy++) {
            for (let xx = 1; xx < COLNO; xx++) {
                const t = game.level?.at(xx, yy)?.typ;
                if (IS_FOUNTAIN(t))
                    nfountains++;
                if (IS_SINK(t))
                    nsinks++;
            }
        }
        if (game.level?.flags) {
            game.level.flags.nfountains = nfountains;
            game.level.flags.nsinks = nsinks;
        }
    }
    return true;
}