joeylib2/examples/adventure2/adventure2.c

1450 lines
52 KiB
C

// 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';
}
/* ORCA-C's auto-segmenter splits at function boundaries; one TU's
* code can still overflow the 65816's 64 KB code bank if it lands
* in a single segment. Sprinkle ORCA-only segment directives at the
* heavier sections to spread the load across multiple banks. */
#ifdef JOEYLIB_PLATFORM_IIGS
segment "ADV2ART";
#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
segment "ADV2PAINT";
#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
segment "ADV2PATH";
#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
segment "ADV2COMP";
#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
segment "";
#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.maxSurfaces = 8;
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;
}