All files / js input.js

75.4% Statements 785/1041
73.14% Branches 177/242
62.9% Functions 39/62
75.4% Lines 785/1041

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 104273x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 15380x 15380x 15380x 15380x 15380x 15380x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 15469x 15469x 15469x 73x 15469x 15469x     15469x 73x 13379x 13379x             13379x 73x 15380x 15380x 15380x 15347x 6180x 15380x       15380x   15380x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 143x 143x 73x 73x 526x 526x 73x 73x 73x 73x                 73x 73x     73x 73x     73x 73x     73x 73x     73x 73x 73x 73x 73x 73x 29x 29x 29x 11x 11x 11x 29x 18x 29x 73x 29x 29x 29x 29x 29x 29x 29x 29x 29x 29x 29x 29x 73x 12752x 12752x 12752x 73x 73x 73x 8x 8x 8x 8x 73x 73x 73x 21x 21x 19x 21x 2x 2x 21x 21x 73x 73x 73x             73x 73x 73x     73x 73x 73x         73x 73x 73x                   73x 73x 73x                     73x 73x 73x                                     73x 73x 73x 5479x 5479x 5479x 11x 11x 11x 5479x 5479x 73x 73x 73x 2x 2x 73x 73x 73x 1588x 1588x 73x                                       73x 73x     73x                                   73x 73x 73x 73x                                                           73x 7273x 7273x 7273x 7273x     7273x 7273x 7273x 7273x                           7273x 73x 73x 73x 73x 73x 15380x 15380x 15380x 15380x 15380x 15380x 15380x 15380x 15380x 15380x 15380x 15380x 15380x 15307x 15307x 15380x 15307x 15380x 5317x 5317x 5317x 15307x 15380x 15380x 15380x 15380x 73x 73x           73x 73x 73x 73x 73x 73x 73x               73x 73x 73x 73x 73x 73x 73x 73x 73x 7273x 7273x                       7273x 7273x 7273x 7273x 7273x 7273x     7219x 7273x 73x 73x 73x 73x 6164x 6164x 6164x 6164x 6164x 6164x 6164x 73x 73x 73x 73x 73x 73x 73x 1056x 1056x 1056x 1056x 1056x 1056x 1056x 1056x 1056x 1056x 1056x 1056x 1056x 1056x       1056x 1056x 1056x 1056x 1056x 1056x 1056x 1056x 1056x 36x 36x 36x 1056x 1056x 1068x 1068x 1068x 1056x 1056x 1056x 1056x 1056x     1056x 1056x 1056x 1056x 1056x 1056x 1056x 1056x 1943x 1056x 1056x 1056x 1056x 1041x 1056x 1056x 4x 4x 1041x 1041x 1041x 1041x 1041x 1056x 1056x 1056x 1056x 1056x 1056x 1056x 1056x 48x 48x 1041x 1041x 1041x 1041x 1041x 1041x 1041x 1041x 1056x 73x 73x 2097x 2097x 2097x 2097x 2097x 24x 24x 24x 86x 24x 24x 24x 86x 24x       24x 24x 24x 2097x 2097x 2097x 73x 73x 73x 73x 606x 606x 606x 606x 606x 606x 606x 606x 606x 606x 606x 606x 606x 606x 606x 606x 606x 606x 606x 606x 11x 10x 605x 605x 605x 606x 589x 589x 605x 605x 605x 605x 606x 594x 594x 605x 605x 605x 5419x 5419x 5419x 5419x 5419x 1532x 1532x 1532x 1532x 1532x 1532x 1532x 5419x 5419x 5419x 5419x 5419x 5419x 5419x 5419x 5419x 5419x 5419x 5419x 589x 589x 589x 589x 5419x 5419x                                     5419x 5419x 5419x       5419x 5419x 5419x 5419x 605x 605x 605x 605x 605x 605x 605x 605x 605x 605x 605x 606x 5419x 5419x 5419x 5419x 5419x 5419x 5419x 602x 602x 602x 602x 602x 602x 602x 602x 602x 602x 602x 586x 586x 602x 602x 602x 602x 602x 602x 586x 600x 16x 16x 16x 16x 16x 16x 16x 16x 602x 5419x   4817x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x       3x 3x 3x 3x 3x 3x 4817x 4x   4x 3x 3x 4814x 4809x                 4809x 4809x 4809x 5419x 606x 605x 605x 606x 606x 606x 606x 606x 606x 73x 73x 73x 73x 628x 628x 628x 628x 628x 628x 628x 119x 115x 628x 628x 628x 87x 87x 72x 72x 72x 87x 624x 624x 624x 624x 624x 624x       624x 624x 624x 624x 624x 624x 624x 624x 615x 615x 624x 624x 624x 624x 624x 624x 624x 624x 624x 624x 624x 624x 624x 624x 624x 624x 624x 624x 624x     624x 628x 628x 628x 628x 628x 628x 628x 628x 628x 628x 628x 628x 628x 628x 628x 628x 628x 628x 628x 628x 628x 628x 620x 620x 620x 628x 628x 628x 628x 628x 745x 745x 745x 745x 5x 5x 5x 5x 736x 745x 16x       16x 1x 1x 1x 16x       15x 15x 15x 720x 720x 744x 745x 599x 599x 599x 599x 121x 121x 628x 624x 624x 628x 73x 73x 73x 10x 10x 10x 10x 73x 73x 73x 73x 73x 47x 47x 47x 47x 47x 47x 47x 47x 47x 47x 47x 47x 47x 47x 47x 47x 47x 47x 47x 65x 65x 65x 65x 65x 65x 18x 18x 18x 18x   18x     18x 18x 65x               47x     47x 47x 47x 47x 18x 18x 18x 65x 18x 18x 18x 18x 18x               18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 65x 47x 47x 47x 47x 73x 73x 112x 112x 112x 73x 16681x 16681x 16681x 73x 73x 73x 7x 7x 7x 73x 73x 73x 73x 73x 13x 13x 12x 13x 73x 73x 72x 72x 72x 72x 72x 72x  
// input.js -- Runtime-agnostic input primitives.
// Provides an async input queue plus module-level wrappers used by game code.
 
// Thrown when Ctrl-C is pressed during getlin or chargen nhgetch.
export class CtrlCInterrupt extends Error {
    constructor() { super('Interrupted'); this.name = 'CtrlCInterrupt'; }
}
 
import { pushRngLogEntry } from './rng.js';
import { enterModal, exitModal, getModalOwner, resetModalGuard } from './modal_guard.js';
import { CMDQ_KEY, CMDQ_EXTCMD, CMDQ_DIR, CMDQ_USER_INPUT, CMDQ_INT, CQ_CANNED, CQ_REPEAT } from './const.js';
import { envFlag } from './runtime_env.js';
import { CLR_GRAY } from './terminal.js';
// waitForMoreDismissKey/isMoreDismissKey removed — more() now uses a simple loop
import {
    game, beginOriginAwait, endOriginAwait, getCommandExecState,
} from './gstate.js';
import { docorner, flush_screen, tty_clear_nhwindow } from './display.js';
 
const TOPLINE_EMPTY = 0;
const TOPLINE_NEED_MORE = 1;
const TOPLINE_NON_EMPTY = 2;
const TOPLINE_SPECIAL_PROMPT = 3;
const WIN_STOP = 1;
const WIN_NOSTOP = 4;
 
function clearMessageWinStop() {
    const display = getRuntimeDisplay();
    if (display && typeof display.messageWinFlags === 'number') {
        display.messageWinFlags &= ~WIN_STOP;
    }
}
 
import { recordKey } from './keylog.js';
const debugRepaint = () => false;
const logRepaint = () => {};
const repaintHp = () => {};
const repaintBotl = () => {};
const repaintBotlx = () => {};
const repaintTimeBotl = () => {};
const repaintToplineState = () => {};
const repaintCursorRow = () => {};
const repaintCursorCol = () => {};
 
function ynTraceEnabled() {
    return envFlag('WEBHACK_YN_TRACE');
}
 
function ynTrace(...args) {
    if (!ynTraceEnabled()) return;
    // eslint-disable-next-line no-console
    console.log('[YN_TRACE]', ...args);
}
 
function traceConsumedKey(source, ch) {
    if (!envFlag('WEBHACK_TRACE_KEYS')) return;
    if (!game._dbgConsumedKeys) game._dbgConsumedKeys = [];
    game._dbgConsumedKeys.push({
        idx: game._dbgConsumedKeys.length,
        source,
        key: Number(ch) | 0,
    });
}
 
function assertLegalInputOwner(site) {
    const modalOwner = getModalOwner();
    if (modalOwner) return;
    if (!game) return;
    const commandState = getCommandExecState(game);
    if (commandState?.activeToken) return;
    // Monster turns may legitimately read input (e.g. shopkeeper zaps
    // wand → mzapwand → pline → more → nhgetch). In C, wgetch blocks
    // during monster turns with no special execution context needed.
    if (game.context?.mon_moving) return;
    throw new Error(`Illegal input read at ${site}: no modal owner and no active command execution`);
}
 
/**
 * Display contract used by input helpers.
 * @typedef {Object} InputDisplay
 * @property {boolean} [messageNeedsMore]
 * @property {string|null} [topMessage]
 * @property {(row:number) => void} [clearRow]
 * @property {(x:number, y:number, text:string, color?:number) => void} [putstr]
 * @property {(msg:string) => void} [putstr_message]
 */
 
/**
 * Input runtime contract.
 * @typedef {Object} InputRuntime
 * @property {(ch:number) => void} pushInput
 * @property {() => Promise<number>} nhgetch
 * @property {() => void} [clearInputQueue]
 * @property {() => InputDisplay|null} [getDisplay]
 */
 
 
 
export function setStartupPromptPhase(flag) {
    game.startupPromptPhase = !!flag;
}
 
export function isStartupPromptPhase() {
    return game.startupPromptPhase;
}
 
// Reset module-level input/command-queue state.
// Needed for deterministic multi-session replay in long-lived workers.
export function resetInputModuleState() {
    game?.nhDisplay?.clearInputQueue();
    resetModalGuard();
    game.startupPromptPhase = false;
    game.cmdqInputModeDoAgain = false;
    game.cmdqRepeatRecordMode = false;
    _cmdQueues[CQ_CANNED] = null;
    _cmdQueues[CQ_REPEAT] = null;
}
 
export function setCmdqInputMode(inDoAgain) {
    game.cmdqInputModeDoAgain = !!inDoAgain;
}
 
export function setCmdqRepeatRecordMode(enabled) {
    game.cmdqRepeatRecordMode = !!enabled;
}
 
export function pushInput(ch) {
    game.nhDisplay.pushKey(ch);
}
 
export function clearInputQueue() {
    game?.nhDisplay?.clearInputQueue();
}
 
const _cmdQueues = {
    [CQ_CANNED]: null,
    [CQ_REPEAT]: null,
};
 
function cmdq_appendNode(queueKind, node) {
    let cq = _cmdQueues[queueKind];
    if (!cq) {
        _cmdQueues[queueKind] = node;
        return;
    }
    while (cq.next) cq = cq.next;
    cq.next = node;
}
 
function cmdq_makeNode(typ) {
    return {
        typ,
        key: null,
        dirx: 0,
        diry: 0,
        dirz: 0,
        intval: 0,
        ec_entry: null,
        next: null,
    };
}
 
function cmdq_queue_kind(inDoAgain) {
    return inDoAgain ? CQ_REPEAT : CQ_CANNED;
}
 
// C ref: cmd.c cmdq_add_ec()
export function cmdq_add_ec(queueKind, extcmdEntry) {
    const node = cmdq_makeNode(CMDQ_EXTCMD);
    node.ec_entry = extcmdEntry || null;
    cmdq_appendNode(queueKind, node);
}
 
// C ref: cmd.c cmdq_add_key()
export function cmdq_add_key(queueKind, key) {
    const node = cmdq_makeNode(CMDQ_KEY);
    if (typeof key === 'string') {
        node.key = key.length ? key.charCodeAt(0) : 0;
    } else {
        node.key = key | 0;
    }
    cmdq_appendNode(queueKind, node);
}
 
// C ref: cmd.c cmdq_add_dir()
export function cmdq_add_dir(queueKind, dx, dy, dz) {
    const node = cmdq_makeNode(CMDQ_DIR);
    node.dirx = dx | 0;
    node.diry = dy | 0;
    node.dirz = dz | 0;
    cmdq_appendNode(queueKind, node);
}
 
// C ref: cmd.c cmdq_add_userinput()
export function cmdq_add_userinput(queueKind) {
    cmdq_appendNode(queueKind, cmdq_makeNode(CMDQ_USER_INPUT));
}
 
// C ref: cmd.c cmdq_add_int()
export function cmdq_add_int(queueKind, val) {
    const node = cmdq_makeNode(CMDQ_INT);
    node.intval = val | 0;
    cmdq_appendNode(queueKind, node);
}
 
// C ref: cmd.c cmdq_shift() -- shift last entry to first.
export function cmdq_shift(queueKind) {
    let cq = _cmdQueues[queueKind];
    if (!cq || !cq.next) return;
    while (cq.next && cq.next.next) cq = cq.next;
    const tail = cq.next;
    if (!tail) return;
    tail.next = _cmdQueues[queueKind];
    _cmdQueues[queueKind] = tail;
    cq.next = null;
}
 
// C ref: cmd.c cmdq_reverse()
export function cmdq_reverse(head) {
    let prev = null;
    let curr = head || null;
    while (curr) {
        const next = curr.next;
        curr.next = prev;
        prev = curr;
        curr = next;
    }
    return prev;
}
 
// C ref: cmd.c cmdq_copy()
export function cmdq_copy(queueKind) {
    let tmp = null;
    let cq = _cmdQueues[queueKind];
    while (cq) {
        const copy = {
            typ: cq.typ,
            key: cq.key,
            dirx: cq.dirx,
            diry: cq.diry,
            dirz: cq.dirz,
            intval: cq.intval,
            ec_entry: cq.ec_entry,
            next: tmp,
        };
        tmp = copy;
        cq = cq.next;
    }
    return cmdq_reverse(tmp);
}
 
// C ref: cmd.c cmdq_pop() -- queue chosen by in_doagain flag.
export function cmdq_pop(inDoAgain = false) {
    const queueKind = cmdq_queue_kind(inDoAgain);
    const node = _cmdQueues[queueKind];
    if (node) {
        _cmdQueues[queueKind] = node.next;
        node.next = null;
    }
    return node;
}
 
// C ref: cmd.c cmdq_peek()
export function cmdq_peek(queueKind) {
    return _cmdQueues[queueKind] || null;
}
 
// C ref: cmd.c cmdq_clear()
export function cmdq_clear(queueKind) {
    _cmdQueues[queueKind] = null;
}
 
function cmdq_clone_chain(head) {
    let tmp = null;
    let cq = head;
    while (cq) {
        const copy = {
            typ: cq.typ,
            key: cq.key,
            dirx: cq.dirx,
            diry: cq.diry,
            dirz: cq.dirz,
            intval: cq.intval,
            ec_entry: cq.ec_entry,
            next: tmp,
        };
        tmp = copy;
        cq = cq.next;
    }
    return cmdq_reverse(tmp);
}
 
export function cmdq_restore(queueKind, head) {
    _cmdQueues[queueKind] = cmdq_clone_chain(head || null);
}
 
function dirNodeToKey(node) {
    if (!node) return 0;
    if (node.dirz > 0) return '>'.charCodeAt(0);
    if (node.dirz < 0) return '<'.charCodeAt(0);
    const dx = node.dirx | 0;
    const dy = node.diry | 0;
    if (dx === 0 && dy === 0) return '.'.charCodeAt(0);
    if (dx === -1 && dy === 0) return 'h'.charCodeAt(0);
    if (dx === 1 && dy === 0) return 'l'.charCodeAt(0);
    if (dx === 0 && dy === -1) return 'k'.charCodeAt(0);
    if (dx === 0 && dy === 1) return 'j'.charCodeAt(0);
    if (dx === -1 && dy === -1) return 'y'.charCodeAt(0);
    if (dx === 1 && dy === -1) return 'u'.charCodeAt(0);
    if (dx === -1 && dy === 1) return 'b'.charCodeAt(0);
    if (dx === 1 && dy === 1) return 'n'.charCodeAt(0);
    return 0;
}
 
// Pop and decode one top-level queued command.
// Returns null or { key, countPrefix, extcmd }.
export function cmdq_pop_command(inDoAgain = false) {
    const queueKind = cmdq_queue_kind(inDoAgain);
    let countPrefix = 0;
    let head = _cmdQueues[queueKind];
    if (!head) return null;

    if (head.typ === CMDQ_INT) {
        countPrefix = Number.isFinite(head.intval) ? Math.max(0, head.intval | 0) : 0;
        _cmdQueues[queueKind] = head.next || null;
        head = _cmdQueues[queueKind];
    }
    if (!head) return null;

    _cmdQueues[queueKind] = head.next || null;
    head.next = null;

    if (head.typ === CMDQ_KEY) {
        return { key: head.key | 0, countPrefix };
    }
    if (head.typ === CMDQ_DIR) {
        return { key: dirNodeToKey(head), countPrefix };
    }
    if (head.typ === CMDQ_EXTCMD) {
        return { key: 0, countPrefix, extcmd: head.ec_entry || null };
    }
    if (head.typ === CMDQ_USER_INPUT) {
        return null;
    }
    return null;
}
 
function popQueuedInputKey(inDoAgain = false) {
    const queueKind = cmdq_queue_kind(inDoAgain);
    const head = _cmdQueues[queueKind];
    if (!head) return null;
    if (head.typ === CMDQ_EXTCMD) return null;

    _cmdQueues[queueKind] = head.next || null;
    head.next = null;
 
    if (head.typ === CMDQ_KEY) {
        return head.key | 0;
    }
    if (head.typ === CMDQ_DIR) return dirNodeToKey(head);
    if (head.typ === CMDQ_USER_INPUT) return null;
    if (head.typ === CMDQ_INT) {
        const digits = String(Math.max(0, head.intval | 0));
        if (!digits.length) return '0'.charCodeAt(0);
        for (let i = 1; i < digits.length; i++) {
            game.nhDisplay.pushKey(digits.charCodeAt(i));
        }
        return digits.charCodeAt(0);
    }
    return null;
}
 
// Lowest-level runtime key read (no queue/--More-- handling).
// C analogue: raw windowproc read underneath readchar()/nhgetch().
// Calls game._preNhgetchHook if set.
// This hook is replay instrumentation only (PES capture), not gameplay logic.
async function nhgetch_raw() {
    assertLegalInputOwner('nhgetch_raw');
    const hook = game?._preNhgetchHook;
    const display = game?.nhDisplay;
    const snap = beginOriginAwait(game, 'input');
    clearMessageWinStop();
    // C NOMUX writes the screen buffer at each nhgetch entry, showing
    // the state from AFTER the PREVIOUS key was processed. Fire the
    // instrumentation hook BEFORE reading the next key to capture that state.
    try {
        if (hook) await hook();
        // Read directly from the display (Terminal) — no intermediate runtime.
        let ch = await display.readKey();
        // C TTY behavior: carriage return is normalized to linefeed.
        // This affects command dispatch (e.g. Enter mapping to Ctrl-J paths).
        if (ch === 13) ch = 10;
        // C ref: wintty.c:4290 tty_nhgetch transitions NEED_MORE -> NON_EMPTY
        if (display && display.toplin === TOPLINE_NEED_MORE) {
            display.toplin = TOPLINE_NON_EMPTY;
            pushRngLogEntry('^toplin[tty_nhgetch=2]');
        }
        return ch;
    } finally {
        endOriginAwait(game, snap);
    }
}
 
export async function callPreInputHook() {
    const hook = game?._preNhgetchHook;
    if (hook) {
        await hook();
    }
}
 
// nhgetch_display_raw removed — nhgetch is now simple enough for all callers.
 
// Get a character of input (async)
// C ref: tty_nhgetch always transitions toplin NEED_MORE→NON_EMPTY.
// nhgetch_raw does this at line 502-506, but the queued-key and replay paths
// bypass nhgetch_raw.  This helper applies the same transition for those paths.
function _nhgetchToplinTransition() {
    const display = getRuntimeDisplay();
    if (display && display.toplin === TOPLINE_NEED_MORE) {
        display.toplin = TOPLINE_NON_EMPTY;
        pushRngLogEntry('^toplin[tty_nhgetch=2]');
    }
}
 
// This is the JS equivalent of C's nhgetch().
// C ref: winprocs.h win_nhgetch
// C ref: nhgetch() — read one key from input.
// Matches C's simple model: check command queue, then read from runtime.
// Command boundary --More-- handling belongs in callers (_gameLoopStep),
// not here — matching C where tty_clearmsg handles --More-- in parse(),
// not inside nhgetch.
export async function nhgetch() {
    const queuedKey = popQueuedInputKey(game.cmdqInputModeDoAgain);
    if (Number.isFinite(queuedKey)) {
        assertLegalInputOwner('nhgetch:queued');
        clearMessageWinStop();
        ynTrace('raw=queued', queuedKey, String.fromCharCode(queuedKey));
        recordKey(queuedKey);
        traceConsumedKey('queued', queuedKey);
        // C ref: tty_nhgetch transitions toplin 1→2 on every key read,
        // regardless of source.  The queued-key path was skipping nhgetch_raw
        // which does this transition, so do it here too.
        _nhgetchToplinTransition();
        return queuedKey;
    }
 
    const ch = await nhgetch_raw();
    ynTrace('raw=runtime', ch, Number.isFinite(ch) ? String.fromCharCode(ch) : String(ch));
    recordKey(ch);
    traceConsumedKey('runtime', ch);
    if (game.cmdqRepeatRecordMode && Number.isFinite(ch)) {
        cmdq_add_key(CQ_REPEAT, ch);
    }
    return ch;
}
 
// Prompt/modal key reads must bypass command queues.
// C ref: tty_getlin/tty_yn_function call tty_nhgetch directly; cmdq dispatch
// is handled in parse/rhack, not inside prompt readers.
async function nhgetch_prompt() {
    const ch = await nhgetch_raw();
    ynTrace('raw=prompt', ch, Number.isFinite(ch) ? String.fromCharCode(ch) : String(ch));
    recordKey(ch);
    traceConsumedKey('prompt', ch);
    return ch;
}
 
// C ref: topl.c:204 more()
// Display --More-- after current message, wait for space/ESC to dismiss.
// This is modal — blocks until the key is pressed. Simple loop like C.
// C ref: topl.c:204 more()
// Append --More-- to the current message and wait for dismiss key.
export async function more(display) {
    pushRngLogEntry('>more');
    if (!display) { pushRngLogEntry('<more'); return; }
    const g = game;
    const rawMsgText = g._pending_message || '';
    const hadMoreSuffix = rawMsgText.endsWith('--More--');
    const msgText = hadMoreSuffix
        ? rawMsgText.slice(0, -8)
        : rawMsgText;
 
    // C ref: topl.c:224-228 — if curx >= CO-8, output newline before
    // "--More--" so it has room on the next row without wrapping.
    if (hadMoreSuffix
        && envFlag('WEBHACK_STRICT_MORE_ASSERT')
        && display.toplin !== TOPLINE_NEED_MORE) {
        const err = new Error('more() called twice without dismiss');
        throw err;
    }
    if (typeof display.putstr === 'function') {
        const cols = Number.isInteger(display.cols) ? display.cols : 80;
        const wrappedRows = wrapToplineRows(msgText, cols);
        const cury = Math.max(0, wrappedRows.length - 1);
        const curx = (wrappedRows[wrappedRows.length - 1] || '').length;
        // C ref: topl.c:226 — if (cw->curx >= CO - 8) topl_putsym('\n');
        let moreRow = cury;
        let moreCol = curx;
        if (curx >= cols - 8) {
            moreRow = cury + 1;
            moreCol = 0;
        }
        g._pending_message = msgText + '--More--';
        for (let r = 0; r < wrappedRows.length; r++) {
            display.clearRow(r);
            display.putstr(0, r, wrappedRows[r]);
        }
        display.putstr(moreCol, moreRow, '--More--');
        if (typeof display.setCursor === 'function') {
            display.setCursor(moreCol + 8, moreRow);
        }
    } else {
        g._pending_message = (g._pending_message || '') + '--More--';
    }
 
    // C: xwaitforspace — loop nhgetch until space/ESC/enter.
    // The terminal text stays visible across the loop, but toplin only
    // transitions NEED_MORE -> NON_EMPTY once, on the first read.
    let ch;
    enterModal('more');
    try {
        do {
            ch = await nhgetch_raw();
        } while (ch !== 32 && ch !== 27 && ch !== 13 && ch !== 10);
    } finally {
        exitModal('more');
    }
 
    if (ch === 27 && typeof display.messageWinFlags === 'number'
        && !(display.messageWinFlags & WIN_NOSTOP)) {
        display.messageWinFlags |= WIN_STOP;
    }
 
    // C ref: topl.c:261-264 — if toplin && cury, docorner to clear message rows.
    // C tracks cw->cury as the cursor row after all output including "--More--".
    // If the message cursor was at column >= CO-8, more() adds a newline before
    // "--More--", putting it on the next row. This makes cury > 0.
    const cols = Number.isInteger(display?.cols) ? display.cols : 80;
    const wrappedRows = wrapToplineRows(msgText || '', cols);
    const curyBase = Math.max(0, wrappedRows.length - 1);
    const curx = (wrappedRows[wrappedRows.length - 1] || '').length;
    // C ref: topl.c:236 — if (cw->curx >= CO - 8) topl_putsym('\n')
    const moreOnNewLine = curx >= cols - 8;
    const cury = curyBase + (moreOnNewLine ? 1 : 0);
    if (display.toplin && cury > 0) {
        await docorner(1, cury + 1);
    }
 
    // C: toplin = TOPLINE_EMPTY, clear message
    pushRngLogEntry('^toplin[more=0]');
    display.toplin = 0;
    // C ref: topl.c more() does not clear gt.toplines; it keeps the
    // remembered message text while clearing the visible topline.
    g._pending_message = msgText;
    pushRngLogEntry('<more');
}
 
// C ref: topl.c:update_topl() word-wrap for long messages before redotoplin.
function wrapToplineRows(msg, cols) {
    const out = [];
    let remaining = String(msg || '');
    if (cols <= 0) return [''];
    while (remaining.length >= cols) {
        const hard = cols - 1; // C starts scan from CO-1
        let split = -1;
        for (let i = hard; i > 0; i--) {
            if (remaining[i] === ' ') {
                split = i;
                break;
            }
        }
        if (split < 0) {
            split = remaining.indexOf(' ');
            if (split < 0) break;
        }
        out.push(remaining.slice(0, split));
        remaining = remaining.slice(split + 1);
    }
    out.push(remaining);
    return out;
}
 
// Get a line of input (async)
// C ref: winprocs.h win_getlin
export async function getlin(prompt, display, options = {}) {
    const runtimeDisplay = getRuntimeDisplay();
    const disp = display || runtimeDisplay;
    let line = '';
    let overflowCursor = 0;
    let loggedInitialPromptPaint = false;
    const logHookedLifecycle = !!options.logHookedLifecycle;
    const maxLineLength = Math.max(0, (Number.isInteger(disp?.cols) ? disp.cols : 80) - 1);
    const baseRow = Number.isInteger(options?.row) ? options.row : 0;
    const overflowRow = baseRow + 1;
    const oldBotDisabled = !!game?.bot_disabled;
    const autocomplete = (typeof options?.autocomplete === 'function')
        ? options.autocomplete
        : null;
    if (game) {
        game.bot_disabled = true;
    }
 
    try {
        // C ref: tty_getlin calls more() when toplin=NEED_MORE before the prompt
        if (disp?.toplin === TOPLINE_NEED_MORE) {
            await more(disp);
        }
        if (disp) {
            disp.toplin = TOPLINE_SPECIAL_PROMPT;
        }
        if (logHookedLifecycle) {
            pushRngLogEntry('^toplin[hooked_tty_getlin=3]');
        }
 
        // C ref: tty_getlin() -> custompline() -> vpline() -> flush_screen(1) -> bot().
        // C flushes the screen (including bot update) before showing the getlin prompt.
        // Skip during chargen (C: guarded by `if (u.ux)`).
        if (game?.u?.ux) {
            await flush_screen(1);
        }
 
    // Helper to update display
    const updateDisplay = async () => {
        if (disp) {
            const promptPrefix = prompt.endsWith(' ') ? prompt : `${prompt} `;
            const typedPromptLine = `${promptPrefix}${line}`;
            let shownInput = line;
            if (autocomplete) {
                const candidate = autocomplete(line);
                if (typeof candidate === 'string'
                    && candidate.length >= line.length
                    && candidate.startsWith(line)) {
                    shownInput = candidate;
                }
            }
            const promptLine = `${promptPrefix}${shownInput}`;
            const cols = disp.cols || 80;
            const wrapWidth = Math.max(1, cols - 1);
            // Clear the message row and display prompt + current input.
            // Don't use putstr_message as it concatenates short messages.
            disp.clearRow(baseRow);
            const row0Text = promptLine.slice(0, wrapWidth);
            await disp.putstr(0, baseRow, row0Text, CLR_GRAY);
            // C ref: gt.toplines tracks getlin prompt text so subsequent
            // pline calls can detect overflow against it.
            if (typeof disp.toplines === 'string') disp.toplines = row0Text;
            if (logHookedLifecycle && !loggedInitialPromptPaint) {
                pushRngLogEntry('^toplin[addtopl=1]');
                disp.toplin = TOPLINE_NEED_MORE;
                loggedInitialPromptPaint = true;
            }
            // C ref: tty terminal auto-wraps text past column 80 to row 1.
            if (promptLine.length > wrapWidth || overflowCursor > 0) {
                const overflow = promptLine.length > wrapWidth
                    ? promptLine.slice(wrapWidth)
                    : '';
                disp.clearRow(overflowRow);
                if (overflow.length > 0) {
                    await disp.putstr(0, overflowRow, overflow, CLR_GRAY);
                }
                disp._topMessageRow1 = overflow;
                const typedOverflow = typedPromptLine.length > wrapWidth
                    ? typedPromptLine.slice(wrapWidth)
                    : '';
                const baseOverflowCol = Math.min(typedOverflow.length, wrapWidth);
                const cursorCol = (overflowCursor > 0)
                    ? Math.min(baseOverflowCol + overflowCursor, wrapWidth)
                    : baseOverflowCol;
                if (typeof disp.setCursor === 'function') {
                    disp.setCursor(cursorCol, typedPromptLine.length > wrapWidth ? overflowRow : baseRow);
                }
            } else {
                // Clear row 1 if we previously overflowed but backspaced back
                if (disp._topMessageRow1 !== undefined) {
                    disp.clearRow(overflowRow);
                    disp._topMessageRow1 = undefined;
                }
                const cursorCol = Math.min(typedPromptLine.length, cols - 1);
                if (typeof disp.setCursor === 'function') disp.setCursor(cursorCol, baseRow);
            }
        }
    };
 
    // No explicit initial display or capture here — the loop-top
    // updateDisplay() + nhgetch hook handles the first frame.
    // C ref: getline.c renders the prompt, then immediately calls pgetchar()
    // which does nhgetch. The NOMUX capture at that nhgetch sees the prompt.
 
    const readPromptKey = async () => await nhgetch_prompt();
 
        enterModal('getlin');
        try {
            while (true) {
        // C ref: getline.c pgetchar() echoes the previous character via
        // tty_putsym BEFORE calling nhgetch for the next key. The NOMUX
        // capture at nhgetch sees the echoed character. Match that ordering
        // by updating the display BEFORE reading the next key.
        await updateDisplay();
        const ch = await readPromptKey();
        if (ch === 13 || ch === 10) { // Enter
            // C ref: hooked_tty_getlin (getline.c) — on Enter, C does NOT
            // clear the prompt row. The typed text remains visible on screen.
            // C's toplin transitions to NON_EMPTY(2) here, unconditionally.
            // C ref: getline.c:214-217 — toplin=NON_EMPTY, event, then
            // clear_nhwindow(WIN_MESSAGE).
            // logHookedLifecycle gates event emission: JS chargen uses
            // getlin for name entry, but C uses pgetchar/rawchar there
            // (no getlin events). Gameplay getlin (wish, engrave, etc.)
            // passes logHookedLifecycle=true to emit matching events.
            if (disp) disp.toplin = TOPLINE_NON_EMPTY;
            if (logHookedLifecycle) {
                pushRngLogEntry('^toplin[hooked_tty_getlin=2]');
            }
            if (disp) {
                disp.topMessage = null;
                disp.messageNeedsMore = false;
                if (typeof disp.toplines === 'string') disp.toplines = '';
            }
            if (logHookedLifecycle) {
                await tty_clear_nhwindow('message');
            } else {
                // C ref: getline.c:217 — clear_nhwindow clears row 0.
                // Emit no events for chargen/non-lifecycle paths.
                if (disp) {
                    disp.clearRow(0);
                    disp.toplin = TOPLINE_EMPTY;
                    game._pending_message = '';
                }
            }
            return line;
        } else if (ch === 3) { // Ctrl-C
            throw new CtrlCInterrupt();
        } else if (ch === 27) { // ESC
            if (disp) disp.toplin = TOPLINE_NON_EMPTY;
            if (logHookedLifecycle) {
                pushRngLogEntry('^toplin[hooked_tty_getlin=2]');
            }
            if (disp) {
                disp.topMessage = null;
                disp.messageNeedsMore = false;
                if (typeof disp.toplines === 'string') disp.toplines = '';
                if (typeof disp.clearRow === 'function') {
                    disp.clearRow(baseRow);
                    if (disp._topMessageRow1 !== undefined) {
                        disp.clearRow(overflowRow);
                        disp._topMessageRow1 = undefined;
                    }
                }
            }
            if (logHookedLifecycle) {
                await tty_clear_nhwindow('message');
            }
            return null; // cancelled
        } else if (ch === 8 || ch === 127) { // Backspace
            if (overflowCursor > 0) {
                overflowCursor--;
            } else if (line.length > 0) {
                line = line.slice(0, -1);
            }
        } else if (ch >= 32 && ch < 127) {
            if (line.length >= maxLineLength) {
                const promptPrefix = prompt.endsWith(' ') ? prompt : `${prompt} `;
                const promptLineLength = promptPrefix.length + line.length;
                const wrapWidth = Math.max(1, (disp?.cols || 80) - 1);
                overflowCursor = (promptLineLength > wrapWidth)
                    ? 1
                    : (overflowCursor + 1);
                continue;
            }
            overflowCursor = 0;
            line += String.fromCharCode(ch);
        }
            }
        } finally {
            exitModal('getlin');
        }
    } finally {
        if (game) {
            game.bot_disabled = oldBotDisabled;
        }
    }
}
 
// Yes/no/quit prompt (async)
// C ref: winprocs.h win_yn_function
export async function ynFunction(query, choices, def, display, options = {}) {
    const runtimeDisplay = getRuntimeDisplay();
    const disp = display || runtimeDisplay;
    const consumePendingMore = options?.consumePendingMore !== false;
    // C-faithful boundary: don't overwrite/append over a pending --More--;
    // first consume it so this prompt starts on a fresh topline.
    // C ref: tty_yn_function (topl.c:427) calls more() when toplin=NEED_MORE
    if (consumePendingMore && disp?.toplin === TOPLINE_NEED_MORE) {
        await more(disp);
    }
    const statusPlayer = disp?._lastMapState?.player || game?.player || null;
    let prompt = query;
    if (choices) {
        prompt += ` [${choices}]`;
        if (def && def !== '\0' && def !== 0) {
            const defChar = typeof def === 'string' ? def : String.fromCharCode(def);
            prompt += ` (${defChar})`;
        }
    }
    prompt += ' ';
 
    if (disp) {
        if (typeof disp.clearRow === 'function') {
            disp.clearRow(0);
            if (Object.hasOwn(disp, '_topMessageRow1') && disp._topMessageRow1 !== undefined) {
                disp.clearRow(1);
                disp._topMessageRow1 = undefined;
            }
        }
        // C ref: topl.c:394 — ttyDisplay->toplin = TOPLINE_SPECIAL_PROMPT
        pushRngLogEntry('^toplin[tty_yn_function=3]');
        disp.toplin = TOPLINE_SPECIAL_PROMPT;
        // C ref: topl.c:420 — custompline() → vpline() → flush_screen(1)
        // before putmesg(). This is where C's >bot event fires if botl is set.
        // C ref: pline.c:273 — guarded by `if (u.ux)` (skip during chargen)
        if (game?.u?.ux) {
            await flush_screen(1);
        }
        // C ref: tty_yn_function() issues the prompt via custompline with
        // SUPPRESS_HISTORY. C show_topl only emits toplin event when
        // cury && toplin != SPECIAL_PROMPT. For yn prompts, toplin is
        // SPECIAL_PROMPT so show_topl event does NOT fire.
        pushRngLogEntry('^toplin[addtopl=1]');
        disp.toplin = TOPLINE_NEED_MORE;
        if (Object.hasOwn(disp, 'topMessage')) disp.topMessage = null;
        if (typeof disp.putstr === 'function') {
            await disp.putstr(0, 0, prompt, CLR_GRAY);
            const cols = Number.isInteger(disp.cols) ? disp.cols : 80;
            if (typeof disp.setCursor === 'function') {
                disp.setCursor(Math.min(prompt.length, cols - 1), 0);
            }
            if (Object.hasOwn(disp, 'topMessage')) disp.topMessage = prompt.trimEnd();
            if (Array.isArray(disp.messages)) {
                disp.messages.push(prompt.trimEnd());
                if (disp.messages.length > 20) disp.messages.shift();
            }
        } else {
            await disp.putstr_message(prompt);
        }
    }
    ynTrace('prompt', prompt.trimEnd(), `choices=${choices || ''}`, `def=${def || 0}`);
    debugRepaint('yn', 'input.ynFunction.prompt', {
        hp: repaintHp(game?.player),
        topl: repaintToplineState(disp),
        def: def || 0,
        query: query || '',
    }, {
        top: disp?.topMessage || null,
        messageNeedsMore: disp?.messageNeedsMore,
    });
    logRepaint('yn', {
        hp: repaintHp(game?.player),
        topl: repaintToplineState(disp),
        def: def || 0,
        query: query || '',
    });
 
    // C behavior: when choices is null, preserve raw keycase.
    // When choices is provided and all-lowercase, normalize key to lowercase.
    const preserveCase = !choices || /[A-Z]/.test(choices);
    const readPromptKey = async () => await nhgetch_prompt();
    const finalizeYnReturn = (value) => {
        // C ref: topl.c tty_yn_function clean_up: ttyDisplay->toplin = TOPLINE_NON_EMPTY
        if (disp) disp.toplin = TOPLINE_NON_EMPTY;
        return value;
    };
 
    enterModal('yn');
    try {
    while (true) {
        const ch = await readPromptKey();
        ynTrace('key', ch, Number.isFinite(ch) ? String.fromCharCode(ch) : String(ch));
        // C quitchars handling for yn prompts: Space/CR/LF use default.
        if ((ch === 32 || ch === 13 || ch === 10) && def && def !== '\0' && def !== 0) {
            ynTrace('return=default', def, String.fromCharCode(def));
            if (disp) pushRngLogEntry('^toplin[tty_yn_function=2]');
            return finalizeYnReturn(def);
        }
        // ESC returns 'q' or 'n' or default
        if (ch === 27) {
            if (choices && choices.includes('q')) {
                if (disp) pushRngLogEntry('^toplin[tty_yn_function=2]');
                return finalizeYnReturn('q'.charCodeAt(0));
            }
            if (choices && choices.includes('n')) {
                if (disp) pushRngLogEntry('^toplin[tty_yn_function=2]');
                return finalizeYnReturn('n'.charCodeAt(0));
            }
            if (def && def !== '\0' && def !== 0) {
                if (disp) pushRngLogEntry('^toplin[tty_yn_function=2]');
                return finalizeYnReturn(def);
            }
            if (disp) pushRngLogEntry('^toplin[tty_yn_function=2]');
            return finalizeYnReturn(27);
        }
        // Check if this is a valid choice
        let c = String.fromCharCode(ch);
        if (!preserveCase) c = c.toLowerCase();
        if (!choices || choices.includes(c)) {
            ynTrace('return=choice', c);
            if (disp) pushRngLogEntry('^toplin[tty_yn_function=2]');
            return finalizeYnReturn(c.charCodeAt(0));
        }
        ynTrace('reject', c);
    }
    } finally {
        exitModal('yn');
    }
}
 
// C: y_n(query) — simple yes/no prompt
export async function y_n(query) {
    if (game.program_state.gameover) return 'n';
    const ch = await ynFunction(query, 'yn', 'n');
    return typeof ch === 'number' ? String.fromCharCode(ch) : ch;
}
 
// Gather typed digits into a number; return the next non-digit
// C ref: cmd.c:4851 get_count()
// Returns: { count: number, key: number }
export async function getCount(firstKey, maxCount, display) {
    const runtimeDisplay = getRuntimeDisplay();
    const disp = display || runtimeDisplay;
    let cnt = 0;
    let key = firstKey || 0;
    let backspaced = false;
    let showzero = true;
    const LARGEST_INT = 32767; // C ref: global.h:133 LARGEST_INT (2^15 - 1)
    const MAX_COUNT = maxCount || LARGEST_INT;
    const ERASE_CHAR = 127; // DEL
 
    // If first key is provided and it's a digit, use it
    if (key && isDigit(key)) {
        cnt = key - 48; // '0' = 48
        key = 0; // Clear so we read next key
    }
 
    const readPromptKey = async () => await nhgetch();
 
    while (true) {
        // If we don't have a key yet, read one
        if (!key) {
            key = await readPromptKey();
        }
 
        if (isDigit(key)) {
            const digit = key - 48;
            // cnt = (10 * cnt) + digit
            cnt = (cnt * 10) + digit;
            if (cnt < 0) {
                cnt = 0;
            } else if (cnt > MAX_COUNT) {
                cnt = MAX_COUNT;
            }
            showzero = (key === 48); // '0'
            key = 0; // Read next key
        } else if (key === 8 || key === ERASE_CHAR) { // Backspace
            if (!cnt) {
                break; // No count entered, just cancel
            }
            showzero = false;
            cnt = Math.floor(cnt / 10);
            backspaced = true;
            key = 0; // Read next key
        } else if (key === 27) { // ESC
            cnt = 0;
            break;
        } else {
            // Non-digit, non-backspace, non-ESC: this is the command key
            break;
        }
 
        // Show "Count: N" when cnt > 9 or after backspace
        // C ref: cmd.c:4911 - shows count when cnt > 9 || backspaced || echoalways
        if (cnt > 9 || backspaced) {
            // C ref: cmd.c:get_count() clears WIN_MESSAGE before each
            // Count: prompt repaint.
            await tty_clear_nhwindow('message');
            if (disp) {
                if (backspaced && !cnt && !showzero) {
                    const countText = 'Count: ';
                    pushRngLogEntry('^toplin[addtopl=1]');
                    await disp.putstr_message(countText);
                    if (game) game._menuActive = true;
                    if (typeof disp.moveCursorTo === 'function') {
                        disp.moveCursorTo(countText.length, 0);
                    }
                } else {
                    const countText = `Count: ${cnt}`;
                    pushRngLogEntry('^toplin[addtopl=1]');
                    await disp.putstr_message(countText);
                    if (game) game._menuActive = true;
                    if (typeof disp.moveCursorTo === 'function') {
                        disp.moveCursorTo(countText.length, 0);
                    }
                }
            }
            backspaced = false;
        }
    }
 
    if (game) game._menuActive = false;
    return { count: cnt, key: key };
}
 
// Helper: check if character code is a digit '0'-'9'
function isDigit(ch) {
    return ch >= 48 && ch <= 57; // '0' = 48, '9' = 57
}
 
function getRuntimeDisplay() {
    return game?.nhDisplay || null;
}
 
// C ref: ynq(query) = yn_function(query, "ynq", 'q', TRUE)
export async function ynq(query) {
    const display = getRuntimeDisplay();
    return String.fromCharCode(await ynFunction(query, 'ynq', 'q'.charCodeAt(0), display));
}
 
// C ref: cmd.c:5652 paranoid_query() — y/n confirmation prompt
// When be_paranoid is true, C requires typing "yes" instead of just 'y'.
// Simplified: always uses y/n prompt (paranoid "yes" typing not implemented).
export async function paranoid_query(be_paranoid, prompt) {
    const display = getRuntimeDisplay();
    const ans = await ynFunction(prompt, 'yn', 'n'.charCodeAt(0), display);
    return ans === 'y'.charCodeAt(0);
}
 
export function init_input_globals() {
    game.cmdqInputModeDoAgain = false;
    game.cmdqRepeatRecordMode = false;
    game.startupPromptPhase = false;
    _cmdQueues[CQ_CANNED] = null;
    _cmdQueues[CQ_REPEAT] = null;
}