// adventure2 -- a Sierra-faithful sample built on JoeyLib.
//
// The first adventure example (examples/adventure/adventure.c) used
// a simple "redraw foreground props on top of the ego" trick to get
// the walk-behind-the-tree illusion. That works but is unlike how the
// real Sierra engines did it. This second example reproduces the SCI
// model:
//
//   *** Three parallel screens per room ***
//   ------------------------------------------------------------------
//   1. VISUAL screen   -- the picture the player sees. 16 colors,
//                         320x200, ordinary 4bpp surface.
//   2. PRIORITY screen -- same dimensions; each pixel's value is a
//                         priority band 0..15. Painted programmatically
//                         when the room is built. Each picture object
//                         (tree, rock, grass tuft, the floor itself)
//                         has its pixels set to its priority. Higher
//                         number = closer to the camera = drawn on top.
//   3. CONTROL screen  -- same dimensions; each pixel's value is a
//                         "control color" 0..15 encoding what the game
//                         logic should do at that pixel:
//                            0  = blocked (the actor cannot stand here)
//                            1  = walkable floor
//                            2  = trigger: east-exit zone
//                         Sierra used the rest of the slots for other
//                         per-pixel game state (cliff edges, door
//                         openings, scriptable trigger regions, etc.).
//
//   *** How the engine uses them ***
//   ------------------------------------------------------------------
//   - When the actor is drawn, each candidate actor pixel reads the
//     PRIORITY screen at the same (x, y). If picture_priority >
//     actor_priority, the actor pixel is skipped: the picture is
//     in front, the actor goes behind. This is per-pixel masking,
//     so a single object can occlude part of the actor while another
//     part of the actor draws over the same object -- e.g. when an
//     actor steps half-behind a tree.
//   - The actor's priority is one value, derived from its feet Y via
//     a priority-band table. Down-on-screen = up-priority. The feet
//     position alone determines what the actor is in front of.
//   - When the player clicks to walk somewhere, the engine reads the
//     CONTROL screen pixel under the click. If it's 1 (walkable), a
//     BFS is run over the same control screen at a coarse cell grid.
//     Click on a tree trunk and the BFS still finds a path to the
//     nearest walkable pixel.
//
//   *** Visualising the buffers ***
//   ------------------------------------------------------------------
//   Press P to swap the visible screen with the priority buffer; the
//   palette is replaced with a rainbow ramp so each priority band is
//   easy to identify. Press C to switch to a control-buffer overlay
//   where each control code is a distinct flat color (green=walk,
//   red=blocked, yellow=trigger). Press N (or the same key again) to
//   return to the normal view. Watch the priority view as the ego walks --
//   the per-pixel masking that produces the "walk behind the tree"
//   illusion comes straight out of those colored bands.
//
// Controls:
//   Mouse left         walk to / act on whatever is under the cursor
//   Mouse right        cycle verb (WALK / LOOK / TAKE)
//   SPACE              cycle verb (one-button-mouse fallback)
//   1 / 2 / 3          jump straight to a verb
//   P                  toggle priority-screen overlay
//   C                  toggle control-screen overlay
//   N                  return to normal view (no overlay)
//   ESC                quit
//
// The forest is the only room. The point of this demo is the engine,
// not the game content. Walk behind any tree; the per-pixel masking
// is what makes it look right.

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

#include <joey/joey.h>


/* ===================================================================
 * Section 1 -- Layout, priority bands, control codes
 * =================================================================== */

#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
#define INV_BAR_Y        (PLAY_AREA_H + MSG_BAR_H)

/* Priority bands: sky (y<70) is priority 0, ground (y=70..167) is
 * split into 14 bands, each PRI_BAND_HEIGHT pixels tall. A picture
 * pixel with priority P obscures any actor with priority < P. An
 * actor's priority is derived from its feet Y via the same mapping
 * so depth ordering "just works" as the actor walks up and down
 * the scene. */
#define PRI_BAND_TOP      70
#define PRI_BAND_COUNT    14
#define PRI_BAND_HEIGHT   ((PLAY_AREA_H - PRI_BAND_TOP) / PRI_BAND_COUNT)
#define PRI_SKY           0
#define PRI_MAX           14

/* Control colors. 4-bit values stored as the pixel value in the
 * control surface. Sierra used 16 slots but most demos only need a
 * handful. */
#define CTRL_BLOCK         0   /* unwalkable */
#define CTRL_WALK          1   /* walkable floor */
#define CTRL_TRIGGER_LOOK  2   /* "you smell the forest" trigger zone */

/* Pathfinding grid -- one cell per 4x4 pixel block, queried against
 * the control surface (sampled at the cell centre). */
#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 */

/* Ego dimensions and animation. Larger sprite would be more
 * impressive but per-pixel priority masking scales linearly with
 * ego pixel count, so 16x24 = 384 pixels/frame is the comfortable
 * sweet spot on the slowest target (IIgs 2.8 MHz). */
#define EGO_W             16
#define EGO_H             24
#define EGO_FRAMES        3    /* idle + 2 walk frames */

/* Palettes. */
#define PAL_SCENE         0
#define PAL_DEBUG_PRI     1
#define PAL_DEBUG_CTRL    2
#define PAL_UI            3

/* Scene palette slots (same convention as adventure.c). */
#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

/* UI palette slots. */
#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_COUNT
} VerbE;

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

typedef enum {
    VIEW_NORMAL = 0,
    VIEW_PRIORITY,
    VIEW_CONTROL,
    VIEW_COUNT
} DebugViewE;


/* ===================================================================
 * Section 2 -- Globals
 * =================================================================== */

static jlSurfaceT *gScreen;
static jlSurfaceT *gVisual;     /* room background -- "picture" buffer */
static jlSurfaceT *gPriority;   /* per-pixel priority codes */
static jlSurfaceT *gControl;    /* per-pixel control codes */

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

static int16_t     gPathX[256];
static int16_t     gPathY[256];
static uint16_t    gPathLen;
static uint16_t    gPathStep;

static VerbE       gCurVerb;
static char        gMessage[80];
static uint8_t     gMessageTtl;
static DebugViewE  gDebugView;


/* ===================================================================
 * Section 3 -- ASCII-row sprite authoring (same idea as adventure.c)
 * =================================================================== */

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 'b':           return C_BLUE;
        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;
    }
}


/* Discriminate "trunk" pixels (which set CTRL_BLOCK so the actor
 * can't walk through them) from "foliage" pixels (which don't,
 * because the actor walks UNDER the canopy). Used when painting
 * the trees into the parallel screens. */
static bool isTrunkChar(char c) {
    return c == 'n' || c == 'N';
}


/* One TU's code can overflow the 65816's 64 KB code bank if it lands
 * in a single segment. Split the heavier sections across segments to
 * spread the load across multiple banks. */
#ifdef JOEYLIB_PLATFORM_IIGS
#endif


/* ===================================================================
 * Section 4 -- Sprite ASCII art (ego + cursors)
 * =================================================================== */

#define EGO_W_PAD 16
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#      ", "  #bbbbbb#      ", "   #bbbb#       ",
    "  #bb##         ", "  #b#           ", " ##             ",
    "                ", "                ", "                ",
};
static const char * const kEgoWalk2S[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 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#    ", "         ##     ", "                ",
    "                ", "                ", "                ",
};

static const char * const * const gEgoFrames[DIR_COUNT][EGO_FRAMES] = {
    { kEgoIdleS, kEgoWalk1S, kEgoWalk2S },
    { kEgoIdleE, kEgoWalk1E, kEgoWalk2E },
    { kEgoIdleN, kEgoWalk1N, kEgoWalk2N },
    { kEgoIdleW, kEgoWalk1W, kEgoWalk2W },
};


/* Cursor sprites: 16x16 each, 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 * const gCursorFrames[VERB_COUNT] = {
    kCursorWalk, kCursorLook, kCursorTake
};


/* ===================================================================
 * Section 5 -- Prop ASCII art and per-room placement
 *
 * Each "prop" gets a base Y (its anchor on the ground). The base Y
 * drives:
 *   - the prop's priority value (so the actor's depth ordering works
 *     when walking past it)
 *   - the prop's vertical placement
 *
 * Foliage cells in a tree do NOT set the control screen to BLOCKED
 * (the actor walks under the canopy). Only TRUNK cells block. The
 * isTrunkChar() predicate above is what tells the paint pass which
 * pixels are which.
 * =================================================================== */

#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 ROCK_W   24
#define ROCK_H   12

static const char * const kPropRock[ROCK_H] = {
    "                        ",
    "        WWWWWW          ",
    "      WWWWWWWWWW        ",
    "    WWWWWWWWWWWWWW      ",
    "   WWkWWWWWWWWWWWWW     ",
    "   WWWWWWWWWWWWWWkW     ",
    "   WkWWWWWWWWWWWWWW     ",
    "   WWWWWWWWWWWWWWWW     ",
    "    WWWWWWWWWWWWWW      ",
    "      WWWWWWWWWW        ",
    "        WWWWWW          ",
    "                        ",
};

#define GRASS_W  8
#define GRASS_H  8

static const char * const kPropGrass[GRASS_H] = {
    "  g     ",
    " ggg g  ",
    " gggGg  ",
    "ggGgGgg ",
    "ggGgGgg ",
    " gGgGg  ",
    "  gGg   ",
    "   g    ",
};


typedef struct {
    int16_t              baseX;     /* top-left of the prop sprite on the visual screen */
    int16_t              baseY;
    int16_t              w;
    int16_t              h;
    const char * const  *rows;
    bool                 blocksWalk;  /* if false, only trunk-class chars block */
} PropDef;

/* Five trees, three grass tufts, two rocks. Base Ys span the ground
 * area so the priority bands are all exercised. */
static const PropDef kProps[] = {
    /* Trees: trunk chars block, foliage chars don't. */
    {  20,  82, TREE_W, TREE_H, kPropTree,  false },
    {  72,  98, TREE_W, TREE_H, kPropTree,  false },
    { 148,  74, TREE_W, TREE_H, kPropTree,  false },
    { 220,  92, TREE_W, TREE_H, kPropTree,  false },
    { 268, 110, TREE_W, TREE_H, kPropTree,  false },
    /* Rocks: every non-transparent pixel blocks. */
    { 100, 138, ROCK_W, ROCK_H, kPropRock,  true  },
    { 200, 150, ROCK_W, ROCK_H, kPropRock,  true  },
    /* Grass tufts: never block (the actor walks through them). */
    {  40, 152, GRASS_W, GRASS_H, kPropGrass, false },
    { 180, 158, GRASS_W, GRASS_H, kPropGrass, false },
    { 260, 152, GRASS_W, GRASS_H, kPropGrass, false },
};
#define PROP_COUNT  ((int)(sizeof(kProps)/sizeof(kProps[0])))


/* ===================================================================
 * Section 6 -- Tiny 5x7 font (lifted from adventure.c for self-
 * containment).
 * =================================================================== */

#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 '!' */
};

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');
    if (c == '.') return 37;
    if (c == ',') return 38;
    if (c == ':') return 39;
    if (c == '!') return 40;
    return 0;
}

static void drawText(int16_t x, int16_t y, const char *text, uint8_t color) {
    int16_t cx;
    int     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; }
    }
}


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;
}


/* ===================================================================
 * Section 7 -- Palettes (scene, debug-priority rainbow,
 *                       debug-control categorical, UI)
 * =================================================================== */

static uint16_t rgb(uint8_t r, uint8_t g, uint8_t b) {
    return (uint16_t)(((r & 0x0F) << 8) | ((g & 0x0F) << 4) | (b & 0x0F));
}


static void buildPalettes(void) {
    uint16_t scene[16];
    uint16_t pri[16];
    uint16_t ctl[16];
    uint16_t ui[16];
    uint16_t i;

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

    /* Scene palette: matches the C_* indices above. */
    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);

    /* Debug-priority palette: 0..14 rainbow ramp, plus white for 15
     * (unused in our band scheme). Priority 0 = deep blue (sky/back),
     * priority 14 = bright red (foreground). Easy to read at a
     * glance. */
    pri[0]  = rgb(0,  0,  5);
    pri[1]  = rgb(0,  2,  9);
    pri[2]  = rgb(0,  5,  12);
    pri[3]  = rgb(0,  9,  13);
    pri[4]  = rgb(0,  12, 11);
    pri[5]  = rgb(2,  13, 6);
    pri[6]  = rgb(6,  14, 3);
    pri[7]  = rgb(10, 14, 0);
    pri[8]  = rgb(14, 13, 0);
    pri[9]  = rgb(14, 10, 0);
    pri[10] = rgb(14, 7,  0);
    pri[11] = rgb(14, 4,  0);
    pri[12] = rgb(14, 2,  2);
    pri[13] = rgb(14, 0,  4);
    pri[14] = rgb(14, 0,  8);
    pri[15] = rgb(15, 15, 15);

    /* Debug-control palette: categorical. 0 = solid red (blocked),
     * 1 = mid green (walk), 2 = bright yellow (trigger). Others are
     * gray to highlight any unintended values. */
    ctl[CTRL_BLOCK]        = rgb(13, 1,  1);
    ctl[CTRL_WALK]         = rgb(2,  10, 3);
    ctl[CTRL_TRIGGER_LOOK] = rgb(15, 14, 3);
    for (i = 3; i < 16; i++) { ctl[i] = rgb(6, 6, 6); }

    /* UI palette: bottom message + 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(gScreen, PAL_SCENE,      scene);
    jlPaletteSet(gScreen, PAL_DEBUG_PRI,  pri);
    jlPaletteSet(gScreen, PAL_DEBUG_CTRL, ctl);
    jlPaletteSet(gScreen, PAL_UI,         ui);
}


/* The stage's SCB selects which of the 4 palettes is active on each
 * scanline. Normal view uses PAL_SCENE for the play area and PAL_UI
 * for the message/inventory bars. Debug views swap PAL_SCENE for
 * the matching debug palette. */
static void programScbForView(DebugViewE view) {
    int16_t line;
    uint8_t playPal;

    switch (view) {
        case VIEW_PRIORITY: playPal = PAL_DEBUG_PRI;  break;
        case VIEW_CONTROL:  playPal = PAL_DEBUG_CTRL; break;
        default:            playPal = PAL_SCENE;      break;
    }
    for (line = 0; line < PLAY_AREA_H; line++) {
        jlScbSet(gScreen, (uint16_t)line, playPal);
    }
    for (line = PLAY_AREA_H; line < SURFACE_HEIGHT; line++) {
        jlScbSet(gScreen, (uint16_t)line, PAL_UI);
    }
}


#ifdef JOEYLIB_PLATFORM_IIGS
#endif


/* ===================================================================
 * Section 8 -- Painting the three room screens (visual / priority /
 *              control)
 *
 * Each pass operates on the same logical scene -- the difference is
 * what value gets written at each pixel. The PROPS pass writes to
 * all three at once because the prop's screen footprint is shared.
 * =================================================================== */

/* Map a screen Y to a priority band 0..14. The sky band sits above
 * PRI_BAND_TOP; everything below is divided into PRI_BAND_COUNT
 * even-height bands. Higher Y -> higher priority. */
static uint8_t priorityForY(int16_t y) {
    int16_t band;
    if (y < PRI_BAND_TOP) {
        return PRI_SKY;
    }
    band = (int16_t)((y - PRI_BAND_TOP) / PRI_BAND_HEIGHT);
    band++;
    if (band > PRI_MAX) { band = PRI_MAX; }
    return (uint8_t)band;
}


/* Paint the visual buffer: a normal-looking forest scene, drawn
 * with JoeyLib primitives only (no asset files). */
static void paintForestVisual(void) {
    int16_t y;
    int16_t i;

    jlSurfaceClear(gVisual, C_DARK_GREEN);

    /* Sky band. */
    jlFillRect(gVisual, 0, 0, SURFACE_WIDTH, PRI_BAND_TOP, C_BLUE);

    /* Far tree-line silhouette at the horizon. */
    for (i = 0; i < SURFACE_WIDTH; i += 16) {
        jlFillCircle(gVisual, i, (int16_t)(PRI_BAND_TOP - 6), 6, C_DARK_GREEN);
    }

    /* Ground gradient: brighter green stripes near the camera. */
    for (y = PRI_BAND_TOP + 4; y < PLAY_AREA_H; y += 8) {
        jlDrawLine(gVisual, 0, y, SURFACE_WIDTH - 1, y, C_GREEN);
    }

    /* A vague dirt path that hints at the east-exit trigger zone
     * but does NOT correspond perfectly to it -- the trigger logic
     * is in the control buffer, the path is just visual flavor. */
    jlFillRect(gVisual, SURFACE_WIDTH - 30, 130, 30, 14, C_TAN);
    jlFillRect(gVisual, SURFACE_WIDTH - 28, 132, 28, 10, C_BROWN);
}


/* Paint the priority buffer:
 *   - Sky (y < PRI_BAND_TOP): solid PRI_SKY (0). The actor never
 *     stands here; the sky is always behind everything.
 *   - Ground area: filled rows of priority 1..PRI_MAX, each
 *     PRI_BAND_HEIGHT pixels tall, increasing with Y.
 *
 * The PROPS pass below will overwrite this with each prop's
 * single priority value, covering the prop's exact pixel footprint.
 * That's how a 40-pixel-tall tree gets a uniform priority equal to
 * its base Y's band -- the actor can be in front of the WHOLE tree
 * or behind the WHOLE tree, no in-between, exactly as in SCI. */
static void paintForestPriority(void) {
    int16_t y;

    /* Sky band. */
    jlFillRect(gPriority, 0, 0, SURFACE_WIDTH, PRI_BAND_TOP, PRI_SKY);

    /* Ground bands. */
    for (y = PRI_BAND_TOP; y < PLAY_AREA_H; y++) {
        jlDrawLine(gPriority, 0, y, SURFACE_WIDTH - 1, y, priorityForY(y));
    }
}


/* Paint the control buffer:
 *   - Sky band: CTRL_BLOCK (the actor can't stand on the sky).
 *   - Ground area: CTRL_WALK.
 *   - Right-edge column: CTRL_TRIGGER_LOOK as a sample trigger zone
 *     so we can demonstrate non-walk control codes.
 *
 * Props patch in their own collision pixels in the PROPS pass: tree
 * trunks block, foliage doesn't, rocks block entirely. */
static void paintForestControl(void) {
    int16_t y;

    jlFillRect(gControl, 0, 0, SURFACE_WIDTH, PRI_BAND_TOP, CTRL_BLOCK);
    jlFillRect(gControl, 0, PRI_BAND_TOP, SURFACE_WIDTH, PLAY_AREA_H - PRI_BAND_TOP, CTRL_WALK);

    /* East-edge trigger zone: a 14 px wide vertical strip the actor
     * walks into to "smell the deeper forest." Painted only over the
     * walkable region so the actor can step into it. */
    for (y = PRI_BAND_TOP; y < PLAY_AREA_H; y++) {
        jlDrawLine(gControl, SURFACE_WIDTH - 14, y, SURFACE_WIDTH - 1, y, CTRL_TRIGGER_LOOK);
    }
}


/* Stamp one prop into all three screens. */
static void paintProp(const PropDef *p) {
    int16_t r;
    int16_t c;
    int16_t px;
    int16_t py;
    char    ch;
    uint8_t color;
    uint8_t pri;

    pri = priorityForY((int16_t)(p->baseY + p->h - 1));   /* prop priority = priority at its bottom */

    for (r = 0; r < p->h; r++) {
        py = (int16_t)(p->baseY + r);
        if (py < 0 || py >= PLAY_AREA_H) { continue; }
        for (c = 0; c < p->w; c++) {
            ch = p->rows[r][c];
            color = pixCharToColor(ch);
            if (color == C_TRANS) { continue; }
            px = (int16_t)(p->baseX + c);
            if (px < 0 || px >= SURFACE_WIDTH) { continue; }

            jlDrawPixel(gVisual,   px, py, color);
            jlDrawPixel(gPriority, px, py, pri);

            if (p->blocksWalk) {
                jlDrawPixel(gControl, px, py, CTRL_BLOCK);
            } else if (isTrunkChar(ch)) {
                /* Tree trunks block; foliage doesn't. The actor walks
                 * under the canopy because the control screen is
                 * untouched at those pixels. */
                jlDrawPixel(gControl, px, py, CTRL_BLOCK);
            }
        }
    }
}


static void paintAllProps(void) {
    int i;
    for (i = 0; i < PROP_COUNT; i++) {
        paintProp(&kProps[i]);
    }
}


/* ===================================================================
 * Section 9 -- Per-pixel actor draw against the priority screen
 *
 * For each non-transparent pixel of the ego sprite, we sample the
 * priority buffer at the same screen coordinate. If the picture's
 * priority at that pixel exceeds the actor's priority, we leave the
 * picture pixel alone -- the picture is in front of the actor.
 * Otherwise we draw the actor pixel.
 *
 * This is the same algorithm SCI used. Per-pixel jlSamplePixel +
 * jlDrawPixel is slower than the compiled-sprite path JoeyLib
 * provides, but it's faithful to the engine being demonstrated; the
 * actor footprint (16x24 = 384 candidate pixels) keeps the cost
 * manageable even on a 2.8 MHz IIgs.
 * =================================================================== */

static uint8_t actorPriority(int16_t feetY) {
    return priorityForY(feetY);
}


static void drawEgoMasked(int16_t x, int16_t y, DirE dir, uint8_t frame) {
    const char * const *rows;
    int16_t r;
    int16_t c;
    int16_t px;
    int16_t py;
    uint8_t actorPri;
    uint8_t picPri;
    char    ch;
    uint8_t color;

    rows = gEgoFrames[dir][frame];
    actorPri = actorPriority((int16_t)(y + EGO_H - 1));    /* feet = bottom of sprite */

    for (r = 0; r < EGO_H; r++) {
        py = (int16_t)(y + r);
        if (py < 0 || py >= PLAY_AREA_H) { continue; }
        for (c = 0; c < EGO_W_PAD; c++) {
            ch = rows[r][c];
            color = pixCharToColor(ch);
            if (color == C_TRANS) { continue; }
            px = (int16_t)(x + c);
            if (px < 0 || px >= SURFACE_WIDTH) { continue; }

            picPri = jlSamplePixel(gPriority, px, py);
            if (actorPri < picPri) {
                /* Picture pixel is in front. Skip the actor pixel. */
                continue;
            }
            jlDrawPixel(gScreen, px, py, color);
        }
    }
}


#ifdef JOEYLIB_PLATFORM_IIGS
#endif


/* ===================================================================
 * Section 10 -- Pathfinding via the control buffer
 *
 * The walk-mask in the original adventure example was an authored
 * grid. Here we just SAMPLE the control screen at each cell's center
 * pixel; if it reports CTRL_WALK or any trigger code, the cell is
 * walkable. CTRL_BLOCK cells (tree trunks, rocks, sky) are skipped.
 * =================================================================== */

static bool cellWalkable(int16_t cx, int16_t cy) {
    int16_t px;
    int16_t py;
    uint8_t v;

    if (cx < 0 || cx >= WALK_GRID_W || cy < 0 || cy >= WALK_GRID_H) {
        return false;
    }
    px = (int16_t)((cx * WALK_CELL_PX) + (WALK_CELL_PX / 2));
    py = (int16_t)((cy * WALK_CELL_PX) + (WALK_CELL_PX / 2));
    v = jlSamplePixel(gControl, px, py);
    return v != CTRL_BLOCK;
}


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];


static bool findPath(int16_t sx, int16_t sy, int16_t tx, int16_t ty) {
    int16_t qhead;
    int16_t qtail;
    int16_t cx, cy;
    int16_t nx, ny;
    uint8_t d;
    int16_t scx, scy, tcx, tcy;
    int16_t row, col;

    scx = (int16_t)(sx / WALK_CELL_PX); scy = (int16_t)(sy / WALK_CELL_PX);
    tcx = (int16_t)(tx / WALK_CELL_PX); tcy = (int16_t)(ty / WALK_CELL_PX);

    /* Snap unwalkable target to nearest walkable neighbor. Keeps
     * "click on a tree trunk" working: the actor walks to the foot
     * of the tree, not into the trunk pixels. */
    if (!cellWalkable(tcx, tcy)) {
        int16_t r;
        int16_t dx, dy;
        bool    found;
        found = false;
        for (r = 1; r < 12 && !found; r++) {
            for (dy = -r; dy <= r && !found; dy++) {
                for (dx = -r; dx <= r && !found; dx++) {
                    if (cellWalkable((int16_t)(tcx + dx), (int16_t)(tcy + dy))) {
                        tcx = (int16_t)(tcx + dx);
                        tcy = (int16_t)(tcy + dy);
                        found = true;
                    }
                }
            }
        }
        if (!found) { gPathLen = 0; return false; }
    }

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

    qhead = 0;
    qtail = 0;
    gBfsQueueX[qtail] = scx;
    gBfsQueueY[qtail] = scy;
    qtail++;
    gBfsParent[scy][scx] = 0xFE;

    while (qhead < qtail) {
        cx = gBfsQueueX[qhead];
        cy = gBfsQueueY[qhead];
        qhead++;
        if (cx == tcx && cy == tcy) { break; }
        for (d = 0; d < 4; d++) {
            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 (!cellWalkable(nx, ny))   { continue; }
            if (gBfsParent[ny][nx] != 0xFF) { continue; }
            gBfsParent[ny][nx] = d;
            gBfsQueueX[qtail] = nx;
            gBfsQueueY[qtail] = ny;
            qtail++;
        }
    }

    if (gBfsParent[tcy][tcx] == 0xFF) { gPathLen = 0; return false; }

    {
        int16_t pathRevX[256];
        int16_t pathRevY[256];
        int16_t n;
        int16_t i;

        n = 0;
        cx = tcx; cy = tcy;
        while (n < 256) {
            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--) {
            gPathX[gPathLen] = (int16_t)((pathRevX[i] * WALK_CELL_PX) + (WALK_CELL_PX / 2));
            gPathY[gPathLen] = (int16_t)((pathRevY[i] * WALK_CELL_PX) + (WALK_CELL_PX / 2));
            gPathLen++;
            if (gPathLen >= 256) { break; }
        }
        gPathStep = 0;
    }

    return gPathLen > 0;
}


/* ===================================================================
 * Section 11 -- Ego motion
 * =================================================================== */

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);
}

#define EGO_WALK_SPEED  1
#define ANIM_TICKS      6

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) {
        egoSetFeet(tx, ty);
        gPathStep++;
        if (gPathStep >= gPathLen) {
            gEgoMoving = false;
            gEgoFrameIdx = 0;
        }
        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; }
    }
}


/* Check the control buffer at the actor's feet and fire any
 * trigger that lives there. Called once per frame after motion. */
static void runFootTriggers(void) {
    uint8_t v;
    v = jlSamplePixel(gControl, egoFeetX(), (int16_t)(egoFeetY() - 1));
    if (v == CTRL_TRIGGER_LOOK) {
        if (gMessageTtl == 0) {
            setMessage("You smell pine and damp earth.");
        }
    }
}


/* ===================================================================
 * Section 12 -- Verb cursor + UI
 * =================================================================== */

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


/* Draw the verb cursor at the mouse position. Like the actor draw,
 * cursors are walked from ASCII rows and plotted via jlDrawPixel.
 * Cursors do NOT consult the priority buffer -- the cursor is
 * always on top. */
static void drawCursor(int16_t mx, int16_t my, VerbE v) {
    const char * const *rows;
    int16_t r;
    int16_t c;
    int16_t px, py;
    uint8_t color;

    rows = gCursorFrames[v];
    for (r = 0; r < 16; r++) {
        py = (int16_t)(my - 6 + r);
        if (py < 0 || py >= SURFACE_HEIGHT) { continue; }
        for (c = 0; c < 16; c++) {
            px = (int16_t)(mx - 6 + c);
            if (px < 0 || px >= SURFACE_WIDTH) { continue; }
            color = pixCharToColor(rows[r][c]);
            if (color == C_TRANS) { continue; }
            jlDrawPixel(gScreen, px, py, color);
        }
    }
}


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') {
        drawText(4, (int16_t)(MSG_BAR_Y + 2), gMessage, UI_INK);
    }
}


static void drawInventoryBar(void) {
    char buf[40];
    const char *vw;

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

    snprintf(buf, sizeof(buf), "VERB: %s", verbName(gCurVerb));
    drawText(4, (int16_t)(INV_BAR_Y + 2), buf, UI_HILITE);

    switch (gDebugView) {
        case VIEW_PRIORITY: vw = "VIEW: PRIORITY (C ctrl, N normal)"; break;
        case VIEW_CONTROL:  vw = "VIEW: CONTROL  (P pri,  N normal)"; break;
        default:            vw = "VIEW: NORMAL   (P pri,  C ctrl)";   break;
    }
    drawText(4, (int16_t)(INV_BAR_Y + 11), vw, UI_INK);
}


#ifdef JOEYLIB_PLATFORM_IIGS
#endif


/* ===================================================================
 * Section 13 -- Frame compositor
 * =================================================================== */

static void composeFrameNormal(int16_t mx, int16_t my) {
    jlSurfaceCopy(gScreen, gVisual);
    drawEgoMasked(gEgoX, gEgoY, gEgoDir, gEgoFrameIdx);
    drawMessageBar();
    drawInventoryBar();
    drawCursor(mx, my, gCurVerb);
    jlStagePresent();
}


static void composeFrameDebug(int16_t mx, int16_t my) {
    /* The debug overlays blit the raw priority/control buffer as the
     * play-area picture; the palette swap (PAL_DEBUG_PRI or
     * PAL_DEBUG_CTRL) makes the codes visible. */
    jlSurfaceCopy(gScreen, (gDebugView == VIEW_PRIORITY) ? gPriority : gControl);
    /* Show where the actor is by overlaying ego pixels in pure white
     * with priority always-on. */
    {
        const char * const *rows;
        int16_t r;
        int16_t c;
        int16_t px;
        int16_t py;
        rows = gEgoFrames[gEgoDir][gEgoFrameIdx];
        for (r = 0; r < EGO_H; r++) {
            py = (int16_t)(gEgoY + r);
            if (py < 0 || py >= PLAY_AREA_H) { continue; }
            for (c = 0; c < EGO_W_PAD; c++) {
                px = (int16_t)(gEgoX + c);
                if (px < 0 || px >= SURFACE_WIDTH) { continue; }
                if (pixCharToColor(rows[r][c]) == C_TRANS) { continue; }
                jlDrawPixel(gScreen, px, py, 15);
            }
        }
    }
    drawMessageBar();
    drawInventoryBar();
    drawCursor(mx, my, gCurVerb);
    jlStagePresent();
}


/* ===================================================================
 * Section 14 -- Click handling
 * =================================================================== */

static void cycleVerb(void) {
    gCurVerb = (VerbE)((gCurVerb + 1) % VERB_COUNT);
}


static void handleClick(int16_t mx, int16_t my, bool leftClick, bool rightClick) {
    uint8_t ctrlHere;

    if (rightClick) { cycleVerb(); return; }
    if (!leftClick) { return; }
    if (my >= PLAY_AREA_H) { return; }

    /* Inspect the control screen under the click. Trigger codes
     * give the player a "look" hint without walking. */
    ctrlHere = jlSamplePixel(gControl, mx, my);

    switch (gCurVerb) {
        case VERB_LOOK:
            if (ctrlHere == CTRL_BLOCK) {
                /* Could be sky, tree trunk, rock... distinguish by
                 * looking at the priority value at the same pixel. */
                uint8_t pri;
                pri = jlSamplePixel(gPriority, mx, my);
                if (pri == PRI_SKY) {
                    setMessage("Blue sky through the canopy.");
                } else {
                    uint8_t vis;
                    vis = jlSamplePixel(gVisual, mx, my);
                    if (vis == C_GRAY || vis == C_WHITE || vis == C_DARK_GRAY) {
                        setMessage("A weather-rounded rock.");
                    } else {
                        setMessage("A tall, narrow tree.");
                    }
                }
            } else if (ctrlHere == CTRL_TRIGGER_LOOK) {
                setMessage("The forest is deeper here. Pine and damp earth.");
            } else {
                setMessage("Soft, well-trodden ground.");
            }
            return;

        case VERB_TAKE:
            if (ctrlHere == CTRL_BLOCK) {
                setMessage("You can't pick that up.");
            } else {
                setMessage("There is nothing here to take.");
            }
            return;

        case VERB_WALK:
        default:
            if (findPath(egoFeetX(), egoFeetY(), mx, my)) {
                gEgoMoving = true;
            }
            return;
    }
}


#ifdef JOEYLIB_PLATFORM_IIGS
#endif


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

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

    /* No compiled sprites used in this demo -- everything is plotted
     * pixel-by-pixel against the priority screen. Codegen budget can
     * be small. We need 4 surfaces (stage + visual + priority +
     * control). */
    config.codegenBytes = 4UL * 1024;
    config.audioBytes   = 0;

    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;
    }

    gVisual   = jlSurfaceCreate();
    gPriority = jlSurfaceCreate();
    gControl  = jlSurfaceCreate();
    if (gVisual == NULL || gPriority == NULL || gControl == NULL) {
        fprintf(stderr, "Failed to allocate parallel screens: %s\n", jlLastError());
        jlShutdown();
        return 1;
    }

    buildPalettes();
    gDebugView = VIEW_NORMAL;
    programScbForView(gDebugView);

    paintForestVisual();
    paintForestPriority();
    paintForestControl();
    paintAllProps();

    /* Plant the ego near the front of the scene, idle, facing south. */
    egoSetFeet(SURFACE_WIDTH / 2, 150);
    gEgoDir      = DIR_S;
    gEgoFrameIdx = 0;
    gEgoAnimTick = 0;
    gEgoMoving   = false;
    gPathLen = 0; gPathStep = 0;

    gCurVerb     = VERB_WALK;
    gMessage[0]  = '\0';
    gMessageTtl  = 0;
    setMessage("Click to walk. P = priority view, C = control view.");

    for (;;) {
        int16_t mx, 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_SPACE))  { cycleVerb(); }

        /* Debug overlay toggles. Letters chosen because the IIgs's
         * stock Apple Keyboard II has no function keys; P/C/N work
         * on every target's standard keyboard. */
        if (jlKeyPressed(KEY_P)) {
            gDebugView = (gDebugView == VIEW_PRIORITY) ? VIEW_NORMAL : VIEW_PRIORITY;
            programScbForView(gDebugView);
        }
        if (jlKeyPressed(KEY_C)) {
            gDebugView = (gDebugView == VIEW_CONTROL)  ? VIEW_NORMAL : VIEW_CONTROL;
            programScbForView(gDebugView);
        }
        if (jlKeyPressed(KEY_N)) {
            gDebugView = VIEW_NORMAL;
            programScbForView(gDebugView);
        }

        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();
        runFootTriggers();

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

        if (gDebugView == VIEW_NORMAL) {
            composeFrameNormal(mx, my);
        } else {
            composeFrameDebug(mx, my);
        }
    }

    jlShutdown();
    return 0;
}
