All files / js pager.js

92% Statements 863/938
70.93% Branches 266/375
79.31% Functions 23/29
92% Lines 863/938

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 93973x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 31x 31x 31x 31x 31x 240x 240x 29x 31x 73x 73x 73x 30x 30x 30x 30x 30x 30x 25x 13x 13x 13x 13x 13x 4x 4x 3x 3x             30x 73x 73x 73x 73x 73x 56x 56x 56x 56x 56x 56x 56x 56x 56x 56x 56x 73x 73x 73x 73x 73x 878x 878x 878x 878x 878x 878x 54x 54x 824x 824x 824x 878x 42x 42x 782x 782x 782x 878x 14x 14x 768x 768x 768x 878x 2x 2x 766x 766x 766x 878x 73x 73x 44x 44x 44x 44x 44x 44x 44x 39x 44x 44x 44x 44x 44x 44x 44x 44x 44x 44x 44x 44x 44x 44x 44x 44x 73x 73x 73x 18x 18x 18x 18x 18x 18x 18x     18x 18x   18x 73x 73x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 73x 73x 766x 766x 766x 766x 766x 766x 766x 766x 751x 751x 766x 17x 17x 17x 17x 16x 11x 11x 734x 734x 734x 734x 766x 377x 377x 766x 766x 766x 728x 699x 698x 698x 398x 166x 35x 35x 32x 31x 6x 6x 157x         107x 106x 106x 106x 72x 65x 65x 65x 4x     766x 73x 73x 73x 73x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 13x 31x 18x 18x 31x 31x 31x 31x 31x 31x 31x     31x 31x 31x 31x 31x 31x 31x 31x 31x 31x       31x 2x 2x 2x 2x 2x 31x 31x 31x 31x 31x 1860x 1860x 1860x 1829x 1860x 1860x 5x 5x 5x 5x 5x 5x             5x 1860x 31x 31x 31x 31x 31x 527x 527x 527x 527x 527x 10x 10x 10x 10x 10x 10x 10x 10x             10x 527x 31x 31x 31x 31x 3255x 3255x 2356x 2356x 3255x 3255x 74x 74x 56x 56x 56x 74x 16x 74x 2x 40x 38x 38x 56x 74x 74x 74x 74x 74x 14x 14x 14x 74x 42x 39x 39x 39x 42x 74x 3255x 31x 31x 1x 1x 31x 31x 31x 28x 28x 28x 28x 28x 28x 28x 31x 31x 31x 73x 73x 8x 8x 8x 8x 8x 8x 8x 8x 8x 73x 73x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 20x 20x 20x 114x 114x 6x 6x 20x 2x 2x 2x 2x 4x 4x 4x 4x 76x 3896x 3896x 3896x 3896x 3896x 1948x 1948x 2x 2x 2x 2x 2x 1948x 1946x 1946x 4x 4x 2x 2x 2x 4x 1946x 1948x 1948x 1948x 1948x 1948x 1948x 1948x 4x 4x 4x 1948x 3896x 3896x 8x 4x 4x 4x 4x 2x 2x 2x 2x 4x 4x 4x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 3896x 76x 4x 4x 4x 4x 4x     4x 4x 73x 73x 2x 2x 2x 2x 2x 38x 1948x 1948x 1948x 38x 2x 2x     2x 2x 2x 2x 73x 73x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 38x 1948x 1948x 1948x 1948x 2x 1948x 1948x 1948x 1948x 1948x 1948x 1948x 1948x 1948x 1948x 2x 1948x     2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 38x 2x 2x 2x 2x 2x     2x 2x 73x 73x 73x 73x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x     7x 1x 7x 6x 6x 6x 7x 7x 7x 7x 73x 73x 73x 73x 7x 7x 7x 7x 7x 7x 7x 7x 7x           7x 7x 7x 7x 7x 3x     3x 3x 4x 4x 4x 7x 2x 2x 2x 2x 2x 2x 4x 7x 2x 2x 2x 2x 33x 33x 33x 33x 33x         33x 33x 2x 2x 99x 2x 2x 99x 97x 97x 99x 2x 2x 33x 33x 2x 2x 2x 2x 2x 2x 2x 2x 2x 33x 33x 2x 2x 2x 2x 2x 2x 2x 7x 73x 73x 73x 73x 73x 73x 73x 73x 73x 20x 20x 20x 20x 20x 20x 20x 20x 20x 7x 20x 13x 13x 13x 13x 13x 13x 13x 13x 13x 13x 13x 13x 13x 13x 13x 13x 13x 13x 13x 20x 20x 20x 20x 20x 20x 1x 20x 20x 9x 9x 9x 9x 9x 20x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 20x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x           20x 1x 1x 20x 1x 1x 20x 1x 1x 20x 1x 1x 20x 1x 1x 20x 1x 1x 20x 1x 1x 20x 1x 1x 20x 20x 9x 9x 20x 20x 9x 9x 20x 14x 14x 2x 14x 12x 12x 14x 14x 14x 10x 14x 10x 14x 14x 14x 14x 14x 14x 10x 10x 10x 10x 10x 10x 10x 5x 5x 5x 5x 12x     20x 20x 20x 20x 8x 20x 73x 73x 73x 13x 13x 13x 73x 73x 73x 7x 7x 73x 73x 73x           73x 73x 73x 73x 21x 21x 73x 73x 73x     73x 73x 73x     73x 73x 73x     73x 73x 73x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 1x 1x 1x 1x 1x 1x 1x 2x 2x 73x 73x 73x     73x 73x 73x      
// pager.js — Object/monster description pager (port of pager.c)
// Handles '/' look command, object descriptions, and data file lookups.
 
import { game } from './gstate.js';
import { pline, custompline } from './pline.js';
import { Blind, Hallucination, Is_waterlevel, Role_if, Underwater, Upolyd } from './macros.js';
import { hliquid, pmname } from './do_name.js';
import { PICK_ONE, PICK_NONE, STONE, VWALL, HWALL, TREE, IRONBARS, DOOR, CORR, ROOM, STAIRS, LADDER, FOUNTAIN, THRONE, SINK, GRAVE, ALTAR, DRAWBRIDGE_DOWN, DRAWBRIDGE_UP, AIR, CLOUD, SCORR, SDOOR, D_BROKEN, D_CLOSED, D_ISOPEN, D_LOCKED, D_TRAPPED, ICE, Is_juiblex_level, IS_WATERWALL, LAVAPOOL, LAVAWALL, MOAT, POOL, BOLT_LIM, COLNO, ROWNO, SYM_OFF_P } from './const.js';
import { add_menu, add_menu_str, create_nhwindow_menu, destroy_nhwindow_menu, end_menu, select_menu, start_menu, NHW_TEXT } from './menu.js';
import { getpos } from './getpos.js';
import { coord_desc } from './getpos.js';
import { mons, PM_SAMURAI, S_invisible } from './monsters.js';
import { gs, SYM_OFF_M, SYM_OFF_O } from './drawing.js';
import { glyph_is_cmap, glyph_to_cmap } from './glyph.js';
import { rendered_ch, rendered_decgfx } from './display.js';
import { m_at, obj_at, t_at } from './map_access.js';
import { defsyms, def_monsyms, def_oc_syms, S_upstair, S_dnstair, S_brupstair, S_brdnstair, S_upladder, S_dnladder, S_vodbridge, S_hcdbridge, S_sw_tl, S_sw_br } from './symbols.js';
import { objectData, VENOM_CLASS, STATUE } from './objects.js';
import { an, xname, singular, doname_with_price } from './objnam.js';
import { display_inventory } from './invent.js';
import { getlin, y_n, ynFunction } from './input.js';
import { encyclopediaLookup } from './encyclopedia.js';
import { engr_at } from './engrave.js';
 
// C ref: pager.c:62 is_swallow_sym
export function is_swallow_sym(c) {
    const code = (typeof c === 'number')
        ? c
        : (typeof c === 'string' && c.length > 0 ? c.charCodeAt(0) : NaN);
    if (!Number.isFinite(code)) return false;
    for (let i = S_sw_tl; i <= S_sw_br; i++) {
        if ((gs.showsyms[i] ?? -1) === code) return true;
    }
    return false;
}
 
// C ref: pager.c:561 waterbody_name()
export function waterbody_name(x, y) {
    const loc = game.level?.at?.(x, y);
    const ltyp = loc?.typ;
    const hallucinate = !!Hallucination();
 
    if (ltyp === LAVAPOOL) return `molten ${hliquid('lava')}`;
    if (ltyp === ICE) return hallucinate ? `frozen ${hliquid('water')}` : 'ice';
    if (ltyp === POOL) return `pool of ${hliquid('water')}`;
    if (ltyp === MOAT) {
        if (hallucinate) return `deep ${hliquid('water')}`;
        const uz = game.u?.uz;
        const medusa = game.medusa_level;
        if (uz && medusa && uz.dnum === medusa.dnum && uz.dlevel === medusa.dlevel) return 'shallow sea';
        if (Is_juiblex_level(uz)) return 'swamp';
        if (Role_if(PM_SAMURAI)) return 'pond';
        return 'moat';
    }
    if (IS_WATERWALL(ltyp)) {
        if (Is_waterlevel(game.u?.uz)) return 'limitless water';
        return `wall of ${hliquid('water')}`;
    }
    if (ltyp === LAVAWALL) return `wall of ${hliquid('lava')}`;
    return 'water';
}
 
// C ref: pager.c:76 def_monsyms — stub
// Monster symbol data is in symbols.js
 
// C ref: pager.c:108 self_lookat
function self_lookat() {
    const g = game;
    const race = g.urace?.adj || 'human';
    // C ref: do_name.c pmname(pm, Mgender) uses gender-specific name.
    // For the hero in normal form, use the role's gendered name.
    const roleName = (!Upolyd() && g.flags?.female && g.urole?.name?.f)
        ? g.urole.name.f.toLowerCase()
        : (g.urole?.name?.m || pmname(mons[g.u.umonnum])).toLowerCase();
    const name = g.plname || 'player';
    return `${race} ${roleName} called ${name}`;
}
 
// C ref: pager.c:656 lookat
// Returns a description string for what's visible at (x, y).
// Simplified port: reads game.level, m_at, obj_at directly.
export function lookat(x, y) {
    const g = game;
    const loc = g.level?.at(x, y);
    if (!loc) return 'unexplored area';
 
    // Check if hero is at this position
    if (g.u && x === g.u.ux && y === g.u.uy) {
        return self_lookat();
    }
 
    // Monster at this position?
    const mtmp = m_at(x, y);
    if (mtmp) {
        return look_at_monster_desc(mtmp, x, y);
    }
 
    // Object at this position?
    const otmp = obj_at(x, y);
    if (otmp) {
        return look_at_object_desc(otmp);
    }
 
    // Trap at this position?
    const trap = t_at(x, y);
    if (trap && loc.seenv) {
        return trap_desc(trap);
    }
 
    // Terrain / dungeon feature
    return look_at_terrain_desc(loc, x, y);
}
 
// Describe a monster — simplified port of C's look_at_monster()
function look_at_monster_desc(mtmp, x, y) {
    const mdata = mtmp.data || (mtmp.mnum >= 0 ? mons[mtmp.mnum] : null);
    if (!mdata) return 'a monster';
 
    let name = mdata.mname || 'creature';
    let prefix = '';
    if (mtmp.mtame) prefix = 'tame ';
    else if (mtmp.mpeaceful) prefix = 'peaceful ';
 
    let desc = prefix + name;
 
    // C ref: monhealthdescr is currently disabled (#if 0) in C source
    // so no health status shown
 
    // Status effects — match C's look_at_monster
    if (mtmp.mfrozen)
        desc += ", can't move (paralyzed or sleeping or busy)";
    else if (mtmp.msleeping)
        desc += ', asleep';
 
    if (mtmp.mleashed) desc += ', leashed to you';
 
    return desc;
}
 
// Describe an object — C ref: pager.c look_at_object()
// Uses doname which respects identification state (won't spoil unknown items).
function look_at_object_desc(otmp) {
    const name = doname_with_price(otmp);
    if (name) {
        if (otmp?.otyp === STATUE
            && Number.isInteger(otmp?.corpsenm)
            && mons?.[otmp.corpsenm]?.mname
            && !/statue of /i.test(name)) {
            return `${name} of ${an(mons[otmp.corpsenm].mname)}`;
        }
        return name;
    }
    return 'an object';
}
 
// Describe a trap
function trap_desc(trap) {
    const trapNames = [
        '', 'arrow trap', 'dart trap', 'falling rock trap',
        'squeaky board', 'bear trap', 'land mine', 'rolling boulder trap',
        'sleeping gas trap', 'rust trap', 'fire trap', 'pit',
        'spiked pit', 'hole', 'trap door', 'teleportation trap',
        'level teleporter', 'magic portal', 'web', 'statue trap',
        'magic trap', 'anti-magic field', 'polymorph trap', 'vibrating square',
    ];
    const ttyp = trap.ttyp || 0;
    return trapNames[ttyp] || 'a trap';
}
 
// Describe terrain — simplified port of C's cmap switch in lookat()
function look_at_terrain_desc(loc, x, y) {
    const typ = loc.typ;
    if (typ === undefined || typ === null) return 'unexplored area';
 
    // Water types (use waterbody_name for context-sensitive description)
    if (typ === POOL || typ === MOAT || typ === LAVAPOOL ||
        IS_WATERWALL(typ) || typ === LAVAWALL || typ === ICE)
        return waterbody_name(x, y);
 
    // Door — check doormask for state
    if (typ === DOOR) {
        const mask = (loc.doormask || 0) & ~D_TRAPPED;
        if (mask === D_BROKEN) return 'broken door';
        if (mask & D_ISOPEN) return 'open door';
        if (mask & D_CLOSED) return 'closed door';
        if (mask & D_LOCKED) return 'locked door';
        return 'doorway';
    }
 
    // C ref: lookat() checks the display glyph, not raw terrain type.
    // If the cell hasn't been explored (seenv=0), the hero doesn't know
    // what's there. Return "unexplored area" regardless of actual typ.
    if (!loc.seenv) return 'unexplored area';
 
    // Named terrain types using actual constants
    if (typ === STONE) return 'stone';
    if (typ >= VWALL && typ <= HWALL + 9) return 'wall'; // VWALL..TRWALL
    if (typ === SDOOR) return !loc.seenv ? 'unexplored area' : 'wall'; // secret door looks like wall
    if (typ === SCORR) return 'stone'; // secret corridor looks like stone
    if (typ === TREE) return 'tree';
    if (typ === IRONBARS) return 'iron bars';
    if (typ === CORR) return 'corridor';
    if (typ === ROOM) return 'floor of a room';
    if (typ === STAIRS) {
        const symIdx = glyph_is_cmap(loc.glyph) ? glyph_to_cmap(loc.glyph) : -1;
        if (symIdx === S_brupstair) return 'branch staircase up';
        if (symIdx === S_brdnstair) return 'branch staircase down';
        if (symIdx === S_dnstair) return 'staircase down';
        return 'staircase up';
    }
    if (typ === LADDER) {
        const symIdx = glyph_is_cmap(loc.glyph) ? glyph_to_cmap(loc.glyph) : -1;
        if (symIdx === S_dnladder) return 'ladder down';
        return 'ladder up';
    }
    if (typ === FOUNTAIN) return 'fountain';
    if (typ === THRONE) return 'opulent throne';
    if (typ === SINK) return 'sink';
    if (typ === GRAVE) return 'grave';
    if (typ === ALTAR) return 'altar';
    if (typ === DRAWBRIDGE_DOWN) return 'lowered drawbridge';
    if (typ === DRAWBRIDGE_UP) return 'raised drawbridge';
    if (typ === AIR) return 'air';
    if (typ === CLOUD) return 'cloud';

    return 'dungeon feature';
}
 
// C ref: pager.c:1246 do_screen_description
// Builds the description string for a map position.
// Returns { found, out_str, firstmatch }
function do_screen_description(cc, looked, sym) {
    const g = game;
    const loc = g.level?.at(cc.x, cc.y);
    const mon_interior = 'the interior of a monster';
    const unreconnoitered = 'unreconnoitered';
    let found = 0;
    let out_str = '';
    let firstmatch = '';
    let prefix = '';
    let need_to_look = false;
    let lookedSymCode = -1;
 
    if (looked) {
        // Get the display character at this position.
        const dispCh = rendered_ch(cc.x, cc.y) || ' ';
        const decgfx = rendered_decgfx(cc.x, cc.y);
 
        // Build prefix with glyph character — C uses encglyph + 8 spaces
        if (decgfx) {
            prefix = '\x0e' + dispCh + '\x0f' + '        ';
        } else {
            prefix = dispCh + '        ';
        }
 
        // C do_screen_description compares glyphinfo.ttychar against
        // gs.showsyms entries (which can carry DEC high-bit values).
        lookedSymCode = dispCh.charCodeAt(0);
        if (decgfx) lookedSymCode |= 0x80;
        sym = dispCh;
    } else {
        prefix = (sym || '?') + '        ';
    }
 
    // C ref: pager.c:1278-1315 swallowed/underwater restricted-vision handling.
    if (looked) {
        const ux = g.u?.ux ?? 0;
        const uy = g.u?.uy ?? 0;
        const adjacent = Math.abs(cc.x - ux) <= 1
            && Math.abs(cc.y - uy) <= 1
            && !(cc.x === ux && cc.y === uy);
        const submerged = !!(Underwater() && !Is_waterlevel(g.u?.uz));
        if ((g.u?.uswallow || submerged) && !adjacent) {
            out_str = prefix + unreconnoitered;
            firstmatch = unreconnoitered;
            return { found: 1, out_str, firstmatch };
        } else if (is_swallow_sym(lookedSymCode)) {
            out_str = prefix + mon_interior;
            firstmatch = mon_interior;
            found = 1;
            need_to_look = true;
        }
    }
 
    // Check monster symbol classes
    // C ref: pager.c:1329-1345
    for (let i = 1; i < def_monsyms.length; i++) {
        const ms = def_monsyms[i];
        if (!ms.explain) continue;
        if (i === S_invisible) continue; // C: skip S_invisible
        const monShowCode = gs.showsyms[i + SYM_OFF_M];
        const monMatch = looked ? (lookedSymCode === monShowCode) : (sym === ms.sym);
        if (monMatch) {
            need_to_look = true;
            if (!found) {
                out_str = prefix + an(ms.explain);
                firstmatch = ms.explain;
                found = 1;
            } else {
                const addition = an(ms.explain);
                if (!out_str.includes(addition)) {
                    out_str += ' or ' + addition;
                    found++;
                }
            }
        }
    }
 
    // Check object symbol classes
    // C ref: pager.c:1357-1403
    let skipped_venom = 0;
    for (let i = 1; i < def_oc_syms.length; i++) {
        const oc = def_oc_syms[i];
        if (!oc.explain) continue;
        const ocShowCode = gs.showsyms[i + SYM_OFF_O];
        const ocMatch = looked ? (lookedSymCode === ocShowCode) : (sym === oc.sym);
        if (ocMatch) {
            // C ref: pager.c:1387 — skip venom when looked
            if (looked && i === VENOM_CLASS) { skipped_venom++; continue; }
            need_to_look = true;
            if (!found) {
                out_str = prefix + an(oc.explain);
                firstmatch = oc.explain;
                found = 1;
            } else {
                const addition = an(oc.explain);
                if (!out_str.includes(addition)) {
                    out_str += ' or ' + addition;
                    found++;
                }
            }
        }
    }
 
    // Check dungeon feature symbols (defsyms/cmap)
    // C ref: pager.c:1445-1508 — iterate over MAXPCHARS defsyms entries
    for (let i = 0; i < defsyms.length; i++) {
        const ds = defsyms[i];
        if (!ds || !ds.desc) continue;
        // When looked=true, C compares sym against gs.showsyms[i] (SYM_OFF_P + i)
        const pcharShowCode = gs.showsyms[i + SYM_OFF_P];
        const pcharMatch = looked ? (lookedSymCode === pcharShowCode) : (sym === ds.ch);
        if (pcharMatch) {
            // C ref: pager.c:1230 — skip drawbridge when found >= 3
            if (found >= 3 && i >= S_vodbridge && i <= S_hcdbridge) continue;
            const x_str = ds.desc;
            // Determine article: "the" for "X of a room", none for "stone"/"air"/"land", "a/an" otherwise
            let article = 0;
            if (x_str.includes(' of a room')) {
                article = 2; // "the"
            } else if (x_str === 'stone' || x_str === 'air' || x_str === 'land') {
                article = 0; // no article
            } else {
                article = 1; // "a/an"
            }
 
            const desc = article === 2 ? ('the ' + x_str)
                       : article === 1 ? an(x_str)
                       : x_str;
 
            if (!found) {
                out_str = prefix + desc;
                firstmatch = x_str;
                found = 1;
            } else {
                if (!out_str.includes(x_str)) {
                    out_str += ' or ' + desc;
                    found++;
                }
            }
        }
    }
 
    if (found > 4) {
        out_str = prefix + 'can be many things';
    }
 
    // If we're looking at the screen, add the specific lookat() description
    if (looked && (found > 1 || need_to_look)) {
        const lookDesc = lookat(cc.x, cc.y);
        if (lookDesc) {
            firstmatch = lookDesc;
            out_str += ` (${lookDesc})`;
            found = 1; // signal single match for checkfile
        }
    }
 
    return { found, out_str, firstmatch };
}
 
// C ref: pager.c:1965 look_region_nearby
function look_region_nearby(nearby) {
    const u = game.u;
    return {
        lo_x: nearby ? Math.max(u.ux - BOLT_LIM, 1) : 1,
        lo_y: nearby ? Math.max(u.uy - BOLT_LIM, 0) : 0,
        hi_x: nearby ? Math.min(u.ux + BOLT_LIM, COLNO - 1) : COLNO - 1,
        hi_y: nearby ? Math.min(u.uy + BOLT_LIM, ROWNO - 1) : ROWNO - 1,
    };
}
 
// C ref: pager.c:1978 look_all
async function look_all(nearby, do_mons) {
    const g = game;
    const u = g.u;
    const { lo_x, lo_y, hi_x, hi_y } = look_region_nearby(nearby);
    const win = create_nhwindow_menu(NHW_TEXT);
    // C ref: NHW_TEXT forces fullscreen (offx=0, maxcol=cols)
    win.forceOffx0 = true;
    let count = 0;
    const shownChAt = (x, y) => rendered_ch(x, y);
    const isShownObjectGlyph = (x, y, otmp = null) => {
        const ch = shownChAt(x, y);
        if (!ch || ch === ' ') return false;
        for (let i = 1; i < def_oc_syms.length; i++) {
            if (def_oc_syms[i]?.sym === ch) return true;
        }
        // C look_all uses glyph_at()+glyph_is_object. Statues can be rendered
        // with their monster symbol while still being object glyphs.
        if (otmp?.otyp === STATUE && Number.isInteger(otmp?.corpsenm)) {
            const mlet = mons?.[otmp.corpsenm]?.mlet;
            const monSym = (mlet != null) ? def_monsyms[mlet]?.sym : '';
            if (monSym && ch === monSym) return true;
        }
        return false;
    };
 
    for (let y = lo_y; y <= hi_y; y++) {
        for (let x = lo_x; x <= hi_x; x++) {
            let lookbuf = '';
            const loc = g.level?.at(x, y);
            if (!loc) continue;
 
            if (do_mons) {
                // Check for monster at this location
                if (x === u.ux && y === u.uy) {
                    // C uses glyph_at()+glyph_is_monster() for "currently shown".
                    if (shownChAt(x, y) === '@') {
                        lookbuf = self_lookat();
                        count++;
                    }
                } else {
                    const mtmp = m_at(x, y);
                    if (mtmp) {
                        const shownSym = def_monsyms[mtmp?.data?.mlet]?.sym || '';
                        if (shownSym && shownChAt(x, y) === shownSym) {
                            lookbuf = look_at_monster_desc(mtmp, x, y);
                            count++;
                        }
                    }
                }
            } else {
                // Check for object at this location
                const otmp = obj_at(x, y);
                // C ref: pager.c look_all() uses glyph_at()+glyph_is_object(),
                // so include only objects currently shown on map, not every
                // object present in state at that coordinate.
                if (otmp && isShownObjectGlyph(x, y, otmp)) {
                    lookbuf = look_at_object_desc(otmp);
                    count++;
                }
            }
 
            if (lookbuf) {
                if (count === 1) {
                    const which = do_mons ? 'monsters' : 'objects';
                    const coordStr = coord_desc(u.ux, u.uy);
                    let header;
                    if (nearby) {
                        header = `${which.charAt(0).toUpperCase() + which.slice(1)} currently shown near ${coordStr}:`;
                    } else {
                        header = `All ${which} currently shown on the map:`;
                    }
                    add_menu_str(win, header);
                    add_menu_str(win, '    '); // separator
                }
 
                const coordStr = coord_desc(x, y);
                // Right-justify coord to 8 chars, add trailing space if y < 10
                let coordPad = coordStr;
                if (y < 10) coordPad += ' ';
                coordPad = coordPad.padStart(8);
 
                // Get the display character for this position
                const dispCh = rendered_ch(x, y) || '?';
                const outLine = `${coordPad}  ${dispCh}  ${lookbuf}`;
                add_menu_str(win, outLine);
            }
        }
    }
 
    if (count) {
        end_menu(win, null);
        await select_menu(win, PICK_NONE);
    } else {
        await pline(`No ${do_mons ? 'monsters' : 'objects'} are currently shown ${nearby ? 'nearby' : 'on the map'}.`);
    }
    destroy_nhwindow_menu(win);
}
 
// C ref: pager.c:2077 look_traps
async function look_traps(nearby) {
    const { lo_x, lo_y, hi_x, hi_y } = look_region_nearby(nearby);
    let count = 0;
 
    for (let y = lo_y; y <= hi_y; y++) {
        for (let x = lo_x; x <= hi_x; x++) {
            const trap = t_at(x, y);
            if (trap && trap.tseen) count++;
        }
    }
 
    if (count) {
        // TODO: full trap listing in text window
        await pline(`No traps seen or remembered${nearby ? ' nearby' : ''}.`);
    } else {
        await pline(`No traps seen or remembered${nearby ? ' nearby' : ''}.`);
    }
}
 
// C ref: pager.c:2143 look_engrs
async function look_engrs(nearby) {
    const g = game;
    const { lo_x, lo_y, hi_x, hi_y } = look_region_nearby(nearby);
    const win = create_nhwindow_menu(NHW_TEXT);
    win.forceOffx0 = true;
    let count = 0;
 
    const quoteEngr = (txt = '') => {
        const clean = String(txt || '').replace(/\s+/g, ' ').trim();
        return clean ? `"${clean}"` : '"?"';
    };
 
    for (let y = lo_y; y <= hi_y; y++) {
        for (let x = lo_x; x <= hi_x; x++) {
            const loc = g.level?.at?.(x, y);
            if (!loc?.seenv) continue;
            const e = engr_at(x, y);
            if (!e) continue;
 
            const isHeadstone = loc?.typ === GRAVE;
            const engrSym = '`';
            const graveSym = '|';
            const shownCh = rendered_ch(x, y) || '?';
            let glyphCh = isHeadstone ? graveSym : engrSym;
            let lookbuf = isHeadstone
                ? `text: ${quoteEngr(e.text)}`
                : `remembered text: ${quoteEngr(e.text)}`;
            const shownIsEngr = shownCh === engrSym || (isHeadstone && shownCh === graveSym);
            if (!shownIsEngr) {
                lookbuf += `, obscured by ${shownCh}`;
            } else {
                glyphCh = shownCh;
            }
 
            if (count === 0) {
                const header = nearby
                    ? 'Nearby seen or remembered engravings:'
                    : 'Seen or remembered engravings on this level:';
                add_menu_str(win, header);
                add_menu_str(win, '    ');
            }
            count++;
 
            const coordStr = coord_desc(x, y).padStart(8);
            add_menu_str(win, `${coordStr}  ${glyphCh}  ${lookbuf}`);
        }
    }
 
    if (count) {
        end_menu(win, null);
        await select_menu(win, PICK_NONE);
    } else {
        await pline(`No engravings seen or remembered${nearby ? ' nearby' : ''}.`);
    }
    destroy_nhwindow_menu(win);
}
 
// C ref: pager.c:830 — strip prefixes from a name for encyclopedia lookup.
// Shared by checkfile (in-game) and the hover panel (browser UI).
export function stripForLookup(inp) {
    let s = (inp || '').toLowerCase();
    if (s.startsWith('interior of ')) s = s.slice(12);
    if (s.startsWith('a ')) s = s.slice(2);
    else if (s.startsWith('an ')) s = s.slice(3);
    else if (s.startsWith('the ')) s = s.slice(4);
    else if (s.startsWith('some ')) s = s.slice(5);
    else if (/^\d/.test(s)) s = s.replace(/^\d+\s*/, '');
    if (s.startsWith('pair of ')) s = s.slice(8);
    if (s.startsWith('tame ')) s = s.slice(5);
    else if (s.startsWith('peaceful ')) s = s.slice(9);
    if (s.startsWith('invisible ')) s = s.slice(10);
    if (s.startsWith('saddled ')) s = s.slice(8);
    if (s.startsWith('blessed ')) s = s.slice(8);
    else if (s.startsWith('uncursed ')) s = s.slice(9);
    else if (s.startsWith('cursed ')) s = s.slice(7);
    if (s.startsWith('empty ')) s = s.slice(6);
    if (s.startsWith('partly used ')) s = s.slice(12);
    else if (s.startsWith('partly eaten ')) s = s.slice(13);
    if (s.startsWith('statue of ')) s = 'statue';
    else if (s.startsWith('figurine of ')) s = 'figurine';
    const enchMatch = s.match(/^[+-]\d+\s*/);
    if (enchMatch) s = s.slice(enchMatch[0].length);
    let namedIdx = s.indexOf(' named ');
    let calledIdx = s.indexOf(' called ');
    if (namedIdx >= 0) {
        if (calledIdx >= 0 && calledIdx < namedIdx) s = s.slice(0, calledIdx);
        else s = s.slice(0, namedIdx);
    } else if (calledIdx >= 0) {
        s = s.slice(0, calledIdx);
    } else {
        const commaIdx = s.indexOf(', ');
        if (commaIdx >= 0) s = s.slice(0, commaIdx);
    }
    const parenIdx = s.indexOf(' (');
    if (parenIdx >= 0) s = s.slice(0, parenIdx);
    return s.trim();
}
 
// C ref: pager.c:830 checkfile
// Looks up a name in the encyclopedia data and optionally displays it.
// chkflags: bit 1 = user_typed_name, bit 2 = without_asking (don't prompt)
async function checkfile_impl(inp, pm, chkflags, supplemental_name) {
    const user_typed_name = !!(chkflags & 1);
    const without_asking = !!(chkflags & 2);
 
    if (!inp) return false;
 
    const dbase_str = stripForLookup(inp);
 
    if (!dbase_str) {
        if (user_typed_name) {
            await pline("You don't have any information on those things.");
        }
        return false;
    }
 
    // Look up in encyclopedia
    const text = encyclopediaLookup(dbase_str);
 
    if (!text) {
        if (user_typed_name) {
            await pline("You don't have any information on those things.");
        }
        return false;
    }
 
    // Ask "More info?" if not user_typed_name and not without_asking
    let show = false;
    if (user_typed_name || without_asking) {
        show = true;
    } else {
        const question = `More info about "${dbase_str}"?`;
        const ch = await y_n(question);
        if (ch === 'y') show = true;
    }
 
    if (show) {
        // Display the encyclopedia text — C uses NHW_MENU (pager.c:1080)
        const win = create_nhwindow_menu();
        const lines = text.split('\n');
        for (let line of lines) {
            // C ref: pager.c:1093-1101 — strip leading tab or up to 8 spaces,
            // then expand remaining tabs to spaces (pager.c:1108)
            if (line.startsWith('\t')) {
                line = line.slice(1);
            } else if (line.startsWith(' ')) {
                let skip = 0;
                while (skip < 8 && line[skip] === ' ') skip++;
                line = line.slice(skip);
            }
            // Expand any remaining internal tabs
            if (line.includes('\t')) {
                let expanded = '';
                for (let ci = 0; ci < line.length; ci++) {
                    if (line[ci] === '\t') {
                        const spaces = 8 - (expanded.length % 8);
                        expanded += ' '.repeat(spaces);
                    } else {
                        expanded += line[ci];
                    }
                }
                line = expanded;
            }
            add_menu_str(win, line);
        }
        // C ref: checkfile calls display_nhwindow(datawin, FALSE) which
        // renders with --More-- prompt, not (end).
        win.textLikePrompt = true;
        end_menu(win, null);
        // C ref: checkfile uses putstr (not add_menu) to add lines, so
        // maxcol = strlen(line) + 1 (for attr byte), not +2 from tty_end_menu.
        // Override cols after end_menu to match C's putstr width.
        let maxLen = 0;
        for (const item of win.items) {
            if (item.str.length + 1 > maxLen) maxLen = item.str.length + 1;
        }
        win.cols = maxLen;
        await select_menu(win, PICK_NONE);
        destroy_nhwindow_menu(win);
        return true;
    }
 
    return false;
}
 
// LOOK_TRADITIONAL = 0, LOOK_QUICK = 1, LOOK_ONCE = 2, LOOK_VERBOSE = 3
const LOOK_TRADITIONAL = 0;
const LOOK_QUICK = 1;
const LOOK_ONCE = 2;
const LOOK_VERBOSE = 3;
 
// C ref: pager.c:1672 do_look — async
export async function do_look(quick, clicklook) {
    const g = game;
    let i = '\0';
    let from_screen = false;
    let sym = 0;
    const cc = { x: 0, y: 0 };
    let ans = 0;
 
    if (!clicklook) {
        if (quick) {
            i = 'y';
        } else {
            const win = create_nhwindow_menu();
            start_menu(win);
            add_menu(win, '/', '/', 0, 0, 8, 'something on the map', 0);
            add_menu(win, 'i', 'i', 0, 0, 8, "something you're carrying", 0);
            add_menu(win, '?', '?', 0, 0, 8, 'something else (by symbol or name)', 0);
            add_menu(win, null, 0, 0, 0, 8, '', 0);
            add_menu(win, 'm', 'm', 0, 0, 8, 'nearby monsters', 0);
            add_menu(win, 'M', 'M', 0, 0, 8, 'all monsters shown on map', 0);
            add_menu(win, 'o', 'o', 0, 0, 8, 'nearby objects', 0);
            add_menu(win, 'O', 'O', 0, 0, 8, 'all objects shown on map', 0);
            add_menu(win, 't', 't', 0, 0, 8, 'nearby traps', 0);
            add_menu(win, 'T', 'T', 0, 0, 8, 'all seen or remembered traps', 0);
            add_menu(win, 'e', 'e', 0, 0, 8, 'nearby engravings', 0);
            add_menu(win, 'E', 'E', 0, 0, 8, 'all seen or remembered engravings', 0);
            end_menu(win, 'What do you want to look at:');
            const picked = await select_menu(win, PICK_ONE);
            destroy_nhwindow_menu(win);
            i = picked?.items?.[0]?.identifier || '';
        }
 
        // C ref: pager.c:1812 dowhatiscmd switch
        switch (i) {
        default:
        case 'q':
        case '':
            return 0;
        case 'y':
        case '/':
            from_screen = true;
            sym = 0;
            cc.x = g.u?.ux || 1;
            cc.y = g.u?.uy || 1;
            break;
        case 'i': {
            // Display inventory and look up selected item
            const invlet = await display_inventory(null, true);
            if (!invlet || invlet === '\x1b' || invlet === '\0') return 0;
            let out_str = '';
            for (let otmp = g.invent; otmp; otmp = otmp.nobj) {
                if (otmp.invlet === invlet) {
                    out_str = singular(otmp, xname);
                    break;
                }
            }
            if (out_str) {
                await checkfile_impl(out_str, null, 1 | 2, null); // user_typed + without_asking
            }
            return 0;
        }
        case '?': {
            // Get text input from user
            const out_str = await getlin('Specify what? (type the word)', null, { logHookedLifecycle: true });
            if (!out_str || out_str === '\x1b' || out_str === '') return 0;
            const trimmed = out_str.trim();
            if (!trimmed) return 0;
            if (trimmed.length > 1) {
                // User typed a complete string — look up directly
                await checkfile_impl(trimmed, null, 1 | 2, null); // user_typed + without_asking
                return 0;
            }
            // Single character — treat as symbol
            sym = trimmed;
            from_screen = false;
            break;
        }
        case 'm':
            await look_all(true, true);
            return 0;
        case 'M':
            await look_all(false, true);
            return 0;
        case 'o':
            await look_all(true, false);
            return 0;
        case 'O':
            await look_all(false, false);
            return 0;
        case 't':
            await look_traps(true);
            return 0;
        case 'T':
            await look_traps(false);
            return 0;
        case 'e':
            await look_engrs(true);
            return 0;
        case 'E':
            await look_engrs(false);
            return 0;
        }
    }
 
    // Save verbose flag — C ref: pager.c:1891
    const save_verbose = g.flags?.verbose;
    if (g.flags) g.flags.verbose = g.flags.verbose && !quick;
 
    // C ref: pager.c:1897 — cursor-based map looking loop
    do {
        if (from_screen) {
            if (g.flags?.verbose) {
                await pline('Please move the cursor to a monster, object or location.');
            } else {
                await pline('Pick a monster, object or location.');
            }
 
            ans = await getpos(cc, !!quick, 'a monster, object or location');
            if (ans < 0 || cc.x < 0) break;
            if (g.flags) g.flags.verbose = false;
        }
 
        const result = do_screen_description(cc, from_screen || clicklook, sym);
        const found = result.found;
        const out_str = result.out_str;
        const firstmatch = result.firstmatch;
 
        if (found) {
            // C uses putmixed(WIN_MESSAGE, 0, out_str) which goes through
            // the message window — equivalent to custompline in JS
            await custompline(out_str);
 
            // Check for encyclopedia entry
            if (found === 1 && ans !== LOOK_QUICK && ans !== LOOK_ONCE
                && (ans === LOOK_VERBOSE || (g.flags?.help && !quick))) {
                await checkfile_impl(firstmatch, null,
                    ans === LOOK_VERBOSE ? 2 : 0, // without_asking if LOOK_VERBOSE
                    null);
            }
        } else {
            await pline("I've never heard of such things.");
        }
    } while (from_screen && !quick && ans !== LOOK_ONCE && !clicklook);
 
    // Restore verbose flag
    if (g.flags) g.flags.verbose = save_verbose;
    return 0;
}
 
// C ref: pager.c:900 dowhatis — async
export async function dowhatis() {
    // Main '/' command handler
    return await do_look(false, false);
}
 
// C ref: pager.c:910 doquickwhatis — async
export async function doquickwhatis() {
    return await do_look(true, false);
}
 
// C ref: pager.c:1200 checkfile — async (public interface)
export async function checkfile(inp, pm, user_typed_name, without_asking, supplemental_name) {
    let flags = 0;
    if (user_typed_name) flags |= 1;
    if (without_asking) flags |= 2;
    return await checkfile_impl(inp, pm, flags, supplemental_name);
}
 
// C ref: pager.c:1600 do_screen_description — async (public interface)
// Kept for external callers; internal use is the synchronous version above.
export async function do_screen_description_async(cc, looked, sym, out_str, firstmatch, for_supplement) {
    return do_screen_description(cc, looked, sym);
}
 
// C ref: pager.c:2000 look_at_monster — async
export async function look_at_monster(buf, monbuf, mtmp, x, y) {
    // Describe a monster
}
 
// C ref: pager.c:2500 look_at_object — async
export async function look_at_object(buf, x, y, nb) {
    // Describe an object
}
 
// C ref: pager.c:2700 whatdoes_usage
export function whatdoes_usage() {
    return 'Usage: whatdoes <command>';
}
 
// C ref: pager.c:2800 dowhatdoes — async
export async function dowhatdoes() {
    if (!game._dowhatdoes_once) {
        await pline("Ask about '&' or '?' to get more info.");
        game._dowhatdoes_once = true;
    }
 
    const qCode = await ynFunction('What command?', null, '\0', null);
    const q = String.fromCharCode((qCode || 0) & 0xff);
 
    // Minimal key-description map used by current parity sessions.
    // C ref: pager.c dowhatdoes_core() key2extcmddesc().
    const keyDesc = {
        i: 'show your inventory (#inventory).',
    };
 
    if (Object.prototype.hasOwnProperty.call(keyDesc, q)) {
        await pline(`${q.padEnd(8)}${keyDesc[q]}`);
    } else {
        const code = (qCode || 0) & 0xff;
        const oct = code.toString(8).padStart(3, '0');
        const hex = code.toString(16).padStart(2, '0');
        await pline(`No such command '${q}', char code ${code} (0${oct} or 0x${hex}).`);
    }
    return 0;
}
 
// C ref: pager.c:2900 docontact — async
export async function docontact() {
    return 0;
}
 
// C ref: pager.c ia_checkfile — check if item has data file entry
export function ia_checkfile(otmp) {
    return false;
}