1450 lines
52 KiB
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;
|
|
}
|