// Sierra-style mini-adventure built on JoeyLib.
//
// What this demonstrates:
//   - Multi-room scene management (two rooms, edge-exit transitions)
//   - Procedural 320x200x16 backgrounds drawn from JoeyLib primitives
//     (no external art files) -- portable across all four platforms
//   - SCB per-line palettes for the sky gradient (showcases the
//     library's IIgs-style SCB model on Amiga/ST copper-emulated)
//   - 4-direction animated ego sprite with sprite save/restore
//   - SCI-style verb cursor (WALK / LOOK / TAKE / USE / TALK),
//     right-click to cycle verb, left-click to act
//   - Hotspots with per-verb response strings
//   - Inventory bar with item pickup and item-on-hotspot use
//   - Grid pathfinding over a per-room walk-mask
//   - Sierra priority illusion: foreground props redraw on top of the
//     ego whenever their bottom-Y is greater than the ego's bottom-Y,
//     so the character "walks behind" tall objects
//   - Message bar for narration / hotspot text
//
// Controls:
//   Mouse        - left-click to act with the current verb on the
//                  thing under the cursor (or walk if no hotspot)
//   Right-click  - cycle verb (WALK / LOOK / TAKE / USE / TALK)
//   Space        - cycle verb (one-button-mouse fallback for IIgs)
//   1..5         - jump straight to a verb
//   I            - toggle "look at inventory" cursor mode
//   Click item   - select item; cursor becomes the item icon. Next
//                  left-click on a hotspot triggers the item-on-target
//                  handler; right-click clears item selection.
//   ESC          - quit
//
// This is intentionally one file: every system (art, engine, rooms,
// pathfinding, UI) lives below as a labelled section so a reader can
// follow the whole engine without chasing headers.

#include <stdio.h>
#include <string.h>

#include <joey/joey.h>


/* ===================================================================
 * Section 1 -- Layout, constants, types
 * =================================================================== */

#define MSG_BAR_H        12
#define INV_BAR_H        20
#define PLAY_AREA_H      (SURFACE_HEIGHT - MSG_BAR_H - INV_BAR_H)  // 168
#define MSG_BAR_Y        PLAY_AREA_H                               // 168
#define INV_BAR_Y        (PLAY_AREA_H + MSG_BAR_H)                 // 180

#define WALK_CELL_PX     4
#define WALK_GRID_W      (SURFACE_WIDTH  / WALK_CELL_PX)           // 80
#define WALK_GRID_H      (PLAY_AREA_H    / WALK_CELL_PX)           // 42

#define EGO_W            16
#define EGO_H            24
#define EGO_TILES_X      (EGO_W / 8)                               // 2
#define EGO_TILES_Y      (EGO_H / 8)                               // 3
#define EGO_TILE_BYTES   (EGO_TILES_X * EGO_TILES_Y * TILE_BYTES)  // 192
/* SaveUnder rounds x down to platform alignment: 8 px on Amiga
 * planar, 2 px on chunky. Add 4 bytes/row of slack to cover the
 * worst case on either. */
#define EGO_BACKUP_BYTES JOEY_SPRITE_BACKUP_BYTES(EGO_W / 8, EGO_H / 8)

#define CURSOR_W         12
#define CURSOR_H         12
#define CURSOR_TILES_X   2
#define CURSOR_TILES_Y   2
#define CURSOR_BACKUP_BYTES JOEY_SPRITE_BACKUP_BYTES(CURSOR_TILES_X, CURSOR_TILES_Y)

#define ITEM_ICON_W      16
#define ITEM_ICON_H      16

#define MAX_HOTSPOTS     8
#define MAX_PROPS        6
#define MAX_PATH_NODES   128
#define EGO_WALK_SPEED   1     /* pixels per frame */
#define ANIM_TICKS       6     /* frames per walk-cycle step */

/* Palette layout. Palette 0 is the play-area palette; palette 1 is
 * the sky gradient (SCB swap by line); palette 2 is the UI palette
 * (message bar / inventory bar) so the play-area colors aren't
 * forced to share slots with white-on-blue UI ink. */
#define PAL_SCENE        0
#define PAL_SKY          1
#define PAL_UI           2

/* Color slots in PAL_SCENE. Slot 0 is always transparent on sprites
 * and is the scene's background "void" color. */
#define C_TRANS          0
#define C_BLACK          1
#define C_WHITE          2
#define C_RED            3
#define C_DARK_GREEN     4
#define C_GREEN          5
#define C_BROWN          6
#define C_DARK_BROWN     7
#define C_TAN            8
#define C_GRAY           9
#define C_DARK_GRAY     10
#define C_YELLOW        11
#define C_ORANGE        12
#define C_BLUE          13
#define C_FLESH         14
#define C_PINK          15

/* SCB palette slots. Sky uses its own palette so the gradient bands
 * don't burn play-area colors. Bands cycle through slots 1..15 in
 * the sky palette as the scanline descends from the top. */
#define C_SKY0           0

/* UI palette: keep slot 0 transparent so the same sprite color
 * convention works. */
#define UI_BG            1
#define UI_INK           2
#define UI_HILITE        3
#define UI_DIM           4

typedef enum {
    VERB_WALK = 0,
    VERB_LOOK,
    VERB_TAKE,
    VERB_USE,
    VERB_TALK,
    VERB_COUNT
} VerbE;

typedef enum {
    DIR_S = 0,
    DIR_E,
    DIR_N,
    DIR_W,
    DIR_COUNT
} DirE;

typedef enum {
    ROOM_FOREST = 0,
    ROOM_COTTAGE,
    ROOM_COUNT
} RoomE;

typedef enum {
    ITEM_NONE = 0,
    ITEM_LAMP,
    ITEM_KEY,
    ITEM_COUNT
} ItemE;

typedef enum {
    HS_NONE = 0,
    HS_TREE,
    HS_FOUNTAIN,
    HS_LAMP_ON_TABLE,
    HS_TABLE,
    HS_FIRE,
    HS_DOOR_TO_COTTAGE,
    HS_DOOR_TO_FOREST,
    HS_WINDOW,
    HS_COUNT
} HotspotIdE;

typedef struct {
    HotspotIdE id;
    int16_t    x, y, w, h;
    const char *name;
} HotspotT;

typedef struct {
    int16_t    x, y;        /* top-left of prop on screen */
    int16_t    w, h;        /* pixel size (for priority compare) */
    jlSpriteT *sp;
} PropT;

typedef struct {
    /* Each row is a string of WALK_GRID_W chars: '.' walkable,
     * '#' blocked. Walk-mask cells map 1:1 to walk-grid cells. */
    const char *walkRows[WALK_GRID_H];

    HotspotT   hotspots[MAX_HOTSPOTS];
    uint8_t    hotspotCount;

    PropT      props[MAX_PROPS];
    uint8_t    propCount;

    int16_t    egoEnterFromE_x, egoEnterFromE_y;  /* entry from east edge */
    int16_t    egoEnterFromW_x, egoEnterFromW_y;  /* entry from west edge */
    int16_t    egoDefaultX, egoDefaultY;          /* first time in room */
} RoomT;


/* ===================================================================
 * Section 2 -- Globals (game state)
 * =================================================================== */

static jlSurfaceT *gScreen;
static jlSurfaceT *gBgScene[ROOM_COUNT]; /* pristine background per room */

static RoomE       gCurRoom;
static RoomT       gRooms[ROOM_COUNT];

/* Ego sprites: [DIR][frame].  frame 0 = idle, 1 & 2 = walk cycle. */
#define EGO_FRAMES        3
static jlSpriteT  *gEgo[DIR_COUNT][EGO_FRAMES];
static uint8_t     gEgoTiles[DIR_COUNT][EGO_FRAMES][EGO_TILE_BYTES];

static uint8_t     gEgoBackup[EGO_BACKUP_BYTES] __attribute__((aligned(2)));
static jlSpriteBackupT gEgoBak;
static bool        gEgoBakValid;

/* Cursor sprite per verb (+ item-icon for "using" mode) */
static jlSpriteT  *gCursor[VERB_COUNT];
static jlSpriteT  *gItemSprite[ITEM_COUNT];   /* index 0 = unused */
static jlSpriteT  *gLampLitSprite;            /* lit-state inventory icon */
static uint8_t     gLampLitTiles[2 * 2 * TILE_BYTES];
static uint8_t     gCursorBackup[CURSOR_BACKUP_BYTES] __attribute__((aligned(2)));
static jlSpriteBackupT gCursorBak;
static bool        gCursorBakValid;

/* Ego state */
static int16_t     gEgoX, gEgoY;           /* top-left pixel of sprite */
static DirE        gEgoDir;
static uint8_t     gEgoFrameIdx;
static uint8_t     gEgoAnimTick;
static bool        gEgoMoving;

/* Path */
static int16_t     gPathX[MAX_PATH_NODES];
static int16_t     gPathY[MAX_PATH_NODES];
static uint16_t    gPathLen;
static uint16_t    gPathStep;

/* UI / interaction */
static VerbE       gCurVerb;
static ItemE       gUsingItem;              /* ITEM_NONE if not in use mode */
static bool        gHaveItem[ITEM_COUNT];   /* index 0 unused */
static char        gMessage[80];
static uint8_t     gMessageTtl;             /* frames remaining */

/* Flags */
static bool        gLampIsLit;


/* ===================================================================
 * Section 3 -- Tiny helpers
 * =================================================================== */

static int16_t clampi(int16_t v, int16_t lo, int16_t hi) {
    if (v < lo) { return lo; }
    if (v > hi) { return hi; }
    return v;
}


static void setMessage(const char *text) {
    size_t n;
    n = strlen(text);
    if (n >= sizeof(gMessage)) {
        n = sizeof(gMessage) - 1;
    }
    memcpy(gMessage, text, n);
    gMessage[n] = '\0';
    gMessageTtl = 180;   /* roughly 3 sec at 60 Hz */
}


/* RGB444 helper. JoeyLib palette entries are 0x0RGB (4 bits each). */
static uint16_t rgb(uint8_t r, uint8_t g, uint8_t b) {
    return (uint16_t)(((r & 0x0F) << 8) | ((g & 0x0F) << 4) | (b & 0x0F));
}


/* This single-file example can blow the 65816's 64 KB code-bank
 * limit, so on the IIgs we split code across segments at section
 * boundaries to spread the load across multiple banks. */
#ifdef JOEYLIB_PLATFORM_IIGS
#endif


/* ===================================================================
 * Section 4 -- Procedural sprite art
 *
 * Every sprite is authored as an ASCII "row of pixel chars" grid;
 * a tiny helper converts that to JoeyLib's 4bpp packed tile-major
 * layout at startup. Same approach as examples/sprite/sprite.c but
 * generalised so we can author dozens of frames in one file.
 *
 * Char palette in the strings:
 *   ' ' or '.'  -> transparent (color 0)
 *   '#'         -> outline (black, C_BLACK)
 *   'F'         -> flesh
 *   'h' / 'H'   -> hair / dark hair
 *   'r' / 'R'   -> red / dark red (shirt)
 *   'b' / 'B'   -> blue / dark blue (pants)
 *   'g' / 'G'   -> green / dark green
 *   'n' / 'N'   -> brown / dark brown
 *   'y' / 'Y'   -> yellow / orange
 *   'w' / 'W'   -> white / gray
 *   'T'         -> tan
 *   'k'         -> dark gray
 *   'p'         -> pink
 * =================================================================== */

static uint8_t pixCharToColor(char c) {
    switch (c) {
        case ' ': case '.': return C_TRANS;
        case '#':           return C_BLACK;
        case 'F':           return C_FLESH;
        case 'h':           return C_BROWN;
        case 'H':           return C_DARK_BROWN;
        case 'r':           return C_RED;
        case 'R':           return C_DARK_BROWN;
        case 'b':           return C_BLUE;
        case 'B':           return C_DARK_GRAY;
        case 'g':           return C_GREEN;
        case 'G':           return C_DARK_GREEN;
        case 'n':           return C_BROWN;
        case 'N':           return C_DARK_BROWN;
        case 'y':           return C_YELLOW;
        case 'Y':           return C_ORANGE;
        case 'w':           return C_WHITE;
        case 'W':           return C_GRAY;
        case 'T':           return C_TAN;
        case 'k':           return C_DARK_GRAY;
        case 'p':           return C_PINK;
        default:            return C_TRANS;
    }
}


/* Author a sprite from `widthPx` x `heightPx` ASCII rows into the
 * tile-major 4bpp packed layout jlSpriteCreate expects.
 *
 * Both dimensions must be multiples of 8. `rows[]` must contain
 * heightPx entries, each at least widthPx chars long. */
static void authorSpriteFromRows(uint8_t *dstTiles,
                                 uint16_t widthPx, uint16_t heightPx,
                                 const char * const *rows) {
    uint16_t tilesX;
    uint16_t tilesY;
    uint16_t tx, ty;
    uint16_t r, b;
    uint8_t *dst;
    uint8_t  pix0, pix1;
    char     c0, c1;

    tilesX = (uint16_t)(widthPx / 8);
    tilesY = (uint16_t)(heightPx / 8);

    for (ty = 0; ty < tilesY; ty++) {
        for (tx = 0; tx < tilesX; tx++) {
            dst = &dstTiles[((ty * tilesX) + tx) * TILE_BYTES];
            for (r = 0; r < 8; r++) {
                for (b = 0; b < 4; b++) {
                    /* Each byte = 2 pixels: high nibble = left. */
                    c0 = rows[(ty * 8) + r][(tx * 8) + (b * 2)];
                    c1 = rows[(ty * 8) + r][(tx * 8) + (b * 2) + 1];
                    pix0 = pixCharToColor(c0);
                    pix1 = pixCharToColor(c1);
                    dst[r * 4 + b] = (uint8_t)((pix0 << 4) | (pix1 & 0x0F));
                }
            }
        }
    }
}


/* --- Ego sprites: 16x24, 4 directions, 3 frames each (idle + 2 walk).
 *
 * Style: small chibi character. Brown hair, red tunic, blue pants. */

static const char * const kEgoIdleS[24] = {
    "    ####        ",
    "   #HHHH#       ",
    "  #HHhhHH#      ",
    "  #HFFFFH#      ",
    "  #FF##FF#      ",
    "   #FFFF#       ",
    "    #FF#        ",
    "   #rrrr#       ",
    "  #rrrrrr#      ",
    "  #rrrrrr#      ",
    " #rrrrrrrr#     ",
    " #rrrrrrrr#     ",
    "  #rrrrrr#      ",
    "  #brrrrb#      ",
    "  #bbbbbb#      ",
    "  #bbbbbb#      ",
    "  #bbbbbb#      ",
    "   #bbbb#       ",
    "   #bb##        ",
    "   #b#          ",
    "   ##           ",
    "                ",
    "                ",
    "                ",
};

static const char * const kEgoWalk1S[24] = {
    "    ####        ",
    "   #HHHH#       ",
    "  #HHhhHH#      ",
    "  #HFFFFH#      ",
    "  #FF##FF#      ",
    "   #FFFF#       ",
    "    #FF#        ",
    "   #rrrr#       ",
    "  #rrrrrr#      ",
    "  #rrrrrr#      ",
    " #rrrrrrrr#     ",
    " #rrrrrrrr#     ",
    "  #rrrrrr#      ",
    "  #brrrrb#      ",
    "  #bbbbbb#      ",
    "  #bbbbbb#      ",
    "   #bbbb#       ",
    "   #bb#         ",
    "  #bb#          ",
    "  ##            ",
    "                ",
    "                ",
    "                ",
    "                ",
};

static const char * const kEgoWalk2S[24] = {
    "    ####        ",
    "   #HHHH#       ",
    "  #HHhhHH#      ",
    "  #HFFFFH#      ",
    "  #FF##FF#      ",
    "   #FFFF#       ",
    "    #FF#        ",
    "   #rrrr#       ",
    "  #rrrrrr#      ",
    "  #rrrrrr#      ",
    " #rrrrrrrr#     ",
    " #rrrrrrrr#     ",
    "  #rrrrrr#      ",
    "  #brrrrb#      ",
    "  #bbbbbb#      ",
    "  #bbbbbb#      ",
    "   #bbbb#       ",
    "    #bb#        ",
    "     #bb#       ",
    "      ##        ",
    "                ",
    "                ",
    "                ",
    "                ",
};

static const char * const kEgoIdleN[24] = {
    "    ####        ",
    "   #HHHH#       ",
    "  #HHHHHH#      ",
    "  #HHHHHH#      ",
    "  #HHHHHH#      ",
    "   #HHHH#       ",
    "    ####        ",
    "   #rrrr#       ",
    "  #rrrrrr#      ",
    "  #rrrrrr#      ",
    " #rrrrrrrr#     ",
    " #rrrrrrrr#     ",
    "  #rrrrrr#      ",
    "  #brrrrb#      ",
    "  #bbbbbb#      ",
    "  #bbbbbb#      ",
    "  #bbbbbb#      ",
    "   #bbbb#       ",
    "   ##bb#        ",
    "     #b#        ",
    "      ##        ",
    "                ",
    "                ",
    "                ",
};

static const char * const kEgoWalk1N[24] = {
    "    ####        ",
    "   #HHHH#       ",
    "  #HHHHHH#      ",
    "  #HHHHHH#      ",
    "  #HHHHHH#      ",
    "   #HHHH#       ",
    "    ####        ",
    "   #rrrr#       ",
    "  #rrrrrr#      ",
    "  #rrrrrr#      ",
    " #rrrrrrrr#     ",
    " #rrrrrrrr#     ",
    "  #rrrrrr#      ",
    "  #brrrrb#      ",
    "  #bbbbbb#      ",
    "  #bbbbbb#      ",
    "   #bbbb#       ",
    "    #bb#        ",
    "    #bb#        ",
    "    ##          ",
    "                ",
    "                ",
    "                ",
    "                ",
};

static const char * const kEgoWalk2N[24] = {
    "    ####        ",
    "   #HHHH#       ",
    "  #HHHHHH#      ",
    "  #HHHHHH#      ",
    "  #HHHHHH#      ",
    "   #HHHH#       ",
    "    ####        ",
    "   #rrrr#       ",
    "  #rrrrrr#      ",
    "  #rrrrrr#      ",
    " #rrrrrrrr#     ",
    " #rrrrrrrr#     ",
    "  #rrrrrr#      ",
    "  #brrrrb#      ",
    "  #bbbbbb#      ",
    "  #bbbbbb#      ",
    "   #bbbb#       ",
    "    #bb#        ",
    "     #bb#       ",
    "      ##        ",
    "                ",
    "                ",
    "                ",
    "                ",
};

static const char * const kEgoIdleE[24] = {
    "     ####       ",
    "    #HHHH#      ",
    "   #HHHHhh#     ",
    "   #FFFFhH#     ",
    "   #F##FFH#     ",
    "    #FFF#       ",
    "    #FF#        ",
    "   #rrrr#       ",
    "  #rrrrrr#      ",
    " #rrrrrrrr##    ",
    "#rrrrrrrrFF#    ",
    " #rrrrrrrr#     ",
    "  #rrrrrr#      ",
    "  #brrrrb#      ",
    "  #bbbbbb#      ",
    "  #bbbbbb#      ",
    "  #bbbbbb#      ",
    "   #bb#bb#      ",
    "   ##  ##       ",
    "   #    #       ",
    "                ",
    "                ",
    "                ",
    "                ",
};

static const char * const kEgoWalk1E[24] = {
    "     ####       ",
    "    #HHHH#      ",
    "   #HHHHhh#     ",
    "   #FFFFhH#     ",
    "   #F##FFH#     ",
    "    #FFF#       ",
    "    #FF#        ",
    "   #rrrr#       ",
    "  #rrrrrr#      ",
    " #rrrrrrrr##    ",
    "#rrrrrrrrFF#    ",
    " #rrrrrrrr#     ",
    "  #rrrrrr#      ",
    "  #brrrrb#      ",
    "  #bbbbbb#      ",
    "  #bbbbbb#      ",
    "   #bbbb#       ",
    "   ##  #b#      ",
    "         ##     ",
    "                ",
    "                ",
    "                ",
    "                ",
    "                ",
};

static const char * const kEgoWalk2E[24] = {
    "     ####       ",
    "    #HHHH#      ",
    "   #HHHHhh#     ",
    "   #FFFFhH#     ",
    "   #F##FFH#     ",
    "    #FFF#       ",
    "    #FF#        ",
    "   #rrrr#       ",
    "  #rrrrrr#      ",
    " #rrrrrrrr##    ",
    "#rrrrrrrrFF#    ",
    " #rrrrrrrr#     ",
    "  #rrrrrr#      ",
    "  #brrrrb#      ",
    "  #bbbbbb#      ",
    "  #bbbbbb#      ",
    "   #bbbb#       ",
    "   #bb###       ",
    "  ##            ",
    "                ",
    "                ",
    "                ",
    "                ",
    "                ",
};

static const char * const kEgoIdleW[24] = {
    "       ####     ",
    "      #HHHH#    ",
    "     #hhHHHH#   ",
    "     #HhFFFF#   ",
    "     #HFF##F#   ",
    "      #FFF#     ",
    "       #FF#     ",
    "      #rrrr#    ",
    "     #rrrrrr#   ",
    "   ##rrrrrrrr#  ",
    "   #FFrrrrrrrr# ",
    "    #rrrrrrrr#  ",
    "     #rrrrrr#   ",
    "     #brrrrb#   ",
    "     #bbbbbb#   ",
    "     #bbbbbb#   ",
    "     #bbbbbb#   ",
    "     #bb#bb#    ",
    "      ##  ##    ",
    "      #    #    ",
    "                ",
    "                ",
    "                ",
    "                ",
};

static const char * const kEgoWalk1W[24] = {
    "       ####     ",
    "      #HHHH#    ",
    "     #hhHHHH#   ",
    "     #HhFFFF#   ",
    "     #HFF##F#   ",
    "      #FFF#     ",
    "       #FF#     ",
    "      #rrrr#    ",
    "     #rrrrrr#   ",
    "   ##rrrrrrrr#  ",
    "   #FFrrrrrrrr# ",
    "    #rrrrrrrr#  ",
    "     #rrrrrr#   ",
    "     #brrrrb#   ",
    "     #bbbbbb#   ",
    "     #bbbbbb#   ",
    "      #bbbb#    ",
    "      #b#  ##   ",
    "     ##         ",
    "                ",
    "                ",
    "                ",
    "                ",
    "                ",
};

static const char * const kEgoWalk2W[24] = {
    "       ####     ",
    "      #HHHH#    ",
    "     #hhHHHH#   ",
    "     #HhFFFF#   ",
    "     #HFF##F#   ",
    "      #FFF#     ",
    "       #FF#     ",
    "      #rrrr#    ",
    "     #rrrrrr#   ",
    "   ##rrrrrrrr#  ",
    "   #FFrrrrrrrr# ",
    "    #rrrrrrrr#  ",
    "     #rrrrrr#   ",
    "     #brrrrb#   ",
    "     #bbbbbb#   ",
    "     #bbbbbb#   ",
    "      #bbbb#    ",
    "      ###bb#    ",
    "        #bb#    ",
    "         ##     ",
    "                ",
    "                ",
    "                ",
    "                ",
};


/* --- Cursor sprites: 16x16, drawn at the mouse position. */

static const char * const kCursorWalk[16] = {
    "      ww        ",
    "      ww        ",
    "  w   ww   w    ",
    "  ww  ww  ww    ",
    "  www ww www    ",
    "wwwwwwwwwwwwww  ",
    "  www ww www    ",
    "  ww  ww  ww    ",
    "  w   ww   w    ",
    "      ww        ",
    "      ww        ",
    "                ",
    "                ",
    "                ",
    "                ",
    "                ",
};

static const char * const kCursorLook[16] = {
    "    wwwwww      ",
    "  ww      ww    ",
    " w  wwwwww  w   ",
    "w  w      w  w  ",
    "w w wwwww w  w  ",
    "w w w  bw w  w  ",
    "w w w bbw w  w  ",
    "w w wbbww w  w  ",
    "w w wwwww w  w  ",
    "w  w      w  w  ",
    " w  wwwwww  w   ",
    "  ww      ww    ",
    "    wwwwww      ",
    "                ",
    "                ",
    "                ",
};

static const char * const kCursorTake[16] = {
    "    wwwwww      ",
    "   wnnnnnnw     ",
    "  wnnNNNNnnw    ",
    " wnnnnnnnnnnw   ",
    " wnnnnnnnnnnw   ",
    "wnnnnnnnnnnnnw  ",
    "wnnnnnnnnnnnnw  ",
    "wnnnnnnnnnnnnw  ",
    " wnnnnnnnnnnw   ",
    " wnnnnnnnnnnw   ",
    "  wnnnnnnnnw    ",
    "   wnnnnnnw     ",
    "    wwwwww      ",
    "                ",
    "                ",
    "                ",
};

static const char * const kCursorUse[16] = {
    "       www      ",
    "       www      ",
    "       wYw      ",
    "       wYw      ",
    "      wwYww     ",
    "     wYYYYYw    ",
    "    wYYYYYYYw   ",
    "    wYYYYYYYw   ",
    "    wYYYYYYYw   ",
    "    wYYYYYYYw   ",
    "    wYYYYYYYw   ",
    "     wYYYYYw    ",
    "      wwwww     ",
    "                ",
    "                ",
    "                ",
};

static const char * const kCursorTalk[16] = {
    "  wwwwwwwwww    ",
    " w          w   ",
    "w  wwww wwww w  ",
    "w  w  w w  w w  ",
    "w  w  w w  w w  ",
    "w  wwww wwww w  ",
    "w            w  ",
    "w   wwwwwww  w  ",
    "w            w  ",
    "w  wwwwwwww  w  ",
    " w          w   ",
    "  wwwwww   w    ",
    "       www      ",
    "                ",
    "                ",
    "                ",
};


/* --- Item-icon sprites: 16x16 for inventory bar & "using" cursor. */

static const char * const kItemLampUnlit[16] = {
    "                ",
    "      ####      ",
    "     #wwww#     ",
    "    #wwwwww#    ",
    "    #wwwwww#    ",
    "    #wwwwww#    ",
    "    #wwwwww#    ",
    "    #wwwwww#    ",
    "     #wwww#     ",
    "      ####      ",
    "     #nnnn#     ",
    "    #nnnnnn#    ",
    "    #nnnnnn#    ",
    "    #nnnnnn#    ",
    "     #nnnn#     ",
    "      ####      ",
};

static const char * const kItemLampLit[16] = {
    "      yyyy      ",
    "      yyyy      ",
    "     yYYYYy     ",
    "    yYYYYYYy    ",
    "    yYYYYYYy    ",
    "    yYYYYYYy    ",
    "    yYYYYYYy    ",
    "    yYYYYYYy    ",
    "     yYYYYy     ",
    "      yyyy      ",
    "     #nnnn#     ",
    "    #nnnnnn#    ",
    "    #nnnnnn#    ",
    "    #nnnnnn#    ",
    "     #nnnn#     ",
    "      ####      ",
};

static const char * const kItemKey[16] = {
    "                ",
    "      ####      ",
    "     #yyyy#     ",
    "    #yyyyyy#    ",
    "    #yy##yy#    ",
    "    #yy##yy#    ",
    "    #yyyyyy#    ",
    "     #yyyy#     ",
    "      #yy#      ",
    "      #yy#      ",
    "      #yy#      ",
    "      #yy####   ",
    "      #yyyyy#   ",
    "      #yy####   ",
    "      #yy#      ",
    "      ####      ",
};


/* ===================================================================
 * Section 5 -- Background scene drawing (procedural)
 * =================================================================== */

/* Forest scene: gradient sky (SCB), distant horizon, ground, three
 * trees (drawn into the BG; the prop list re-overlays the trunks
 * after the ego draws so the ego walks behind them). */
static void drawForestBackground(jlSurfaceT *s) {
    int16_t y;
    int16_t baseY;

    /* Ground plane: dark green to lighter green band. The play area
     * is 168 px tall; horizon sits at y=70. */
    jlSurfaceClear(s, C_DARK_GREEN);
    jlFillRect(s, 0, 0, SURFACE_WIDTH, 70, C_BLUE);          /* sky band */
    jlFillRect(s, 0, 70, SURFACE_WIDTH, 6, C_DARK_GREEN);    /* far trees */

    /* Ground gradient: brighter green near the bottom suggests depth. */
    for (y = 76; y < PLAY_AREA_H; y++) {
        if (((y - 76) & 7) == 0) {
            jlDrawLine(s, 0, y, SURFACE_WIDTH - 1, y, C_GREEN);
        }
    }

    /* Distant tree-line silhouettes. */
    for (baseY = 64; baseY <= 72; baseY += 2) {
        int16_t x;
        for (x = 0; x < SURFACE_WIDTH; x += 18) {
            jlFillCircle(s, x, baseY, 5, C_DARK_GREEN);
        }
    }

    /* Right edge: a clearer "doorway" gap in the trees signaling the
     * exit to the cottage. Painted as a faint dirt path leading off
     * the right edge. */
    jlFillRect(s, SURFACE_WIDTH - 40, 110, 40, 14, C_TAN);
    jlFillRect(s, SURFACE_WIDTH - 38, 112, 38, 10, C_BROWN);

    /* Decorative grass tufts on the lower band. */
    {
        int16_t i;
        for (i = 0; i < SURFACE_WIDTH; i += 13) {
            jlDrawLine(s, i,     PLAY_AREA_H - 6, i,     PLAY_AREA_H - 1, C_GREEN);
            jlDrawLine(s, i + 2, PLAY_AREA_H - 4, i + 2, PLAY_AREA_H - 1, C_GREEN);
        }
    }
}


/* Cottage interior: walls, floor, window, door, table, fireplace. */
static void drawCottageBackground(jlSurfaceT *s) {
    int16_t x, y;

    jlSurfaceClear(s, C_DARK_BROWN);

    /* Back wall (top half) tan, floor (bottom half) brown. */
    jlFillRect(s, 0, 0, SURFACE_WIDTH, 90, C_TAN);
    jlFillRect(s, 0, 90, SURFACE_WIDTH, PLAY_AREA_H - 90, C_BROWN);

    /* Wall planks. */
    for (x = 0; x < SURFACE_WIDTH; x += 24) {
        jlDrawLine(s, x, 0, x, 88, C_DARK_BROWN);
    }

    /* Floor planks. */
    for (y = 100; y < PLAY_AREA_H; y += 10) {
        jlDrawLine(s, 0, y, SURFACE_WIDTH - 1, y, C_DARK_BROWN);
    }

    /* Window with sky showing through. */
    jlFillRect(s, 60, 18, 50, 40, C_BLACK);
    jlFillRect(s, 62, 20, 46, 36, C_BLUE);
    /* Window cross. */
    jlDrawLine(s, 85, 20, 85, 56, C_BLACK);
    jlDrawLine(s, 62, 38, 108, 38, C_BLACK);

    /* Fireplace on the right wall: stone surround + dark hearth +
     * flame body. The flame is a hotspot, not a prop, so the bg
     * carries the geometry. */
    jlFillRect(s, 220, 30, 70, 60, C_DARK_GRAY);
    jlFillRect(s, 228, 38, 54, 50, C_BLACK);
    /* Logs. */
    jlFillRect(s, 234, 78, 42, 6, C_DARK_BROWN);
    jlFillRect(s, 240, 72, 30, 5, C_BROWN);
    /* Flame body. */
    jlFillCircle(s, 254, 64, 8, C_ORANGE);
    jlFillCircle(s, 254, 58, 6, C_YELLOW);
    jlFillCircle(s, 254, 54, 3, C_WHITE);

    /* Door on the left edge: leads back to the forest. */
    jlFillRect(s, 8,  60, 36, 80, C_DARK_BROWN);
    jlFillRect(s, 12, 64, 28, 72, C_BROWN);
    jlDrawLine(s, 26, 64, 26, 136, C_DARK_BROWN);
    /* Doorknob. */
    jlFillCircle(s, 38, 100, 2, C_YELLOW);
}


/* SCB sky gradient: walk the sky band and assign sky-palette swaps
 * per line. The sky palette holds 8 shades of blue across slots
 * 1..8 and we map raster lines 0..69 to those. */
static void programSkyScb(jlSurfaceT *s, RoomE room) {
    int16_t line;
    int16_t skyBottom;
    (void)room;

    skyBottom = 70;
    for (line = 0; line < skyBottom; line++) {
        /* Sky-palette swap is via jlScbSet: pick a palette index per
         * line. We use PAL_SKY for sky, PAL_SCENE for everything
         * else. The sky palette itself holds the gradient. */
        jlScbSet(s, (uint16_t)line, PAL_SKY);
    }
    for (line = skyBottom; line < PLAY_AREA_H; line++) {
        jlScbSet(s, (uint16_t)line, PAL_SCENE);
    }
    for (line = PLAY_AREA_H; line < SURFACE_HEIGHT; line++) {
        jlScbSet(s, (uint16_t)line, PAL_UI);
    }
}


/* Sky palette: gradient of blues from horizon (pale) to zenith
 * (deep) keyed off slot index. We splat the gradient into ALL
 * scene-color slots so wherever the BG draws a "blue" pixel it
 * picks up the right shade for that line. The gradient lives in
 * slot C_BLUE (=13) per line via... actually simpler: the
 * background draws SOLID C_BLUE for the sky band, and the SCB
 * swaps the palette per line so C_BLUE maps to a different RGB
 * value on each scanline. Hence we want PAL_SKY's slot 13 to vary
 * per line.
 *
 * JoeyLib's SCB model swaps the WHOLE palette per line. Implement
 * the gradient by allocating a small palette pool: actually the
 * library only exposes 16 palettes. So we drop the gradient
 * approach and use one PAL_SKY palette with slot 13 set to a
 * fixed pale-blue. The "gradient" lives in the background draw
 * (raster-of-lines). Cleaner and still pretty. */
static void buildPalettes(jlSurfaceT *s) {
    uint16_t scene[16];
    uint16_t sky[16];
    uint16_t ui[16];
    uint16_t i;

    for (i = 0; i < 16; i++) {
        scene[i] = 0;
        sky[i]   = 0;
        ui[i]    = 0;
    }

    /* PAL_SCENE: the play-area colors that match C_* constants. */
    scene[C_BLACK]      = rgb(0,  0,  0);
    scene[C_WHITE]      = rgb(15, 15, 15);
    scene[C_RED]        = rgb(13, 2,  2);
    scene[C_DARK_GREEN] = rgb(2,  6,  2);
    scene[C_GREEN]      = rgb(4,  10, 3);
    scene[C_BROWN]      = rgb(9,  6,  3);
    scene[C_DARK_BROWN] = rgb(5,  3,  1);
    scene[C_TAN]        = rgb(13, 11, 7);
    scene[C_GRAY]       = rgb(10, 10, 10);
    scene[C_DARK_GRAY]  = rgb(5,  5,  5);
    scene[C_YELLOW]     = rgb(15, 13, 3);
    scene[C_ORANGE]     = rgb(14, 8,  2);
    scene[C_BLUE]       = rgb(3,  7,  13);
    scene[C_FLESH]      = rgb(14, 11, 8);
    scene[C_PINK]       = rgb(15, 10, 11);

    /* PAL_SKY: only differs from scene in the C_BLUE slot, which is
     * shifted to a pale horizon shade. Used on lines 0..69. */
    for (i = 0; i < 16; i++) {
        sky[i] = scene[i];
    }
    sky[C_BLUE] = rgb(6, 10, 15);

    /* PAL_UI: used on the message bar and inventory bar. */
    ui[UI_BG]     = rgb(2,  2,  3);
    ui[UI_INK]    = rgb(15, 15, 15);
    ui[UI_HILITE] = rgb(15, 13, 3);
    ui[UI_DIM]    = rgb(7,  7,  8);

    jlPaletteSet(s, PAL_SCENE, scene);
    jlPaletteSet(s, PAL_SKY,   sky);
    jlPaletteSet(s, PAL_UI,    ui);
}


/* ===================================================================
 * Section 6 -- Sprite construction (called once at startup)
 * =================================================================== */

static jlSpriteT *createSpriteFromRows(uint8_t *tilesDst,
                                       uint16_t widthPx, uint16_t heightPx,
                                       const char * const *rows) {
    jlSpriteT *sp;

    authorSpriteFromRows(tilesDst, widthPx, heightPx, rows);
    sp = jlSpriteCreate(tilesDst, (uint8_t)(widthPx / 8), (uint8_t)(heightPx / 8));
    if (sp != NULL) {
        (void)jlSpriteCompile(sp);
    }
    return sp;
}


static bool buildAllSprites(void) {
    static uint8_t cursorTiles[VERB_COUNT][CURSOR_TILES_X * CURSOR_TILES_Y * TILE_BYTES];
    static uint8_t itemTiles[ITEM_COUNT][2 * 2 * TILE_BYTES];

    const char * const *egoSrc[DIR_COUNT][EGO_FRAMES] = {
        { kEgoIdleS, kEgoWalk1S, kEgoWalk2S },
        { kEgoIdleE, kEgoWalk1E, kEgoWalk2E },
        { kEgoIdleN, kEgoWalk1N, kEgoWalk2N },
        { kEgoIdleW, kEgoWalk1W, kEgoWalk2W },
    };
    const char * const *cursorSrc[VERB_COUNT] = {
        kCursorWalk, kCursorLook, kCursorTake, kCursorUse, kCursorTalk
    };

    uint8_t d;
    uint8_t f;
    uint8_t v;

    for (d = 0; d < DIR_COUNT; d++) {
        for (f = 0; f < EGO_FRAMES; f++) {
            gEgo[d][f] = createSpriteFromRows(gEgoTiles[d][f], EGO_W, EGO_H, egoSrc[d][f]);
            if (gEgo[d][f] == NULL) { return false; }
        }
    }
    for (v = 0; v < VERB_COUNT; v++) {
        gCursor[v] = createSpriteFromRows(cursorTiles[v], 16, 16, cursorSrc[v]);
        if (gCursor[v] == NULL) { return false; }
    }

    gItemSprite[ITEM_LAMP] = createSpriteFromRows(itemTiles[ITEM_LAMP], 16, 16, kItemLampUnlit);
    gItemSprite[ITEM_KEY]  = createSpriteFromRows(itemTiles[ITEM_KEY],  16, 16, kItemKey);
    gLampLitSprite         = createSpriteFromRows(gLampLitTiles,        16, 16, kItemLampLit);
    if (gItemSprite[ITEM_LAMP] == NULL || gItemSprite[ITEM_KEY] == NULL ||
        gLampLitSprite == NULL) {
        return false;
    }

    return true;
}


#ifdef JOEYLIB_PLATFORM_IIGS
#endif


/* ===================================================================
 * Section 7 -- Room definitions
 *
 * Each room has a walk-mask (ASCII grid, 80 cols x 42 rows of 4-px
 * cells), a hotspot list, and a prop list. Props are drawn AFTER
 * the ego so they can overlap and produce the Sierra "walks behind
 * the tree" depth illusion (Section 12).
 * =================================================================== */

#define WM_FOREST_W "############################################################"
/* The walk-mask is 80 columns wide; we author it in 80-char strings. */
static const char * const kWalkForest[WALK_GRID_H] = {
    /* Sky band 0..17 (rows 0..17 = y 0..71): no-walk */
    "################################################################################",
    "################################################################################",
    "################################################################################",
    "################################################################################",
    "################################################################################",
    "################################################################################",
    "################################################################################",
    "################################################################################",
    "################################################################################",
    "################################################################################",
    "################################################################################",
    "################################################################################",
    "################################################################################",
    "################################################################################",
    "################################################################################",
    "################################################################################",
    "################################################################################",
    "################################################################################",
    /* row 18, y=72: top of walkable ground */
    "................................................................................",
    "................................................................................",
    "................................................................................",
    "................................................................................",
    "................................................................................",
    "................................................................................",
    "................................................................................",
    "................................................................................",
    "................................................................................",
    "................................................................................",
    "................................................................................",
    "................................................................................",
    "................................................................................",
    "................................................................................",
    "................................................................................",
    "................................................................................",
    "................................................................................",
    "................................................................................",
    "................................................................................",
    "................................................................................",
    "................................................................................",
    "................................................................................",
    "................................................................................",
    "................................................................................",
};

/* Cottage walkable area: bounded by walls. */
static const char * const kWalkCottage[WALK_GRID_H] = {
    "################################################################################",
    "################################################################################",
    "################################################################################",
    "################################################################################",
    "################################################################################",
    "################################################################################",
    "################################################################################",
    "################################################################################",
    "################################################################################",
    "################################################################################",
    "################################################################################",
    "################################################################################",
    "################################################################################",
    "################################################################################",
    "################################################################################",
    "################################################################################",
    "################################################################################",
    "################################################################################",
    "################################################################################",
    "################################################################################",
    "################################################################################",
    "################################################################################",
    "################################################################################",
    "################################################################################",
    "################################################################################",
    "##########.....................................................................",
    "##########......................................................###############",
    "##########......................................................###############",
    "##########......................................................###############",
    "##########......................................................###############",
    "................................................................###############",
    "................................................................###############",
    "................................................................###############",
    "................................................................###############",
    "................................................................###############",
    "................................................................###############",
    "................................................................###############",
    "................................................................###############",
    "................................................................###############",
    "................................................................###############",
    "................................................................###############",
    "................................................................###############",
};


/* The forest's three tree-trunk props live on the foreground so the
 * ego can walk behind them. Their pixel positions are chosen so
 * their bottoms sit at y=130-ish (well past the walk-area top of
 * 72), giving a real depth difference. The sprite art for each is
 * 16x32 -- procedurally drawn at startup into a surface, then
 * snapshotted via jlSpriteCreateFromSurface. We define them just as
 * a string-row prop here. */

#define TREE_W   16
#define TREE_H   40

static const char * const kPropTree[TREE_H] = {
    "      gggg      ",
    "    ggggGggg    ",
    "  ggGGggggGgg   ",
    "  gGggggGgggg   ",
    " ggGgggggggGg   ",
    " gggGGggggggg   ",
    " gggggggGGggg   ",
    " gGggggggGgGg   ",
    "  ggGGggggGg    ",
    "  gggGGGgggg    ",
    "   ggggGgGg     ",
    "    gggGgg      ",
    "     gggg       ",
    "      nn        ",
    "      nn        ",
    "      nN        ",
    "      Nn        ",
    "      nn        ",
    "      nN        ",
    "      Nn        ",
    "      nn        ",
    "      nN        ",
    "      Nn        ",
    "      nn        ",
    "      nN        ",
    "      Nn        ",
    "      nn        ",
    "      nN        ",
    "      Nn        ",
    "     nNn        ",
    "     NnN        ",
    "     nNn        ",
    "    nnNN        ",
    "                ",
    "                ",
    "                ",
    "                ",
    "                ",
    "                ",
    "                ",
};

#define TABLE_W  32
#define TABLE_H  24

static const char * const kPropTable[TABLE_H] = {
    "                                ",
    "                                ",
    "                                ",
    "                                ",
    "  ############################  ",
    "  #nnnnnnnnnnnnnnnnnnnnnnnnnn#  ",
    "  #nNNNnnnnnnNNnnnnnnnnnnNNNn#  ",
    "  #nnnnnnnnnnNNnnnnnnnnnnnnnn#  ",
    "  ############################  ",
    "    ##                    ##    ",
    "    nN                    Nn    ",
    "    nN                    Nn    ",
    "    nN                    Nn    ",
    "    nN                    Nn    ",
    "    nN                    Nn    ",
    "    nN                    Nn    ",
    "    nN                    Nn    ",
    "    nN                    Nn    ",
    "    nN                    Nn    ",
    "    nN                    Nn    ",
    "    nN                    Nn    ",
    "    nN                    Nn    ",
    "    nN                    Nn    ",
    "    ##                    ##    ",
};

#define LAMP_W   8
#define LAMP_H   16

static const char * const kPropLamp[LAMP_H] = {
    "  ####  ",
    " #wwww# ",
    "#wwwwww#",
    "#wwwwww#",
    "#wwwwww#",
    "#wwwwww#",
    " #wwww# ",
    "  ####  ",
    "  #nn#  ",
    " #nnnn# ",
    " #nnnn# ",
    " #nnnn# ",
    "  #nn#  ",
    "  ####  ",
    "        ",
    "        ",
};


static uint8_t gPropTreeTiles[(TREE_W / 8) * (TREE_H / 8) * TILE_BYTES];
static uint8_t gPropTableTiles[(TABLE_W / 8) * (TABLE_H / 8) * TILE_BYTES];
static uint8_t gPropLampTiles[(LAMP_W / 8) * (LAMP_H / 8) * TILE_BYTES];

static jlSpriteT *gSpTree;
static jlSpriteT *gSpTable;
static jlSpriteT *gSpLamp;


static void buildPropSprites(void) {
    gSpTree  = createSpriteFromRows(gPropTreeTiles,  TREE_W,  TREE_H,  kPropTree);
    gSpTable = createSpriteFromRows(gPropTableTiles, TABLE_W, TABLE_H, kPropTable);
    gSpLamp  = createSpriteFromRows(gPropLampTiles,  LAMP_W,  LAMP_H,  kPropLamp);
}


/* The single mutable backup-buffer pool for prop save-under. Props
 * draw on top of the ego in the priority pass, but on entry to a
 * frame we just blit the pristine background, so we never have to
 * save-under for props. */


static void initRoomForest(void) {
    RoomT *r;
    int    i;

    r = &gRooms[ROOM_FOREST];
    for (i = 0; i < WALK_GRID_H; i++) {
        r->walkRows[i] = kWalkForest[i];
    }

    r->hotspotCount = 0;
    r->hotspots[r->hotspotCount++] = (HotspotT){ HS_TREE,             50,  92,  TREE_W, TREE_H, "the tree" };
    r->hotspots[r->hotspotCount++] = (HotspotT){ HS_TREE,            140, 100,  TREE_W, TREE_H, "the tree" };
    r->hotspots[r->hotspotCount++] = (HotspotT){ HS_TREE,            240,  95,  TREE_W, TREE_H, "the tree" };
    r->hotspots[r->hotspotCount++] = (HotspotT){ HS_DOOR_TO_COTTAGE, 290, 110,  30,    50,     "the path east" };

    r->propCount = 0;
    r->props[r->propCount++] = (PropT){ 50,  92, TREE_W, TREE_H, NULL };
    r->props[r->propCount++] = (PropT){140, 100, TREE_W, TREE_H, NULL };
    r->props[r->propCount++] = (PropT){240,  95, TREE_W, TREE_H, NULL };

    r->egoEnterFromE_x = 290;
    r->egoEnterFromE_y = 130;
    r->egoEnterFromW_x = 16;
    r->egoEnterFromW_y = 130;
    r->egoDefaultX     = 60;
    r->egoDefaultY     = 130;
}


static void initRoomCottage(void) {
    RoomT *r;
    int    i;

    r = &gRooms[ROOM_COTTAGE];
    for (i = 0; i < WALK_GRID_H; i++) {
        r->walkRows[i] = kWalkCottage[i];
    }

    r->hotspotCount = 0;
    r->hotspots[r->hotspotCount++] = (HotspotT){ HS_WINDOW,         60,  18, 50, 40, "the window" };
    r->hotspots[r->hotspotCount++] = (HotspotT){ HS_FIRE,          220,  30, 70, 60, "the fireplace" };
    r->hotspots[r->hotspotCount++] = (HotspotT){ HS_TABLE,         140, 108, TABLE_W, TABLE_H, "the table" };
    r->hotspots[r->hotspotCount++] = (HotspotT){ HS_LAMP_ON_TABLE, 156, 100, LAMP_W,  LAMP_H,  "a small lamp" };
    r->hotspots[r->hotspotCount++] = (HotspotT){ HS_DOOR_TO_FOREST,  8,  60, 36, 80, "the door" };

    r->propCount = 0;
    r->props[r->propCount++] = (PropT){ 140, 108, TABLE_W, TABLE_H, NULL };
    /* The lamp prop is drawn ONLY while the player hasn't picked it
     * up; the prop is added/removed dynamically. We pre-allocate the
     * slot here so the priority pass can find it. */
    r->props[r->propCount++] = (PropT){ 156, 100, LAMP_W,  LAMP_H,  NULL };

    r->egoEnterFromE_x = 280;
    r->egoEnterFromE_y = 150;
    r->egoEnterFromW_x = 40;
    r->egoEnterFromW_y = 130;
    r->egoDefaultX     = 130;
    r->egoDefaultY     = 130;
}


static void resolvePropSprites(void) {
    /* After buildPropSprites runs, point each room's prop->sp at the
     * shared prop sprite. Done after both buildPropSprites and
     * init*Room so we don't capture NULL. */
    RoomT *r;

    r = &gRooms[ROOM_FOREST];
    r->props[0].sp = gSpTree;
    r->props[1].sp = gSpTree;
    r->props[2].sp = gSpTree;

    r = &gRooms[ROOM_COTTAGE];
    r->props[0].sp = gSpTable;
    r->props[1].sp = gSpLamp;
}


/* ===================================================================
 * Section 8 -- Walk-mask & pathfinding
 *
 * BFS on the per-room walk grid. Cells are WALK_CELL_PX = 4 px wide,
 * so the grid is 80x42. Click destination -> shortest path of grid
 * cells -> waypoint list (one entry per cell). The ego walks
 * along waypoints, picking direction per step.
 * =================================================================== */

static bool walkable(RoomE room, int16_t cx, int16_t cy) {
    if (cx < 0 || cx >= WALK_GRID_W || cy < 0 || cy >= WALK_GRID_H) {
        return false;
    }
    return gRooms[room].walkRows[cy][cx] == '.';
}


/* Pixel-space convenience: ego's feet (bottom-center) at (px,py) ->
 * walkmask cell. */
static void footToCell(int16_t px, int16_t py, int16_t *cx, int16_t *cy) {
    *cx = (int16_t)(px / WALK_CELL_PX);
    *cy = (int16_t)(py / WALK_CELL_PX);
}


static void cellToFoot(int16_t cx, int16_t cy, int16_t *px, int16_t *py) {
    *px = (int16_t)((cx * WALK_CELL_PX) + (WALK_CELL_PX / 2));
    *py = (int16_t)((cy * WALK_CELL_PX) + (WALK_CELL_PX / 2));
}


/* BFS parents stored as a (row,col)-indexed grid. Each entry is a
 * direction byte (0=N,1=E,2=S,3=W) telling us which neighbour we
 * came from; 0xFF means unvisited. */
static uint8_t gBfsParent[WALK_GRID_H][WALK_GRID_W];
static int16_t gBfsQueueX[WALK_GRID_H * WALK_GRID_W];
static int16_t gBfsQueueY[WALK_GRID_H * WALK_GRID_W];


// Reset every BFS parent cell to unvisited (0xFF).
//
// The pathfinder is split across several helpers so no single function
// holds enough simultaneously-live int16_t values to exhaust the very
// small w65816 register file. They are marked noinline so the -O2
// inliner cannot fold them back into findPath and recreate the
// "ran out of registers" failure.
static __attribute__((noinline)) void bfsClear(void) {
    int16_t row;

    for (row = 0; row < WALK_GRID_H; row++) {
        int16_t col;
        for (col = 0; col < WALK_GRID_W; col++) {
            gBfsParent[row][col] = 0xFF;
        }
    }
}


// Breadth-first search from (scx,scy) toward (tcx,tcy) across the room's
// walk grid, recording each visited cell's incoming direction byte in
// gBfsParent. Returns true if the target cell was reached. Split out of
// findPath to keep the hot loop's live-value count low enough for the
// w65816 register allocator.
// Visit one neighbour of (cx,cy) in direction d. If that neighbour is
// walkable and not yet visited, record its incoming direction and append
// it to the BFS queue, advancing *qtail. Kept noinline so bfsSearch's
// inner loop reduces to a single call and stays within the w65816
// register budget.
static __attribute__((noinline)) void bfsExpand(RoomE room, int16_t cx, int16_t cy, uint8_t d, int16_t *qtail) {
    int16_t nx;
    int16_t ny;

    nx = cx;
    ny = cy;
    switch (d) {
        case 0: ny = (int16_t)(cy - 1); break;
        case 1: nx = (int16_t)(cx + 1); break;
        case 2: ny = (int16_t)(cy + 1); break;
        default: nx = (int16_t)(cx - 1); break;
    }
    if (!walkable(room, nx, ny)) {
        return;
    }
    if (gBfsParent[ny][nx] != 0xFF) {
        return;
    }
    gBfsParent[ny][nx] = d;
    gBfsQueueX[*qtail] = nx;
    gBfsQueueY[*qtail] = ny;
    (*qtail)++;
}


static __attribute__((noinline)) bool bfsSearch(RoomE room, int16_t scx, int16_t scy, int16_t tcx, int16_t tcy) {
    int16_t qhead;
    int16_t qtail;
    int16_t cx;
    int16_t cy;
    uint8_t d;

    qhead = 0;
    qtail = 0;
    gBfsQueueX[qtail] = scx;
    gBfsQueueY[qtail] = scy;
    qtail++;
    gBfsParent[scy][scx] = 0xFE; // sentinel: start

    while (qhead < qtail) {
        cx = gBfsQueueX[qhead];
        cy = gBfsQueueY[qhead];
        qhead++;
        if (cx == tcx && cy == tcy) {
            break;
        }
        for (d = 0; d < 4; d++) {
            bfsExpand(room, cx, cy, d, &qtail);
        }
    }

    return gBfsParent[tcy][tcx] != 0xFF;
}


// Reconstruct the path by walking gBfsParent back from the target to
// the start, then emit it forward (start..target) into the gPath*
// waypoint arrays as foot pixel positions.
static __attribute__((noinline)) void buildPath(int16_t tcx, int16_t tcy) {
    int16_t pathRevX[MAX_PATH_NODES];
    int16_t pathRevY[MAX_PATH_NODES];
    int16_t n;
    int16_t i;
    int16_t cx;
    int16_t cy;
    uint8_t d;

    n = 0;
    cx = tcx;
    cy = tcy;
    while (n < MAX_PATH_NODES) {
        pathRevX[n] = cx;
        pathRevY[n] = cy;
        n++;
        d = gBfsParent[cy][cx];
        if (d == 0xFE) {
            break;
        }
        switch (d) {
            case 0: cy = (int16_t)(cy + 1); break;
            case 1: cx = (int16_t)(cx - 1); break;
            case 2: cy = (int16_t)(cy - 1); break;
            default: cx = (int16_t)(cx + 1); break;
        }
    }

    gPathLen = 0;
    for (i = (int16_t)(n - 1); i >= 0; i--) {
        int16_t px;
        int16_t py;
        cellToFoot(pathRevX[i], pathRevY[i], &px, &py);
        gPathX[gPathLen] = px;
        gPathY[gPathLen] = py;
        gPathLen++;
        if (gPathLen >= MAX_PATH_NODES) {
            break;
        }
    }
    gPathStep = 0;
}


// Snap a cell to the nearest walkable one via an expanding box search
// (dy outer, dx inner, radius 1..maxR-1, same scan order as the original
// inline loop). Returns true if *cx/*cy now name a walkable cell (it was
// already walkable, or a snap target was found); false if none was found
// within range. Only writes *cx/*cy when a walkable cell is found.
static __attribute__((noinline)) bool snapToWalkable(RoomE room, int16_t *cx, int16_t *cy, int16_t maxR) {
    int16_t r;

    if (walkable(room, *cx, *cy)) {
        return true;
    }
    for (r = 1; r < maxR; r++) {
        int16_t dy;
        for (dy = (int16_t)(-r); dy <= r; dy++) {
            int16_t dx;
            for (dx = (int16_t)(-r); dx <= r; dx++) {
                int16_t ncx;
                int16_t ncy;
                ncx = (int16_t)(*cx + dx);
                ncy = (int16_t)(*cy + dy);
                if (walkable(room, ncx, ncy)) {
                    *cx = ncx;
                    *cy = ncy;
                    return true;
                }
            }
        }
    }
    return false;
}


static bool findPath(RoomE room, int16_t sx, int16_t sy, int16_t tx, int16_t ty) {
    int16_t scx;
    int16_t scy;
    int16_t tcx;
    int16_t tcy;

    footToCell(sx, sy, &scx, &scy);
    footToCell(tx, ty, &tcx, &tcy);

    // If the target is in an unwalkable cell, snap to the nearest
    // walkable one (radius < 12); give up if none is within range.
    if (!snapToWalkable(room, &tcx, &tcy, 12)) {
        gPathLen = 0;
        return false;
    }

    // If the start is somehow off the walkmask, snap it too (tiny
    // radius < 8); proceed with the original cell if nothing is found.
    snapToWalkable(room, &scx, &scy, 8);

    bfsClear();
    if (!bfsSearch(room, scx, scy, tcx, tcy)) {
        gPathLen = 0;
        return false;
    }

    buildPath(tcx, tcy);
    return gPathLen > 0;
}


#ifdef JOEYLIB_PLATFORM_IIGS
#endif


/* ===================================================================
 * Section 9 -- Ego animation / movement
 *
 * The ego's "feet" are tracked at (gEgoX + EGO_W/2, gEgoY + EGO_H).
 * Walking nudges those feet toward the current waypoint, picks a
 * direction by the dominant axis, and cycles frame every ANIM_TICKS.
 * =================================================================== */

static int16_t egoFeetX(void) { return (int16_t)(gEgoX + (EGO_W / 2)); }
static int16_t egoFeetY(void) { return (int16_t)(gEgoY + EGO_H); }

static void egoSetFeet(int16_t fx, int16_t fy) {
    gEgoX = (int16_t)(fx - (EGO_W / 2));
    gEgoY = (int16_t)(fy - EGO_H);
}


static void advanceEgo(void) {
    int16_t fx, fy;
    int16_t tx, ty;
    int16_t dx, dy;
    int16_t adx, ady;
    int16_t step;

    if (!gEgoMoving) { return; }
    if (gPathStep >= gPathLen) {
        gEgoMoving = false;
        return;
    }

    fx = egoFeetX();
    fy = egoFeetY();
    tx = gPathX[gPathStep];
    ty = gPathY[gPathStep];
    dx = (int16_t)(tx - fx);
    dy = (int16_t)(ty - fy);
    adx = (int16_t)(dx < 0 ? -dx : dx);
    ady = (int16_t)(dy < 0 ? -dy : dy);

    if (adx <= EGO_WALK_SPEED && ady <= EGO_WALK_SPEED) {
        /* Reached this waypoint. */
        egoSetFeet(tx, ty);
        gPathStep++;
        if (gPathStep >= gPathLen) {
            gEgoMoving = false;
            gEgoFrameIdx = 0;
            return;
        }
        return;
    }

    if (adx > ady) {
        step = (int16_t)(dx > 0 ? EGO_WALK_SPEED : -EGO_WALK_SPEED);
        fx = (int16_t)(fx + step);
        gEgoDir = (step > 0) ? DIR_E : DIR_W;
    } else {
        step = (int16_t)(dy > 0 ? EGO_WALK_SPEED : -EGO_WALK_SPEED);
        fy = (int16_t)(fy + step);
        gEgoDir = (step > 0) ? DIR_S : DIR_N;
    }
    egoSetFeet(fx, fy);

    gEgoAnimTick++;
    if (gEgoAnimTick >= ANIM_TICKS) {
        gEgoAnimTick = 0;
        gEgoFrameIdx++;
        if (gEgoFrameIdx >= EGO_FRAMES) { gEgoFrameIdx = 1; }
        if (gEgoFrameIdx == 0)          { gEgoFrameIdx = 1; }
    }
}


/* ===================================================================
 * Section 10 -- Verb cursor / mouse input
 * =================================================================== */

static const char *verbName(VerbE v) {
    switch (v) {
        case VERB_WALK: return "Walk";
        case VERB_LOOK: return "Look";
        case VERB_TAKE: return "Take";
        case VERB_USE:  return "Use";
        case VERB_TALK: return "Talk";
        default:        return "?";
    }
}


/* Hit-test the play-area for a hotspot under (mx,my). Returns the
 * hotspot index in the current room, or -1 if none. */
static int hotspotAt(int16_t mx, int16_t my) {
    const RoomT *r;
    uint8_t      i;

    if (my >= PLAY_AREA_H) { return -1; }

    r = &gRooms[gCurRoom];
    for (i = 0; i < r->hotspotCount; i++) {
        const HotspotT *h;
        h = &r->hotspots[i];
        if (mx >= h->x && mx < (h->x + h->w) &&
            my >= h->y && my < (h->y + h->h)) {
            return (int)i;
        }
    }
    return -1;
}


/* ===================================================================
 * Section 11 -- Tiny inline 5x7 bitmap font
 *
 * Drawing text uses jlDrawPixel directly (set bits only). The
 * message bar updates rarely so per-pixel plot is fine.
 *
 * Each glyph is 5 wide x 7 tall, stored as 7 bytes (one per row),
 * with the high 5 bits as pixel pattern. The font covers space,
 * '0'..'9', 'A'..'Z', '.', ',', ':', '!', '?', '-', '\'', '"'.
 * Other chars render as space.
 * =================================================================== */

#define FONT_W   5
#define FONT_H   7

static const uint8_t kFontGlyph[][FONT_H] = {
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /*  0 space */
    { 0x70, 0x88, 0x98, 0xA8, 0xC8, 0x88, 0x70 }, /*  1 '0' */
    { 0x20, 0x60, 0x20, 0x20, 0x20, 0x20, 0x70 }, /*  2 '1' */
    { 0x70, 0x88, 0x08, 0x10, 0x20, 0x40, 0xF8 }, /*  3 '2' */
    { 0x70, 0x88, 0x08, 0x30, 0x08, 0x88, 0x70 }, /*  4 '3' */
    { 0x10, 0x30, 0x50, 0x90, 0xF8, 0x10, 0x10 }, /*  5 '4' */
    { 0xF8, 0x80, 0xF0, 0x08, 0x08, 0x88, 0x70 }, /*  6 '5' */
    { 0x30, 0x40, 0x80, 0xF0, 0x88, 0x88, 0x70 }, /*  7 '6' */
    { 0xF8, 0x08, 0x10, 0x20, 0x40, 0x40, 0x40 }, /*  8 '7' */
    { 0x70, 0x88, 0x88, 0x70, 0x88, 0x88, 0x70 }, /*  9 '8' */
    { 0x70, 0x88, 0x88, 0x78, 0x08, 0x10, 0x60 }, /* 10 '9' */
    { 0x70, 0x88, 0x88, 0xF8, 0x88, 0x88, 0x88 }, /* 11 'A' */
    { 0xF0, 0x88, 0x88, 0xF0, 0x88, 0x88, 0xF0 }, /* 12 'B' */
    { 0x70, 0x88, 0x80, 0x80, 0x80, 0x88, 0x70 }, /* 13 'C' */
    { 0xF0, 0x88, 0x88, 0x88, 0x88, 0x88, 0xF0 }, /* 14 'D' */
    { 0xF8, 0x80, 0x80, 0xF0, 0x80, 0x80, 0xF8 }, /* 15 'E' */
    { 0xF8, 0x80, 0x80, 0xF0, 0x80, 0x80, 0x80 }, /* 16 'F' */
    { 0x70, 0x88, 0x80, 0xB8, 0x88, 0x88, 0x70 }, /* 17 'G' */
    { 0x88, 0x88, 0x88, 0xF8, 0x88, 0x88, 0x88 }, /* 18 'H' */
    { 0x70, 0x20, 0x20, 0x20, 0x20, 0x20, 0x70 }, /* 19 'I' */
    { 0x38, 0x10, 0x10, 0x10, 0x90, 0x90, 0x60 }, /* 20 'J' */
    { 0x88, 0x90, 0xA0, 0xC0, 0xA0, 0x90, 0x88 }, /* 21 'K' */
    { 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0xF8 }, /* 22 'L' */
    { 0x88, 0xD8, 0xA8, 0xA8, 0x88, 0x88, 0x88 }, /* 23 'M' */
    { 0x88, 0xC8, 0xA8, 0x98, 0x88, 0x88, 0x88 }, /* 24 'N' */
    { 0x70, 0x88, 0x88, 0x88, 0x88, 0x88, 0x70 }, /* 25 'O' */
    { 0xF0, 0x88, 0x88, 0xF0, 0x80, 0x80, 0x80 }, /* 26 'P' */
    { 0x70, 0x88, 0x88, 0x88, 0xA8, 0x90, 0x68 }, /* 27 'Q' */
    { 0xF0, 0x88, 0x88, 0xF0, 0xA0, 0x90, 0x88 }, /* 28 'R' */
    { 0x70, 0x88, 0x80, 0x70, 0x08, 0x88, 0x70 }, /* 29 'S' */
    { 0xF8, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20 }, /* 30 'T' */
    { 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x70 }, /* 31 'U' */
    { 0x88, 0x88, 0x88, 0x88, 0x88, 0x50, 0x20 }, /* 32 'V' */
    { 0x88, 0x88, 0x88, 0xA8, 0xA8, 0xD8, 0x88 }, /* 33 'W' */
    { 0x88, 0x88, 0x50, 0x20, 0x50, 0x88, 0x88 }, /* 34 'X' */
    { 0x88, 0x88, 0x50, 0x20, 0x20, 0x20, 0x20 }, /* 35 'Y' */
    { 0xF8, 0x08, 0x10, 0x20, 0x40, 0x80, 0xF8 }, /* 36 'Z' */
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x60 }, /* 37 '.' */
    { 0x00, 0x00, 0x00, 0x00, 0x60, 0x60, 0x40 }, /* 38 ',' */
    { 0x00, 0x60, 0x60, 0x00, 0x60, 0x60, 0x00 }, /* 39 ':' */
    { 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, 0x20 }, /* 40 '!' */
    { 0x70, 0x88, 0x08, 0x10, 0x20, 0x00, 0x20 }, /* 41 '?' */
    { 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00 }, /* 42 '-' */
    { 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 43 '\'' */
    { 0x50, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 44 '"' */
};

static int glyphIdx(char c) {
    if (c == ' ')                  return 0;
    if (c >= '0' && c <= '9')      return 1 + (c - '0');
    if (c >= 'A' && c <= 'Z')      return 11 + (c - 'A');
    if (c >= 'a' && c <= 'z')      return 11 + (c - 'a');  /* uppercase fallback */
    if (c == '.')                  return 37;
    if (c == ',')                  return 38;
    if (c == ':')                  return 39;
    if (c == '!')                  return 40;
    if (c == '?')                  return 41;
    if (c == '-')                  return 42;
    if (c == '\'')                 return 43;
    if (c == '"')                  return 44;
    return 0;
}

static void drawTextLine(int16_t x, int16_t y, const char *text, uint8_t color) {
    int16_t cx;
    int16_t i;
    int     idx;
    int16_t row;
    int16_t col;
    uint8_t bits;

    cx = x;
    for (i = 0; text[i] != '\0'; i++) {
        idx = glyphIdx(text[i]);
        for (row = 0; row < FONT_H; row++) {
            bits = kFontGlyph[idx][row];
            for (col = 0; col < FONT_W; col++) {
                if (bits & (0x80 >> col)) {
                    jlDrawPixel(gScreen, (int16_t)(cx + col), (int16_t)(y + row), color);
                }
            }
        }
        cx = (int16_t)(cx + FONT_W + 1);
        if (cx > SURFACE_WIDTH - FONT_W) { return; }
    }
}


/* ===================================================================
 * Section 12 -- UI bars (message + inventory)
 *
 * Drawn directly on the stage every frame after the play area is
 * composed. The bars never get save-undered; they're simple
 * fillRects.
 * =================================================================== */

static void drawMessageBar(void) {
    jlFillRect(gScreen, 0, MSG_BAR_Y, SURFACE_WIDTH, MSG_BAR_H, UI_BG);
    jlFillRect(gScreen, 0, MSG_BAR_Y, SURFACE_WIDTH, 1,           UI_DIM);
    if (gMessage[0] != '\0') {
        drawTextLine(4, MSG_BAR_Y + 2, gMessage, UI_INK);
    }
}


static void drawInventoryBar(void) {
    char    buf[40];
    int     n;
    int16_t x;

    jlFillRect(gScreen, 0, INV_BAR_Y, SURFACE_WIDTH, INV_BAR_H, UI_BG);
    jlFillRect(gScreen, 0, INV_BAR_Y, SURFACE_WIDTH, 1,         UI_DIM);

    /* Verb readout at the left. */
    snprintf(buf, sizeof(buf), "VERB: %s", verbName(gCurVerb));
    drawTextLine(4, INV_BAR_Y + 2, buf, UI_HILITE);

    /* Item icons at the right. */
    x = SURFACE_WIDTH - (ITEM_ICON_W + 4);
    n = 0;
    if (gHaveItem[ITEM_LAMP]) {
        jlSpriteT *sp;
        sp = gLampIsLit ? gLampLitSprite : gItemSprite[ITEM_LAMP];
        jlSpriteDraw(gScreen, sp, x, INV_BAR_Y + 2);
        x = (int16_t)(x - (ITEM_ICON_W + 2));
        n++;
    }
    if (gHaveItem[ITEM_KEY]) {
        jlSpriteDraw(gScreen, gItemSprite[ITEM_KEY], x, INV_BAR_Y + 2);
        x = (int16_t)(x - (ITEM_ICON_W + 2));
        n++;
    }
    (void)n;
}


#ifdef JOEYLIB_PLATFORM_IIGS
#endif


/* ===================================================================
 * Section 13 -- Per-frame compositor
 *
 * 1. Blit the pristine room background onto the stage.
 * 2. Draw the ego.
 * 3. Re-draw any prop whose y_bottom is > ego.y_bottom (=> appears
 *    in front of the ego, completing the "walk-behind" illusion).
 * 4. Draw UI bars.
 * 5. Draw cursor on top.
 * 6. Present.
 * =================================================================== */

static void composeFrame(int16_t cursorX, int16_t cursorY) {
    const RoomT *r;
    int16_t      egoBottom;
    uint8_t      i;

    /* 1. background */
    jlSurfaceCopy(gScreen, gBgScene[gCurRoom]);

    /* 2. ego */
    jlSpriteDraw(gScreen, gEgo[gEgoDir][gEgoFrameIdx], gEgoX, gEgoY);

    /* 3. prop priority redraw */
    r = &gRooms[gCurRoom];
    egoBottom = (int16_t)(gEgoY + EGO_H);
    for (i = 0; i < r->propCount; i++) {
        const PropT *p;
        p = &r->props[i];
        if (p->sp == NULL) { continue; }
        if ((p->y + p->h) > egoBottom) {
            jlSpriteDraw(gScreen, p->sp, p->x, p->y);
        }
    }

    /* 4. UI bars */
    drawMessageBar();
    drawInventoryBar();

    /* 5. Cursor: keep it in the play area (and inventory area too).
     * The cursor is drawn last so it sits on top of everything. */
    {
        jlSpriteT *cs;
        int16_t cx, cy;
        cx = (int16_t)(cursorX - 6);
        cy = (int16_t)(cursorY - 6);
        if (gUsingItem != ITEM_NONE) {
            cs = gItemSprite[gUsingItem];
        } else {
            cs = gCursor[gCurVerb];
        }
        if (cs != NULL) {
            jlSpriteDraw(gScreen, cs, cx, cy);
        }
    }

    /* 6. flush */
    jlStagePresent();
}


#ifdef JOEYLIB_PLATFORM_IIGS
#endif


/* ===================================================================
 * Section 14 -- Verb / item interactions
 *
 * Each hotspot has per-verb behavior. Item-on-hotspot combinations
 * are handled when gUsingItem != ITEM_NONE.
 * =================================================================== */

static void enterRoom(RoomE room, bool fromEast, bool fromWest);


static void doVerbOnHotspot(VerbE verb, HotspotIdE hs) {
    if (gUsingItem != ITEM_NONE) {
        /* Item-on-hotspot combinations. */
        if (gUsingItem == ITEM_LAMP && hs == HS_FIRE) {
            if (gLampIsLit) {
                setMessage("The lamp is already lit.");
            } else {
                gLampIsLit = true;
                setMessage("The lamp catches and burns warmly.");
            }
        } else if (gUsingItem == ITEM_LAMP && hs == HS_TREE) {
            setMessage("Burning the tree would be uncivil.");
        } else {
            setMessage("Nothing happens.");
        }
        gUsingItem = ITEM_NONE;
        return;
    }

    switch (verb) {
        case VERB_LOOK:
            switch (hs) {
                case HS_TREE:
                    setMessage("A tall oak. You see no birds today.");
                    break;
                case HS_FOUNTAIN:
                    setMessage("A mossy fountain. The water is clear.");
                    break;
                case HS_LAMP_ON_TABLE:
                    setMessage("A small brass lamp sits on the table.");
                    break;
                case HS_TABLE:
                    setMessage("A weathered oak table.");
                    break;
                case HS_FIRE:
                    setMessage("A small fire crackles in the hearth.");
                    break;
                case HS_DOOR_TO_COTTAGE:
                    setMessage("A path leads east toward a small cottage.");
                    break;
                case HS_DOOR_TO_FOREST:
                    setMessage("The door leads back to the forest.");
                    break;
                case HS_WINDOW:
                    setMessage("Through the window you see trees and sky.");
                    break;
                default:
                    setMessage("Nothing of interest.");
                    break;
            }
            break;

        case VERB_TAKE:
            switch (hs) {
                case HS_LAMP_ON_TABLE: {
                    RoomT *r;
                    if (gHaveItem[ITEM_LAMP]) {
                        setMessage("You already have the lamp.");
                        break;
                    }
                    gHaveItem[ITEM_LAMP] = true;
                    setMessage("You take the small brass lamp.");
                    /* Remove the lamp prop and its hotspot. */
                    r = &gRooms[ROOM_COTTAGE];
                    r->props[1].sp = NULL;
                    {
                        uint8_t k;
                        for (k = 0; k < r->hotspotCount; k++) {
                            if (r->hotspots[k].id == HS_LAMP_ON_TABLE) {
                                r->hotspots[k].id = HS_NONE;
                            }
                        }
                    }
                    break;
                }
                case HS_TABLE:
                    setMessage("The table is too heavy to carry.");
                    break;
                case HS_TREE:
                    setMessage("You can't pick up a tree.");
                    break;
                case HS_FIRE:
                    setMessage("That is much too hot to handle.");
                    break;
                default:
                    setMessage("Better leave that alone.");
                    break;
            }
            break;

        case VERB_USE:
            switch (hs) {
                case HS_DOOR_TO_COTTAGE:
                    enterRoom(ROOM_COTTAGE, false, true);
                    setMessage("You enter the cottage.");
                    break;
                case HS_DOOR_TO_FOREST:
                    enterRoom(ROOM_FOREST, true, false);
                    setMessage("You step back into the forest.");
                    break;
                case HS_LAMP_ON_TABLE:
                    setMessage("Pick it up first.");
                    break;
                case HS_FIRE:
                    setMessage("Warm. Try \"use lamp on fire\" with a lamp.");
                    break;
                default:
                    setMessage("That doesn't seem to do anything.");
                    break;
            }
            break;

        case VERB_TALK:
            switch (hs) {
                case HS_TREE:
                    setMessage("The tree does not reply.");
                    break;
                case HS_FIRE:
                    setMessage("The fire crackles companionably.");
                    break;
                default:
                    setMessage("There's no one to talk to here.");
                    break;
            }
            break;

        case VERB_WALK:
        default:
            /* WALK falls through to path planning at the call site;
             * see handleClick. */
            break;
    }
}


/* ===================================================================
 * Section 15 -- Click handling
 * =================================================================== */

static void handleClick(int16_t mx, int16_t my, bool leftClick, bool rightClick) {
    int hs;

    if (rightClick) {
        if (gUsingItem != ITEM_NONE) {
            gUsingItem = ITEM_NONE;
            setMessage("");
        } else {
            gCurVerb = (VerbE)((gCurVerb + 1) % VERB_COUNT);
        }
        return;
    }

    if (!leftClick) { return; }

    /* Click on inventory bar -> "use this item next". */
    if (my >= INV_BAR_Y) {
        int16_t x;
        x = (int16_t)(SURFACE_WIDTH - (ITEM_ICON_W + 4));
        if (gHaveItem[ITEM_LAMP]) {
            if (mx >= x && mx < (x + ITEM_ICON_W)) {
                gUsingItem = ITEM_LAMP;
                setMessage("Use the lamp on what?");
                return;
            }
            x = (int16_t)(x - (ITEM_ICON_W + 2));
        }
        if (gHaveItem[ITEM_KEY]) {
            if (mx >= x && mx < (x + ITEM_ICON_W)) {
                gUsingItem = ITEM_KEY;
                setMessage("Use the key on what?");
                return;
            }
        }
        return;
    }

    /* Message bar swallows the click but does nothing. */
    if (my >= MSG_BAR_Y && my < INV_BAR_Y) {
        return;
    }

    /* Inside play area. */
    hs = hotspotAt(mx, my);
    if (hs >= 0) {
        HotspotIdE id;
        id = gRooms[gCurRoom].hotspots[hs].id;
        if (id != HS_NONE) {
            if (gCurVerb == VERB_WALK && gUsingItem == ITEM_NONE) {
                /* "Walk to" hotspot = walk near it. */
                int16_t tx;
                int16_t ty;
                tx = (int16_t)(gRooms[gCurRoom].hotspots[hs].x + (gRooms[gCurRoom].hotspots[hs].w / 2));
                ty = (int16_t)(gRooms[gCurRoom].hotspots[hs].y +  gRooms[gCurRoom].hotspots[hs].h - 4);
                if (findPath(gCurRoom, egoFeetX(), egoFeetY(), tx, ty)) {
                    gEgoMoving = true;
                }
            } else {
                doVerbOnHotspot(gCurVerb, id);
            }
            return;
        }
    }

    /* Fallback: walk to the click point (gameplay's main verb). */
    if (gUsingItem != ITEM_NONE) {
        setMessage("Click on an object to use the item on it.");
        return;
    }
    if (findPath(gCurRoom, egoFeetX(), egoFeetY(), mx, my)) {
        gEgoMoving = true;
    }
}


/* ===================================================================
 * Section 16 -- Room transitions
 * =================================================================== */

static void enterRoom(RoomE room, bool fromEast, bool fromWest) {
    const RoomT *r;

    gCurRoom = room;
    r = &gRooms[room];

    if (fromEast) {
        egoSetFeet(r->egoEnterFromE_x, r->egoEnterFromE_y);
    } else if (fromWest) {
        egoSetFeet(r->egoEnterFromW_x, r->egoEnterFromW_y);
    } else {
        egoSetFeet(r->egoDefaultX, r->egoDefaultY);
    }
    gEgoMoving = false;
    gEgoFrameIdx = 0;
    gEgoBakValid = false;
    gCursorBakValid = false;
    gPathLen = 0;
    gPathStep = 0;

    programSkyScb(gScreen, room);
}


/* ===================================================================
 * Section 17 -- Background-scene staging
 *
 * We draw each room background ONCE into its own offscreen surface
 * at startup. Per-frame, we surfaceCopy the pristine background to
 * the stage, then add ego/props/cursor. This avoids per-frame draw
 * cost of all the primitives that compose the scene.
 * =================================================================== */

static bool buildBackgrounds(void) {
    gBgScene[ROOM_FOREST]  = jlSurfaceCreate();
    gBgScene[ROOM_COTTAGE] = jlSurfaceCreate();
    if (gBgScene[ROOM_FOREST] == NULL || gBgScene[ROOM_COTTAGE] == NULL) {
        return false;
    }
    drawForestBackground(gBgScene[ROOM_FOREST]);
    drawCottageBackground(gBgScene[ROOM_COTTAGE]);
    return true;
}


#ifdef JOEYLIB_PLATFORM_IIGS
#endif


/* ===================================================================
 * Section 18 -- main()
 * =================================================================== */

int main(void) {
    jlConfigT config;
    bool      leftPressed;
    bool      rightPressed;

    /* Sprite codegen budget: 12 ego frames + 5 cursors + 3 props +
     * 2 items = 22 sprites. Amiga planar emits 8 shifts/sprite at
     * ~1.5 KB each, so we ask for 256 KB to be safe. Chunky ports
     * need ~16 KB. */
#ifdef JOEYLIB_PLATFORM_IIGS
    // IIgs: the codegen arena is a single attrNoCross Memory Manager
    // block and cannot exceed one 64 KB bank; 16 KB covers the chunky
    // emitter's needs for this sprite set.
    config.codegenBytes = 16UL * 1024;
#else
    config.codegenBytes = 256UL * 1024;
#endif
    config.audioBytes   = 64UL * 1024;

    if (!jlInit(&config)) {
        fprintf(stderr, "jlInit failed: %s\n", jlLastError());
        return 1;
    }

    gScreen = jlStageGet();
    if (gScreen == NULL) {
        fprintf(stderr, "jlStageGet returned NULL\n");
        jlShutdown();
        return 1;
    }

    buildPalettes(gScreen);
    programSkyScb(gScreen, ROOM_FOREST);

    if (!buildAllSprites()) {
        fprintf(stderr, "buildAllSprites failed: %s\n", jlLastError());
        jlShutdown();
        return 1;
    }
    buildPropSprites();
    initRoomForest();
    initRoomCottage();
    resolvePropSprites();

    if (!buildBackgrounds()) {
        fprintf(stderr, "buildBackgrounds failed: %s\n", jlLastError());
        jlShutdown();
        return 1;
    }

    gEgoBak.bytes    = gEgoBackup;
    gCursorBak.bytes = gCursorBackup;

    gCurVerb = VERB_WALK;
    gUsingItem = ITEM_NONE;
    gMessage[0] = '\0';
    gMessageTtl = 0;

    enterRoom(ROOM_FOREST, false, false);
    setMessage("Click to walk. Space or right-click cycles verb.");

    for (;;) {
        int16_t mx;
        int16_t my;

        jlInputPoll();
        if (jlKeyPressed(KEY_ESCAPE)) { break; }
        if (jlKeyPressed(KEY_1))      { gCurVerb = VERB_WALK; }
        if (jlKeyPressed(KEY_2))      { gCurVerb = VERB_LOOK; }
        if (jlKeyPressed(KEY_3))      { gCurVerb = VERB_TAKE; }
        if (jlKeyPressed(KEY_4))      { gCurVerb = VERB_USE;  }
        if (jlKeyPressed(KEY_5))      { gCurVerb = VERB_TALK; }
        /* SPACE cycles verb -- one-button-mouse fallback for the
         * IIgs, where right-click does not exist. Mirrors the
         * right-click behaviour: drops any in-progress item-use
         * mode first, then advances the verb. */
        if (jlKeyPressed(KEY_SPACE)) {
            if (gUsingItem != ITEM_NONE) {
                gUsingItem = ITEM_NONE;
                setMessage("");
            } else {
                gCurVerb = (VerbE)((gCurVerb + 1) % VERB_COUNT);
            }
        }

        mx = jlMouseX();
        my = jlMouseY();
        if (mx < 0) { mx = 0; }
        if (my < 0) { my = 0; }
        if (mx >= SURFACE_WIDTH)  { mx = SURFACE_WIDTH - 1; }
        if (my >= SURFACE_HEIGHT) { my = SURFACE_HEIGHT - 1; }

        leftPressed  = jlMousePressed(MOUSE_BUTTON_LEFT);
        rightPressed = jlMousePressed(MOUSE_BUTTON_RIGHT);
        if (leftPressed || rightPressed) {
            handleClick(mx, my, leftPressed, rightPressed);
        }

        advanceEgo();

        if (gMessageTtl > 0) { gMessageTtl--; if (gMessageTtl == 0) gMessage[0] = '\0'; }

        composeFrame(mx, my);

        gEgoX = clampi(gEgoX, 0, SURFACE_WIDTH  - EGO_W);
        gEgoY = clampi(gEgoY, 0, PLAY_AREA_H    - EGO_H);
    }

    jlShutdown();
    return 0;
}
