joeylib2/examples/adventure/adventure.c

2530 lines
78 KiB
C

// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
// See LICENSE.md for license information.
// SPDX-License-Identifier: MIT
//
// Sierra-style mini-adventure built on JoeyLib.
//
// What this demonstrates:
// - Multi-room scene management (two rooms, edge-exit transitions)
// - Procedural 320x200x16 backgrounds drawn from JoeyLib primitives
// (no external art files) -- portable across all four platforms
// - SCB per-line palettes for the sky gradient (showcases the
// library's IIgs-style SCB model on Amiga/ST copper-emulated)
// - 4-direction animated ego sprite with sprite save/restore
// - SCI-style verb cursor (WALK / LOOK / TAKE / USE / TALK),
// right-click to cycle verb, left-click to act
// - Hotspots with per-verb response strings
// - Inventory bar with item pickup and item-on-hotspot use
// - Grid pathfinding over a per-room walk-mask
// - Sierra priority illusion: foreground props redraw on top of the
// ego whenever their bottom-Y is greater than the ego's bottom-Y,
// so the character "walks behind" tall objects
// - Message bar for narration / hotspot text
//
// Controls:
// Mouse - left-click to act with the current verb on the
// thing under the cursor (or walk if no hotspot)
// Right-click - cycle verb (WALK / LOOK / TAKE / USE / TALK)
// Space - cycle verb (one-button-mouse fallback for IIgs)
// 1..5 - jump straight to a verb
// I - toggle "look at inventory" cursor mode
// Click item - select item; cursor becomes the item icon. Next
// left-click on a hotspot triggers the item-on-target
// handler; right-click clears item selection.
// ESC - quit
//
// This is intentionally one file: every system (art, engine, rooms,
// pathfinding, UI) lives below as a labelled section so a reader can
// follow the whole engine without chasing headers.
#include <stdio.h>
#include <string.h>
#include <joey/joey.h>
/* ===================================================================
* Section 1 -- Layout, constants, types
* =================================================================== */
#define MSG_BAR_H 12
#define INV_BAR_H 20
#define PLAY_AREA_H (JL_SURFACE_HEIGHT - MSG_BAR_H - INV_BAR_H) // 168
#define MSG_BAR_Y PLAY_AREA_H // 168
#define INV_BAR_Y (PLAY_AREA_H + MSG_BAR_H) // 180
#define WALK_CELL_PX 4
#define WALK_GRID_W (JL_SURFACE_WIDTH / WALK_CELL_PX) // 80
#define WALK_GRID_H (PLAY_AREA_H / WALK_CELL_PX) // 42
#define EGO_W 16
#define EGO_H 24
#define EGO_TILES_X (EGO_W / 8) // 2
#define EGO_TILES_Y (EGO_H / 8) // 3
#define EGO_TILE_BYTES (EGO_TILES_X * EGO_TILES_Y * JL_TILE_BYTES) // 192
/* The save-under buffers are sized by jlSpriteBackupLength
* (joey/sprite.h): the worst case at any x, on every port. */
#define CURSOR_W 12
#define CURSOR_H 12
#define CURSOR_TILES_X 2
#define CURSOR_TILES_Y 2
#define ITEM_ICON_W 16
#define ITEM_ICON_H 16
#define MAX_HOTSPOTS 8
#define MAX_PROPS 6
#define MAX_PATH_NODES 128
#define EGO_WALK_SPEED 60 // pixels per second, the same on every port
#define EGO_STEP 1 // pixels advanceEgo moves the ego at a time
#define ANIM_TICKS 6 // steps per walk-cycle step
#define MAX_STEP_MILLIS 100 // after a stall, walk no further than this
#define MESSAGE_MILLIS 3000 // how long a message shows
#define MS_PER_SECOND 1000u // milliseconds in a second
/* Palette layout. Palette 0 is the play-area palette; palette 1 is
* the sky gradient (SCB swap by line); palette 2 is the UI palette
* (message bar / inventory bar) so the play-area colors aren't
* forced to share slots with white-on-blue UI ink. */
#define PAL_SCENE 0
#define PAL_SKY 1
#define PAL_UI 2
/* Color slots in PAL_SCENE. Slot 0 is always transparent on sprites
* and is the scene's background "void" color. */
#define C_TRANS 0
#define C_BLACK 1
#define C_WHITE 2
#define C_RED 3
#define C_DARK_GREEN 4
#define C_GREEN 5
#define C_BROWN 6
#define C_DARK_BROWN 7
#define C_TAN 8
#define C_GRAY 9
#define C_DARK_GRAY 10
#define C_YELLOW 11
#define C_ORANGE 12
#define C_BLUE 13
#define C_FLESH 14
#define C_PINK 15
/* SCB palette slots. Sky uses its own palette so the gradient bands
* don't burn play-area colors. Bands cycle through slots 1..15 in
* the sky palette as the scanline descends from the top. */
#define C_SKY0 0
/* UI palette: keep slot 0 transparent so the same sprite color
* convention works. */
#define UI_BG 1
#define UI_INK 2
#define UI_HILITE 3
#define UI_DIM 4
typedef enum {
VERB_WALK = 0,
VERB_LOOK,
VERB_TAKE,
VERB_USE,
VERB_TALK,
VERB_COUNT
} VerbE;
typedef enum {
DIR_S = 0,
DIR_E,
DIR_N,
DIR_W,
DIR_COUNT
} DirE;
typedef enum {
ROOM_FOREST = 0,
ROOM_COTTAGE,
ROOM_COUNT
} RoomE;
typedef enum {
ITEM_NONE = 0,
ITEM_LAMP,
ITEM_KEY,
ITEM_COUNT
} ItemE;
typedef enum {
HS_NONE = 0,
HS_TREE,
HS_FOUNTAIN,
HS_LAMP_ON_TABLE,
HS_TABLE,
HS_FIRE,
HS_DOOR_TO_COTTAGE,
HS_DOOR_TO_FOREST,
HS_WINDOW,
HS_COUNT
} HotspotIdE;
typedef struct {
HotspotIdE id;
int16_t x;
int16_t y;
int16_t w;
int16_t h;
const char *name;
} HotspotT;
typedef struct {
int16_t x; // top-left of prop on screen
int16_t y;
int16_t w; // pixel size (for priority compare)
int16_t h;
jlSpriteT *sp;
} PropT;
typedef struct {
/* Each row is a string of WALK_GRID_W chars: '.' walkable,
* '#' blocked. Walk-mask cells map 1:1 to walk-grid cells. */
const char *walkRows[WALK_GRID_H];
HotspotT hotspots[MAX_HOTSPOTS];
uint8_t hotspotCount;
PropT props[MAX_PROPS];
uint8_t propCount;
int16_t egoEnterFromE_x; // entry from east edge
int16_t egoEnterFromE_y;
int16_t egoEnterFromW_x; // entry from west edge
int16_t egoEnterFromW_y;
int16_t egoDefaultX; // first time in room
int16_t egoDefaultY;
} RoomT;
/* ===================================================================
* Section 2 -- Globals (game state)
* =================================================================== */
static jlSurfaceT *jlgScreen;
static jlSurfaceT *gBgScene[ROOM_COUNT]; // pristine background per room
static RoomE gCurRoom;
static RoomT gRooms[ROOM_COUNT];
// Ego sprites: [DIR][frame]. frame 0 = idle, 1 & 2 = walk cycle.
#define EGO_FRAMES 3
static jlSpriteT *gEgo[DIR_COUNT][EGO_FRAMES];
static uint8_t gEgoTiles[DIR_COUNT][EGO_FRAMES][EGO_TILE_BYTES];
static uint16_t gEgoBackup[jlSpriteBackupLength(uint16_t, EGO_W / 8, EGO_H / 8)];
static jlSpriteBackupT gEgoBak;
// Cursor sprite per verb (+ item-icon for "using" mode)
static jlSpriteT *gCursor[VERB_COUNT];
static jlSpriteT *gItemSprite[ITEM_COUNT]; // index 0 = unused
static jlSpriteT *gLampLitSprite; // lit-state inventory icon
static uint8_t gLampLitTiles[2 * 2 * JL_TILE_BYTES];
static uint16_t gCursorBackup[jlSpriteBackupLength(uint16_t, CURSOR_TILES_X, CURSOR_TILES_Y)];
static jlSpriteBackupT gCursorBak;
// Ego state
static int16_t gEgoX; // top-left pixel of sprite
static int16_t gEgoY;
static DirE gEgoDir;
static uint8_t gEgoFrameIdx;
static uint8_t gEgoAnimTick;
static bool gEgoMoving;
static uint32_t gEgoWalkCarry; // walked time not yet a step, ms x px/s
// Path
static int16_t gPathX[MAX_PATH_NODES];
static int16_t gPathY[MAX_PATH_NODES];
static uint16_t gPathLen;
static uint16_t gPathStep;
// UI / interaction
static VerbE gCurVerb;
static ItemE gUsingItem; // ITEM_NONE if not in use mode
static bool gHaveItem[ITEM_COUNT]; // index 0 unused
static char gMessage[80];
static uint32_t gMessageUntil; // jlMillisElapsed when it goes
// Flags
static bool gLampIsLit;
/* ===================================================================
* Section 3 -- Tiny helpers
* =================================================================== */
static int16_t clampi(int16_t v, int16_t lo, int16_t hi) {
if (v < lo) {
return lo;
}
if (v > hi) {
return hi;
}
return v;
}
static void setMessage(const char *text) {
size_t n;
n = strlen(text);
if (n >= sizeof(gMessage)) {
n = sizeof(gMessage) - 1;
}
memcpy(gMessage, text, n);
gMessage[n] = '\0';
gMessageUntil = jlMillisElapsed() + MESSAGE_MILLIS;
}
// RGB444 helper. JoeyLib palette entries are 0x0RGB (4 bits each).
static uint16_t rgb(uint8_t r, uint8_t g, uint8_t b) {
return (uint16_t)(((r & 0x0F) << 8) | ((g & 0x0F) << 4) | (b & 0x0F));
}
/* ===================================================================
* Section 4 -- Procedural sprite art
*
* Every sprite is authored as an ASCII "row of pixel chars" grid;
* a tiny helper converts that to JoeyLib's 4bpp packed tile-major
* layout at startup. Same approach as examples/sprite/sprite.c but
* generalised so we can author dozens of frames in one file.
*
* Char palette in the strings:
* ' ' or '.' -> transparent (color 0)
* '#' -> outline (black, C_BLACK)
* 'F' -> flesh
* 'h' / 'H' -> hair / dark hair
* 'r' / 'R' -> red / dark red (shirt)
* 'b' / 'B' -> blue / dark blue (pants)
* 'g' / 'G' -> green / dark green
* 'n' / 'N' -> brown / dark brown
* 'y' / 'Y' -> yellow / orange
* 'w' / 'W' -> white / gray
* 'T' -> tan
* 'k' -> dark gray
* 'p' -> pink
* =================================================================== */
static uint8_t pixCharToColor(char c) {
switch (c) {
case ' ':
case '.':
return C_TRANS;
case '#':
return C_BLACK;
case 'F':
return C_FLESH;
case 'h':
return C_BROWN;
case 'H':
return C_DARK_BROWN;
case 'r':
return C_RED;
case 'R':
return C_DARK_BROWN;
case 'b':
return C_BLUE;
case 'B':
return C_DARK_GRAY;
case 'g':
return C_GREEN;
case 'G':
return C_DARK_GREEN;
case 'n':
return C_BROWN;
case 'N':
return C_DARK_BROWN;
case 'y':
return C_YELLOW;
case 'Y':
return C_ORANGE;
case 'w':
return C_WHITE;
case 'W':
return C_GRAY;
case 'T':
return C_TAN;
case 'k':
return C_DARK_GRAY;
case 'p':
return C_PINK;
default:
return C_TRANS;
}
}
/* Author a sprite from `widthPx` x `heightPx` ASCII rows into the
* tile-major 4bpp packed layout jlSpriteCreate expects.
*
* Both dimensions must be multiples of 8. `rows[]` must contain
* heightPx entries, each at least widthPx chars long. */
static void authorSpriteFromRows(uint8_t *dstTiles, uint16_t widthPx, uint16_t heightPx, const char * const *rows) {
uint16_t tilesX;
uint16_t tilesY;
uint16_t tx;
uint16_t ty;
uint16_t r;
uint16_t b;
uint8_t *dst;
uint8_t pix0;
uint8_t pix1;
char c0;
char c1;
tilesX = (uint16_t)(widthPx / 8);
tilesY = (uint16_t)(heightPx / 8);
for (ty = 0; ty < tilesY; ty++) {
for (tx = 0; tx < tilesX; tx++) {
dst = &dstTiles[((ty * tilesX) + tx) * JL_TILE_BYTES];
for (r = 0; r < 8; r++) {
for (b = 0; b < 4; b++) {
// Each byte = 2 pixels: high nibble = left.
c0 = rows[(ty * 8) + r][(tx * 8) + (b * 2)];
c1 = rows[(ty * 8) + r][(tx * 8) + (b * 2) + 1];
pix0 = pixCharToColor(c0);
pix1 = pixCharToColor(c1);
dst[r * 4 + b] = (uint8_t)((pix0 << 4) | (pix1 & 0x0F));
}
}
}
}
}
/* --- Ego sprites: 16x24, 4 directions, 3 frames each (idle + 2 walk).
*
* Style: small chibi character. Brown hair, red tunic, blue pants. */
static const char * const kEgoIdleS[24] = {
" #### ",
" #HHHH# ",
" #HHhhHH# ",
" #HFFFFH# ",
" #FF##FF# ",
" #FFFF# ",
" #FF# ",
" #rrrr# ",
" #rrrrrr# ",
" #rrrrrr# ",
" #rrrrrrrr# ",
" #rrrrrrrr# ",
" #rrrrrr# ",
" #brrrrb# ",
" #bbbbbb# ",
" #bbbbbb# ",
" #bbbbbb# ",
" #bbbb# ",
" #bb## ",
" #b# ",
" ## ",
" ",
" ",
" ",
};
static const char * const kEgoWalk1S[24] = {
" #### ",
" #HHHH# ",
" #HHhhHH# ",
" #HFFFFH# ",
" #FF##FF# ",
" #FFFF# ",
" #FF# ",
" #rrrr# ",
" #rrrrrr# ",
" #rrrrrr# ",
" #rrrrrrrr# ",
" #rrrrrrrr# ",
" #rrrrrr# ",
" #brrrrb# ",
" #bbbbbb# ",
" #bbbbbb# ",
" #bbbb# ",
" #bb# ",
" #bb# ",
" ## ",
" ",
" ",
" ",
" ",
};
static const char * const kEgoWalk2S[24] = {
" #### ",
" #HHHH# ",
" #HHhhHH# ",
" #HFFFFH# ",
" #FF##FF# ",
" #FFFF# ",
" #FF# ",
" #rrrr# ",
" #rrrrrr# ",
" #rrrrrr# ",
" #rrrrrrrr# ",
" #rrrrrrrr# ",
" #rrrrrr# ",
" #brrrrb# ",
" #bbbbbb# ",
" #bbbbbb# ",
" #bbbb# ",
" #bb# ",
" #bb# ",
" ## ",
" ",
" ",
" ",
" ",
};
static const char * const kEgoIdleN[24] = {
" #### ",
" #HHHH# ",
" #HHHHHH# ",
" #HHHHHH# ",
" #HHHHHH# ",
" #HHHH# ",
" #### ",
" #rrrr# ",
" #rrrrrr# ",
" #rrrrrr# ",
" #rrrrrrrr# ",
" #rrrrrrrr# ",
" #rrrrrr# ",
" #brrrrb# ",
" #bbbbbb# ",
" #bbbbbb# ",
" #bbbbbb# ",
" #bbbb# ",
" ##bb# ",
" #b# ",
" ## ",
" ",
" ",
" ",
};
static const char * const kEgoWalk1N[24] = {
" #### ",
" #HHHH# ",
" #HHHHHH# ",
" #HHHHHH# ",
" #HHHHHH# ",
" #HHHH# ",
" #### ",
" #rrrr# ",
" #rrrrrr# ",
" #rrrrrr# ",
" #rrrrrrrr# ",
" #rrrrrrrr# ",
" #rrrrrr# ",
" #brrrrb# ",
" #bbbbbb# ",
" #bbbbbb# ",
" #bbbb# ",
" #bb# ",
" #bb# ",
" ## ",
" ",
" ",
" ",
" ",
};
static const char * const kEgoWalk2N[24] = {
" #### ",
" #HHHH# ",
" #HHHHHH# ",
" #HHHHHH# ",
" #HHHHHH# ",
" #HHHH# ",
" #### ",
" #rrrr# ",
" #rrrrrr# ",
" #rrrrrr# ",
" #rrrrrrrr# ",
" #rrrrrrrr# ",
" #rrrrrr# ",
" #brrrrb# ",
" #bbbbbb# ",
" #bbbbbb# ",
" #bbbb# ",
" #bb# ",
" #bb# ",
" ## ",
" ",
" ",
" ",
" ",
};
static const char * const kEgoIdleE[24] = {
" #### ",
" #HHHH# ",
" #HHHHhh# ",
" #FFFFhH# ",
" #F##FFH# ",
" #FFF# ",
" #FF# ",
" #rrrr# ",
" #rrrrrr# ",
" #rrrrrrrr## ",
"#rrrrrrrrFF# ",
" #rrrrrrrr# ",
" #rrrrrr# ",
" #brrrrb# ",
" #bbbbbb# ",
" #bbbbbb# ",
" #bbbbbb# ",
" #bb#bb# ",
" ## ## ",
" # # ",
" ",
" ",
" ",
" ",
};
static const char * const kEgoWalk1E[24] = {
" #### ",
" #HHHH# ",
" #HHHHhh# ",
" #FFFFhH# ",
" #F##FFH# ",
" #FFF# ",
" #FF# ",
" #rrrr# ",
" #rrrrrr# ",
" #rrrrrrrr## ",
"#rrrrrrrrFF# ",
" #rrrrrrrr# ",
" #rrrrrr# ",
" #brrrrb# ",
" #bbbbbb# ",
" #bbbbbb# ",
" #bbbb# ",
" ## #b# ",
" ## ",
" ",
" ",
" ",
" ",
" ",
};
static const char * const kEgoWalk2E[24] = {
" #### ",
" #HHHH# ",
" #HHHHhh# ",
" #FFFFhH# ",
" #F##FFH# ",
" #FFF# ",
" #FF# ",
" #rrrr# ",
" #rrrrrr# ",
" #rrrrrrrr## ",
"#rrrrrrrrFF# ",
" #rrrrrrrr# ",
" #rrrrrr# ",
" #brrrrb# ",
" #bbbbbb# ",
" #bbbbbb# ",
" #bbbb# ",
" #bb### ",
" ## ",
" ",
" ",
" ",
" ",
" ",
};
static const char * const kEgoIdleW[24] = {
" #### ",
" #HHHH# ",
" #hhHHHH# ",
" #HhFFFF# ",
" #HFF##F# ",
" #FFF# ",
" #FF# ",
" #rrrr# ",
" #rrrrrr# ",
" ##rrrrrrrr# ",
" #FFrrrrrrrr# ",
" #rrrrrrrr# ",
" #rrrrrr# ",
" #brrrrb# ",
" #bbbbbb# ",
" #bbbbbb# ",
" #bbbbbb# ",
" #bb#bb# ",
" ## ## ",
" # # ",
" ",
" ",
" ",
" ",
};
static const char * const kEgoWalk1W[24] = {
" #### ",
" #HHHH# ",
" #hhHHHH# ",
" #HhFFFF# ",
" #HFF##F# ",
" #FFF# ",
" #FF# ",
" #rrrr# ",
" #rrrrrr# ",
" ##rrrrrrrr# ",
" #FFrrrrrrrr# ",
" #rrrrrrrr# ",
" #rrrrrr# ",
" #brrrrb# ",
" #bbbbbb# ",
" #bbbbbb# ",
" #bbbb# ",
" #b# ## ",
" ## ",
" ",
" ",
" ",
" ",
" ",
};
static const char * const kEgoWalk2W[24] = {
" #### ",
" #HHHH# ",
" #hhHHHH# ",
" #HhFFFF# ",
" #HFF##F# ",
" #FFF# ",
" #FF# ",
" #rrrr# ",
" #rrrrrr# ",
" ##rrrrrrrr# ",
" #FFrrrrrrrr# ",
" #rrrrrrrr# ",
" #rrrrrr# ",
" #brrrrb# ",
" #bbbbbb# ",
" #bbbbbb# ",
" #bbbb# ",
" ###bb# ",
" #bb# ",
" ## ",
" ",
" ",
" ",
" ",
};
// --- Cursor sprites: 16x16, drawn at the mouse position.
static const char * const kCursorWalk[16] = {
" ww ",
" ww ",
" w ww w ",
" ww ww ww ",
" www ww www ",
"wwwwwwwwwwwwww ",
" www ww www ",
" ww ww ww ",
" w ww w ",
" ww ",
" ww ",
" ",
" ",
" ",
" ",
" ",
};
static const char * const kCursorLook[16] = {
" wwwwww ",
" ww ww ",
" w wwwwww w ",
"w w w w ",
"w w wwwww w w ",
"w w w bw w w ",
"w w w bbw w w ",
"w w wbbww w w ",
"w w wwwww w w ",
"w w w w ",
" w wwwwww w ",
" ww ww ",
" wwwwww ",
" ",
" ",
" ",
};
static const char * const kCursorTake[16] = {
" wwwwww ",
" wnnnnnnw ",
" wnnNNNNnnw ",
" wnnnnnnnnnnw ",
" wnnnnnnnnnnw ",
"wnnnnnnnnnnnnw ",
"wnnnnnnnnnnnnw ",
"wnnnnnnnnnnnnw ",
" wnnnnnnnnnnw ",
" wnnnnnnnnnnw ",
" wnnnnnnnnw ",
" wnnnnnnw ",
" wwwwww ",
" ",
" ",
" ",
};
static const char * const kCursorUse[16] = {
" www ",
" www ",
" wYw ",
" wYw ",
" wwYww ",
" wYYYYYw ",
" wYYYYYYYw ",
" wYYYYYYYw ",
" wYYYYYYYw ",
" wYYYYYYYw ",
" wYYYYYYYw ",
" wYYYYYw ",
" wwwww ",
" ",
" ",
" ",
};
static const char * const kCursorTalk[16] = {
" wwwwwwwwww ",
" w w ",
"w wwww wwww w ",
"w w w w w w ",
"w w w w w w ",
"w wwww wwww w ",
"w w ",
"w wwwwwww w ",
"w w ",
"w wwwwwwww w ",
" w w ",
" wwwwww w ",
" www ",
" ",
" ",
" ",
};
// --- Item-icon sprites: 16x16 for inventory bar & "using" cursor.
static const char * const kItemLampUnlit[16] = {
" ",
" #### ",
" #wwww# ",
" #wwwwww# ",
" #wwwwww# ",
" #wwwwww# ",
" #wwwwww# ",
" #wwwwww# ",
" #wwww# ",
" #### ",
" #nnnn# ",
" #nnnnnn# ",
" #nnnnnn# ",
" #nnnnnn# ",
" #nnnn# ",
" #### ",
};
static const char * const kItemLampLit[16] = {
" yyyy ",
" yyyy ",
" yYYYYy ",
" yYYYYYYy ",
" yYYYYYYy ",
" yYYYYYYy ",
" yYYYYYYy ",
" yYYYYYYy ",
" yYYYYy ",
" yyyy ",
" #nnnn# ",
" #nnnnnn# ",
" #nnnnnn# ",
" #nnnnnn# ",
" #nnnn# ",
" #### ",
};
static const char * const kItemKey[16] = {
" ",
" #### ",
" #yyyy# ",
" #yyyyyy# ",
" #yy##yy# ",
" #yy##yy# ",
" #yyyyyy# ",
" #yyyy# ",
" #yy# ",
" #yy# ",
" #yy# ",
" #yy#### ",
" #yyyyy# ",
" #yy#### ",
" #yy# ",
" #### ",
};
/* ===================================================================
* Section 5 -- Background scene drawing (procedural)
* =================================================================== */
/* Forest scene: gradient sky (SCB), distant horizon, ground, three
* trees (drawn into the BG; the prop list re-overlays the trunks
* after the ego draws so the ego walks behind them). */
static void drawForestBackground(jlSurfaceT *s) {
int16_t y;
int16_t baseY;
/* Ground plane: dark green to lighter green band. The play area
* is 168 px tall; horizon sits at y=70. */
jlSurfaceClear(s, C_DARK_GREEN);
jlFillRect(s, 0, 0, JL_SURFACE_WIDTH, 70, C_BLUE); // sky band
jlFillRect(s, 0, 70, JL_SURFACE_WIDTH, 6, C_DARK_GREEN); // far trees
// Ground gradient: brighter green near the bottom suggests depth.
for (y = 76; y < PLAY_AREA_H; y++) {
if (((y - 76) & 7) == 0) {
jlDrawLine(s, 0, y, JL_SURFACE_WIDTH - 1, y, C_GREEN);
}
}
// Distant tree-line silhouettes.
for (baseY = 64; baseY <= 72; baseY += 2) {
int16_t x;
for (x = 0; x < JL_SURFACE_WIDTH; x += 18) {
jlFillCircle(s, x, baseY, 5, C_DARK_GREEN);
}
}
/* Right edge: a clearer "doorway" gap in the trees signaling the
* exit to the cottage. Painted as a faint dirt path leading off
* the right edge. */
jlFillRect(s, JL_SURFACE_WIDTH - 40, 110, 40, 14, C_TAN);
jlFillRect(s, JL_SURFACE_WIDTH - 38, 112, 38, 10, C_BROWN);
// Decorative grass tufts on the lower band.
{
int16_t i;
for (i = 0; i < JL_SURFACE_WIDTH; i += 13) {
jlDrawLine(s, i, PLAY_AREA_H - 6, i, PLAY_AREA_H - 1, C_GREEN);
jlDrawLine(s, i + 2, PLAY_AREA_H - 4, i + 2, PLAY_AREA_H - 1, C_GREEN);
}
}
}
// Cottage interior: walls, floor, window, door, table, fireplace.
static void drawCottageBackground(jlSurfaceT *s) {
int16_t x;
int16_t y;
jlSurfaceClear(s, C_DARK_BROWN);
// Back wall (top half) tan, floor (bottom half) brown.
jlFillRect(s, 0, 0, JL_SURFACE_WIDTH, 90, C_TAN);
jlFillRect(s, 0, 90, JL_SURFACE_WIDTH, PLAY_AREA_H - 90, C_BROWN);
// Wall planks.
for (x = 0; x < JL_SURFACE_WIDTH; x += 24) {
jlDrawLine(s, x, 0, x, 88, C_DARK_BROWN);
}
// Floor planks.
for (y = 100; y < PLAY_AREA_H; y += 10) {
jlDrawLine(s, 0, y, JL_SURFACE_WIDTH - 1, y, C_DARK_BROWN);
}
// Window with sky showing through.
jlFillRect(s, 60, 18, 50, 40, C_BLACK);
jlFillRect(s, 62, 20, 46, 36, C_BLUE);
// Window cross.
jlDrawLine(s, 85, 20, 85, 56, C_BLACK);
jlDrawLine(s, 62, 38, 108, 38, C_BLACK);
/* Fireplace on the right wall: stone surround + dark hearth +
* flame body. The flame is a hotspot, not a prop, so the bg
* carries the geometry. */
jlFillRect(s, 220, 30, 70, 60, C_DARK_GRAY);
jlFillRect(s, 228, 38, 54, 50, C_BLACK);
// Logs.
jlFillRect(s, 234, 78, 42, 6, C_DARK_BROWN);
jlFillRect(s, 240, 72, 30, 5, C_BROWN);
// Flame body.
jlFillCircle(s, 254, 64, 8, C_ORANGE);
jlFillCircle(s, 254, 58, 6, C_YELLOW);
jlFillCircle(s, 254, 54, 3, C_WHITE);
// Door on the left edge: leads back to the forest.
jlFillRect(s, 8, 60, 36, 80, C_DARK_BROWN);
jlFillRect(s, 12, 64, 28, 72, C_BROWN);
jlDrawLine(s, 26, 64, 26, 136, C_DARK_BROWN);
// Doorknob.
jlFillCircle(s, 38, 100, 2, C_YELLOW);
}
/* SCB sky gradient: walk the sky band and assign sky-palette swaps
* per line. The sky palette holds 8 shades of blue across slots
* 1..8 and we map raster lines 0..69 to those. */
static void programSkyScb(jlSurfaceT *s, RoomE room) {
int16_t line;
int16_t skyBottom;
(void)room;
skyBottom = 70;
for (line = 0; line < skyBottom; line++) {
/* Sky-palette swap is via jlScbSet: pick a palette index per
* line. We use PAL_SKY for sky, PAL_SCENE for everything
* else. The sky palette itself holds the gradient. */
jlScbSet(s, (uint16_t)line, PAL_SKY);
}
for (line = skyBottom; line < PLAY_AREA_H; line++) {
jlScbSet(s, (uint16_t)line, PAL_SCENE);
}
for (line = PLAY_AREA_H; line < JL_SURFACE_HEIGHT; line++) {
jlScbSet(s, (uint16_t)line, PAL_UI);
}
}
/* Sky palette: gradient of blues from horizon (pale) to zenith
* (deep) keyed off slot index. We splat the gradient into ALL
* scene-color slots so wherever the BG draws a "blue" pixel it
* picks up the right shade for that line. The gradient lives in
* slot C_BLUE (=13) per line via... actually simpler: the
* background draws SOLID C_BLUE for the sky band, and the SCB
* swaps the palette per line so C_BLUE maps to a different RGB
* value on each scanline. Hence we want PAL_SKY's slot 13 to vary
* per line.
*
* JoeyLib's SCB model swaps the WHOLE palette per line. Implement
* the gradient by allocating a small palette pool: actually the
* library only exposes 16 palettes. So we drop the gradient
* approach and use one PAL_SKY palette with slot 13 set to a
* fixed pale-blue. The "gradient" lives in the background draw
* (raster-of-lines). Cleaner and still pretty. */
static void buildPalettes(jlSurfaceT *s) {
uint16_t scene[16];
uint16_t sky[16];
uint16_t ui[16];
uint16_t i;
for (i = 0; i < 16; i++) {
scene[i] = 0;
sky[i] = 0;
ui[i] = 0;
}
// PAL_SCENE: the play-area colors that match C_* constants.
scene[C_BLACK] = rgb(0, 0, 0);
scene[C_WHITE] = rgb(15, 15, 15);
scene[C_RED] = rgb(13, 2, 2);
scene[C_DARK_GREEN] = rgb(2, 6, 2);
scene[C_GREEN] = rgb(4, 10, 3);
scene[C_BROWN] = rgb(9, 6, 3);
scene[C_DARK_BROWN] = rgb(5, 3, 1);
scene[C_TAN] = rgb(13, 11, 7);
scene[C_GRAY] = rgb(10, 10, 10);
scene[C_DARK_GRAY] = rgb(5, 5, 5);
scene[C_YELLOW] = rgb(15, 13, 3);
scene[C_ORANGE] = rgb(14, 8, 2);
scene[C_BLUE] = rgb(3, 7, 13);
scene[C_FLESH] = rgb(14, 11, 8);
scene[C_PINK] = rgb(15, 10, 11);
/* PAL_SKY: only differs from scene in the C_BLUE slot, which is
* shifted to a pale horizon shade. Used on lines 0..69. */
for (i = 0; i < 16; i++) {
sky[i] = scene[i];
}
sky[C_BLUE] = rgb(6, 10, 15);
// PAL_UI: used on the message bar and inventory bar.
ui[UI_BG] = rgb(2, 2, 3);
ui[UI_INK] = rgb(15, 15, 15);
ui[UI_HILITE] = rgb(15, 13, 3);
ui[UI_DIM] = rgb(7, 7, 8);
jlPaletteSet(s, PAL_SCENE, scene);
jlPaletteSet(s, PAL_SKY, sky);
jlPaletteSet(s, PAL_UI, ui);
}
/* ===================================================================
* Section 6 -- Sprite construction (called once at startup)
* =================================================================== */
static jlSpriteT *createSpriteFromRows(uint8_t *tilesDst, uint16_t widthPx, uint16_t heightPx, const char * const *rows) {
jlSpriteT *sp;
authorSpriteFromRows(tilesDst, widthPx, heightPx, rows);
sp = jlSpriteCreate(tilesDst, (uint8_t)(widthPx / 8), (uint8_t)(heightPx / 8));
if (sp != NULL) {
(void)jlSpriteCompile(sp);
}
return sp;
}
static bool buildAllSprites(void) {
static uint8_t cursorTiles[VERB_COUNT][CURSOR_TILES_X * CURSOR_TILES_Y * JL_TILE_BYTES];
static uint8_t itemTiles[ITEM_COUNT][2 * 2 * JL_TILE_BYTES];
const char * const *egoSrc[DIR_COUNT][EGO_FRAMES] = {
{ kEgoIdleS, kEgoWalk1S, kEgoWalk2S },
{ kEgoIdleE, kEgoWalk1E, kEgoWalk2E },
{ kEgoIdleN, kEgoWalk1N, kEgoWalk2N },
{ kEgoIdleW, kEgoWalk1W, kEgoWalk2W },
};
const char * const *cursorSrc[VERB_COUNT] = {
kCursorWalk, kCursorLook, kCursorTake, kCursorUse, kCursorTalk
};
uint8_t d;
uint8_t f;
uint8_t v;
for (d = 0; d < DIR_COUNT; d++) {
for (f = 0; f < EGO_FRAMES; f++) {
gEgo[d][f] = createSpriteFromRows(gEgoTiles[d][f], EGO_W, EGO_H, egoSrc[d][f]);
if (gEgo[d][f] == NULL) {
return false;
}
}
}
for (v = 0; v < VERB_COUNT; v++) {
gCursor[v] = createSpriteFromRows(cursorTiles[v], 16, 16, cursorSrc[v]);
if (gCursor[v] == NULL) {
return false;
}
}
gItemSprite[ITEM_LAMP] = createSpriteFromRows(itemTiles[ITEM_LAMP], 16, 16, kItemLampUnlit);
gItemSprite[ITEM_KEY] = createSpriteFromRows(itemTiles[ITEM_KEY], 16, 16, kItemKey);
gLampLitSprite = createSpriteFromRows(gLampLitTiles, 16, 16, kItemLampLit);
if (gItemSprite[ITEM_LAMP] == NULL || gItemSprite[ITEM_KEY] == NULL || gLampLitSprite == NULL) {
return false;
}
return true;
}
/* ===================================================================
* Section 7 -- Room definitions
*
* Each room has a walk-mask (ASCII grid, 80 cols x 42 rows of 4-px
* cells), a hotspot list, and a prop list. Props are drawn AFTER
* the ego so they can overlap and produce the Sierra "walks behind
* the tree" depth illusion (Section 12).
* =================================================================== */
#define WM_FOREST_W "############################################################"
// The walk-mask is 80 columns wide; we author it in 80-char strings.
static const char * const kWalkForest[WALK_GRID_H] = {
// Sky band 0..17 (rows 0..17 = y 0..71): no-walk
"################################################################################",
"################################################################################",
"################################################################################",
"################################################################################",
"################################################################################",
"################################################################################",
"################################################################################",
"################################################################################",
"################################################################################",
"################################################################################",
"################################################################################",
"################################################################################",
"################################################################################",
"################################################################################",
"################################################################################",
"################################################################################",
"################################################################################",
"################################################################################",
// row 18, y=72: top of walkable ground
"................................................................................",
"................................................................................",
"................................................................................",
"................................................................................",
"................................................................................",
"................................................................................",
"................................................................................",
"................................................................................",
"................................................................................",
"................................................................................",
"................................................................................",
"................................................................................",
"................................................................................",
"................................................................................",
"................................................................................",
"................................................................................",
"................................................................................",
"................................................................................",
"................................................................................",
"................................................................................",
"................................................................................",
"................................................................................",
"................................................................................",
"................................................................................",
};
// Cottage walkable area: bounded by walls.
static const char * const kWalkCottage[WALK_GRID_H] = {
"################################################################################",
"################################################################################",
"################################################################################",
"################################################################################",
"################################################################################",
"################################################################################",
"################################################################################",
"################################################################################",
"################################################################################",
"################################################################################",
"################################################################################",
"################################################################################",
"################################################################################",
"################################################################################",
"################################################################################",
"################################################################################",
"################################################################################",
"################################################################################",
"################################################################################",
"################################################################################",
"################################################################################",
"################################################################################",
"################################################################################",
"################################################################################",
"################################################################################",
"##########.....................................................................",
"##########......................................................###############",
"##########......................................................###############",
"##########......................................................###############",
"##########......................................................###############",
"................................................................###############",
"................................................................###############",
"................................................................###############",
"................................................................###############",
"................................................................###############",
"................................................................###############",
"................................................................###############",
"................................................................###############",
"................................................................###############",
"................................................................###############",
"................................................................###############",
"................................................................###############",
};
/* The forest's three tree-trunk props live on the foreground so the
* ego can walk behind them. Their pixel positions are chosen so
* their bottoms sit at y=130-ish (well past the walk-area top of
* 72), giving a real depth difference. The sprite art for each is
* 16x32 -- procedurally drawn at startup into a surface, then
* snapshotted via jlSpriteCreateFromSurface. We define them just as
* a string-row prop here. */
#define TREE_W 16
#define TREE_H 40
static const char * const kPropTree[TREE_H] = {
" gggg ",
" ggggGggg ",
" ggGGggggGgg ",
" gGggggGgggg ",
" ggGgggggggGg ",
" gggGGggggggg ",
" gggggggGGggg ",
" gGggggggGgGg ",
" ggGGggggGg ",
" gggGGGgggg ",
" ggggGgGg ",
" gggGgg ",
" gggg ",
" nn ",
" nn ",
" nN ",
" Nn ",
" nn ",
" nN ",
" Nn ",
" nn ",
" nN ",
" Nn ",
" nn ",
" nN ",
" Nn ",
" nn ",
" nN ",
" Nn ",
" nNn ",
" NnN ",
" nNn ",
" nnNN ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
};
#define TABLE_W 32
#define TABLE_H 24
static const char * const kPropTable[TABLE_H] = {
" ",
" ",
" ",
" ",
" ############################ ",
" #nnnnnnnnnnnnnnnnnnnnnnnnnn# ",
" #nNNNnnnnnnNNnnnnnnnnnnNNNn# ",
" #nnnnnnnnnnNNnnnnnnnnnnnnnn# ",
" ############################ ",
" ## ## ",
" nN Nn ",
" nN Nn ",
" nN Nn ",
" nN Nn ",
" nN Nn ",
" nN Nn ",
" nN Nn ",
" nN Nn ",
" nN Nn ",
" nN Nn ",
" nN Nn ",
" nN Nn ",
" nN Nn ",
" ## ## ",
};
#define LAMP_W 8
#define LAMP_H 16
static const char * const kPropLamp[LAMP_H] = {
" #### ",
" #wwww# ",
"#wwwwww#",
"#wwwwww#",
"#wwwwww#",
"#wwwwww#",
" #wwww# ",
" #### ",
" #nn# ",
" #nnnn# ",
" #nnnn# ",
" #nnnn# ",
" #nn# ",
" #### ",
" ",
" ",
};
static uint8_t gPropTreeTiles[(TREE_W / 8) * (TREE_H / 8) * JL_TILE_BYTES];
static uint8_t gPropTableTiles[(TABLE_W / 8) * (TABLE_H / 8) * JL_TILE_BYTES];
static uint8_t gPropLampTiles[(LAMP_W / 8) * (LAMP_H / 8) * JL_TILE_BYTES];
static jlSpriteT *gSpTree;
static jlSpriteT *gSpTable;
static jlSpriteT *gSpLamp;
static void buildPropSprites(void) {
gSpTree = createSpriteFromRows(gPropTreeTiles, TREE_W, TREE_H, kPropTree);
gSpTable = createSpriteFromRows(gPropTableTiles, TABLE_W, TABLE_H, kPropTable);
gSpLamp = createSpriteFromRows(gPropLampTiles, LAMP_W, LAMP_H, kPropLamp);
}
/* The single mutable backup-buffer pool for prop save-under. Props
* draw on top of the ego in the priority pass, but on entry to a
* frame we just blit the pristine background, so we never have to
* save-under for props. */
// A hotspot added to a room (no compound literals: plain C89, so every
// port's compiler takes it).
static void roomHotspotAdd(RoomT *r, HotspotIdE id, int16_t x, int16_t y, int16_t w, int16_t h, const char *name) {
HotspotT *hs;
hs = &r->hotspots[r->hotspotCount++];
hs->id = id;
hs->x = x;
hs->y = y;
hs->w = w;
hs->h = h;
hs->name = name;
}
// A prop added to a room; resolvePropSprites gives it its sprite.
static void roomPropAdd(RoomT *r, int16_t x, int16_t y, int16_t w, int16_t h) {
PropT *prop;
prop = &r->props[r->propCount++];
prop->x = x;
prop->y = y;
prop->w = w;
prop->h = h;
prop->sp = NULL;
}
static void initRoomForest(void) {
RoomT *r;
int i;
r = &gRooms[ROOM_FOREST];
for (i = 0; i < WALK_GRID_H; i++) {
r->walkRows[i] = kWalkForest[i];
}
r->hotspotCount = 0;
roomHotspotAdd(r, HS_TREE, 50, 92, TREE_W, TREE_H, "the tree");
roomHotspotAdd(r, HS_TREE, 140, 100, TREE_W, TREE_H, "the tree");
roomHotspotAdd(r, HS_TREE, 240, 95, TREE_W, TREE_H, "the tree");
roomHotspotAdd(r, HS_DOOR_TO_COTTAGE, 290, 110, 30, 50, "the path east");
r->propCount = 0;
roomPropAdd(r, 50, 92, TREE_W, TREE_H);
roomPropAdd(r, 140, 100, TREE_W, TREE_H);
roomPropAdd(r, 240, 95, TREE_W, TREE_H);
r->egoEnterFromE_x = 290;
r->egoEnterFromE_y = 130;
r->egoEnterFromW_x = 16;
r->egoEnterFromW_y = 130;
r->egoDefaultX = 60;
r->egoDefaultY = 130;
}
static void initRoomCottage(void) {
RoomT *r;
int i;
r = &gRooms[ROOM_COTTAGE];
for (i = 0; i < WALK_GRID_H; i++) {
r->walkRows[i] = kWalkCottage[i];
}
r->hotspotCount = 0;
roomHotspotAdd(r, HS_WINDOW, 60, 18, 50, 40, "the window");
roomHotspotAdd(r, HS_FIRE, 220, 30, 70, 60, "the fireplace");
roomHotspotAdd(r, HS_TABLE, 140, 108, TABLE_W, TABLE_H, "the table");
roomHotspotAdd(r, HS_LAMP_ON_TABLE, 156, 100, LAMP_W, LAMP_H, "a small lamp");
roomHotspotAdd(r, HS_DOOR_TO_FOREST, 8, 60, 36, 80, "the door");
r->propCount = 0;
roomPropAdd(r, 140, 108, TABLE_W, TABLE_H);
/* The lamp prop is drawn ONLY while the player hasn't picked it
* up; the prop is added/removed dynamically. We pre-allocate the
* slot here so the priority pass can find it. */
roomPropAdd(r, 156, 100, LAMP_W, LAMP_H);
r->egoEnterFromE_x = 280;
r->egoEnterFromE_y = 150;
r->egoEnterFromW_x = 40;
r->egoEnterFromW_y = 130;
r->egoDefaultX = 130;
r->egoDefaultY = 130;
}
static void resolvePropSprites(void) {
/* After buildPropSprites runs, point each room's prop->sp at the
* shared prop sprite. Done after both buildPropSprites and
* init*Room so we don't capture NULL. */
RoomT *r;
r = &gRooms[ROOM_FOREST];
r->props[0].sp = gSpTree;
r->props[1].sp = gSpTree;
r->props[2].sp = gSpTree;
r = &gRooms[ROOM_COTTAGE];
r->props[0].sp = gSpTable;
r->props[1].sp = gSpLamp;
}
/* ===================================================================
* Section 8 -- Walk-mask & pathfinding
*
* BFS on the per-room walk grid. Cells are WALK_CELL_PX = 4 px wide,
* so the grid is 80x42. Click destination -> shortest path of grid
* cells -> waypoint list (one entry per cell). The ego walks
* along waypoints, picking direction per step.
* =================================================================== */
static bool walkable(RoomE room, int16_t cx, int16_t cy) {
if (cx < 0 || cx >= WALK_GRID_W || cy < 0 || cy >= WALK_GRID_H) {
return false;
}
return gRooms[room].walkRows[cy][cx] == '.';
}
/* Pixel-space convenience: ego's feet (bottom-center) at (px,py) ->
* walkmask cell. */
static void footToCell(int16_t px, int16_t py, int16_t *cx, int16_t *cy) {
*cx = (int16_t)(px / WALK_CELL_PX);
*cy = (int16_t)(py / WALK_CELL_PX);
}
static void cellToFoot(int16_t cx, int16_t cy, int16_t *px, int16_t *py) {
*px = (int16_t)((cx * WALK_CELL_PX) + (WALK_CELL_PX / 2));
*py = (int16_t)((cy * WALK_CELL_PX) + (WALK_CELL_PX / 2));
}
/* BFS parents stored as a (row,col)-indexed grid. Each entry is a
* direction byte (0=N,1=E,2=S,3=W) telling us which neighbour we
* came from; 0xFF means unvisited. */
static uint8_t gBfsParent[WALK_GRID_H][WALK_GRID_W];
static int16_t gBfsQueueX[WALK_GRID_H * WALK_GRID_W];
static int16_t gBfsQueueY[WALK_GRID_H * WALK_GRID_W];
// Reset every BFS parent cell to unvisited (0xFF).
//
// The pathfinder is split across several helpers so no single function
// holds enough simultaneously-live int16_t values to exhaust the very
// small w65816 register file. They are marked noinline so the -O2
// inliner cannot fold them back into findPath and recreate the
// "ran out of registers" failure.
static __attribute__((noinline)) void bfsClear(void) {
int16_t row;
for (row = 0; row < WALK_GRID_H; row++) {
int16_t col;
for (col = 0; col < WALK_GRID_W; col++) {
gBfsParent[row][col] = 0xFF;
}
}
}
// Breadth-first search from (scx,scy) toward (tcx,tcy) across the room's
// walk grid, recording each visited cell's incoming direction byte in
// gBfsParent. Returns true if the target cell was reached. Split out of
// findPath to keep the hot loop's live-value count low enough for the
// w65816 register allocator.
// Visit one neighbour of (cx,cy) in direction d. If that neighbour is
// walkable and not yet visited, record its incoming direction and append
// it to the BFS queue, advancing *qtail. Kept noinline so bfsSearch's
// inner loop reduces to a single call and stays within the w65816
// register budget.
static __attribute__((noinline)) void bfsExpand(RoomE room, int16_t cx, int16_t cy, uint8_t d, int16_t *qtail) {
int16_t nx;
int16_t ny;
nx = cx;
ny = cy;
switch (d) {
case 0:
ny = (int16_t)(cy - 1);
break;
case 1:
nx = (int16_t)(cx + 1);
break;
case 2:
ny = (int16_t)(cy + 1);
break;
default:
nx = (int16_t)(cx - 1);
break;
}
if (!walkable(room, nx, ny)) {
return;
}
if (gBfsParent[ny][nx] != 0xFF) {
return;
}
gBfsParent[ny][nx] = d;
gBfsQueueX[*qtail] = nx;
gBfsQueueY[*qtail] = ny;
(*qtail)++;
}
static __attribute__((noinline)) bool bfsSearch(RoomE room, int16_t scx, int16_t scy, int16_t tcx, int16_t tcy) {
int16_t qhead;
int16_t qtail;
int16_t cx;
int16_t cy;
uint8_t d;
qhead = 0;
qtail = 0;
gBfsQueueX[qtail] = scx;
gBfsQueueY[qtail] = scy;
qtail++;
gBfsParent[scy][scx] = 0xFE; // sentinel: start
while (qhead < qtail) {
cx = gBfsQueueX[qhead];
cy = gBfsQueueY[qhead];
qhead++;
if (cx == tcx && cy == tcy) {
break;
}
for (d = 0; d < 4; d++) {
bfsExpand(room, cx, cy, d, &qtail);
}
}
return gBfsParent[tcy][tcx] != 0xFF;
}
// Reconstruct the path by walking gBfsParent back from the target to
// the start, then emit it forward (start..target) into the gPath*
// waypoint arrays as foot pixel positions.
static __attribute__((noinline)) void buildPath(int16_t tcx, int16_t tcy) {
int16_t pathRevX[MAX_PATH_NODES];
int16_t pathRevY[MAX_PATH_NODES];
int16_t n;
int16_t i;
int16_t cx;
int16_t cy;
uint8_t d;
n = 0;
cx = tcx;
cy = tcy;
while (n < MAX_PATH_NODES) {
pathRevX[n] = cx;
pathRevY[n] = cy;
n++;
d = gBfsParent[cy][cx];
if (d == 0xFE) {
break;
}
switch (d) {
case 0:
cy = (int16_t)(cy + 1);
break;
case 1:
cx = (int16_t)(cx - 1);
break;
case 2:
cy = (int16_t)(cy - 1);
break;
default:
cx = (int16_t)(cx + 1);
break;
}
}
gPathLen = 0;
for (i = (int16_t)(n - 1); i >= 0; i--) {
int16_t px;
int16_t py;
cellToFoot(pathRevX[i], pathRevY[i], &px, &py);
gPathX[gPathLen] = px;
gPathY[gPathLen] = py;
gPathLen++;
if (gPathLen >= MAX_PATH_NODES) {
break;
}
}
gPathStep = 0;
}
// Snap a cell to the nearest walkable one via an expanding box search
// (dy outer, dx inner, radius 1..maxR-1, same scan order as the original
// inline loop). Returns true if *cx/*cy now name a walkable cell (it was
// already walkable, or a snap target was found); false if none was found
// within range. Only writes *cx/*cy when a walkable cell is found.
static __attribute__((noinline)) bool snapToWalkable(RoomE room, int16_t *cx, int16_t *cy, int16_t maxR) {
int16_t r;
if (walkable(room, *cx, *cy)) {
return true;
}
for (r = 1; r < maxR; r++) {
int16_t dy;
for (dy = (int16_t)(-r); dy <= r; dy++) {
int16_t dx;
for (dx = (int16_t)(-r); dx <= r; dx++) {
int16_t ncx;
int16_t ncy;
ncx = (int16_t)(*cx + dx);
ncy = (int16_t)(*cy + dy);
if (walkable(room, ncx, ncy)) {
*cx = ncx;
*cy = ncy;
return true;
}
}
}
}
return false;
}
static bool findPath(RoomE room, int16_t sx, int16_t sy, int16_t tx, int16_t ty) {
int16_t scx;
int16_t scy;
int16_t tcx;
int16_t tcy;
footToCell(sx, sy, &scx, &scy);
footToCell(tx, ty, &tcx, &tcy);
// If the target is in an unwalkable cell, snap to the nearest
// walkable one (radius < 12); give up if none is within range.
if (!snapToWalkable(room, &tcx, &tcy, 12)) {
gPathLen = 0;
return false;
}
// If the start is somehow off the walkmask, snap it too (tiny
// radius < 8); proceed with the original cell if nothing is found.
snapToWalkable(room, &scx, &scy, 8);
bfsClear();
if (!bfsSearch(room, scx, scy, tcx, tcy)) {
gPathLen = 0;
return false;
}
buildPath(tcx, tcy);
return gPathLen > 0;
}
/* ===================================================================
* Section 9 -- Ego animation / movement
*
* The ego's "feet" are tracked at (gEgoX + EGO_W/2, gEgoY + EGO_H).
* Walking nudges those feet toward the current waypoint, picks a
* direction by the dominant axis, and cycles frame every ANIM_TICKS.
* =================================================================== */
static int16_t egoFeetX(void) { return (int16_t)(gEgoX + (EGO_W / 2)); }
static int16_t egoFeetY(void) { return (int16_t)(gEgoY + EGO_H); }
static void egoSetFeet(int16_t fx, int16_t fy) {
gEgoX = (int16_t)(fx - (EGO_W / 2));
gEgoY = (int16_t)(fy - EGO_H);
}
static void advanceEgo(void) {
int16_t fx;
int16_t fy;
int16_t tx;
int16_t ty;
int16_t dx;
int16_t dy;
int16_t adx;
int16_t 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_STEP && ady <= EGO_STEP) {
// Reached this waypoint.
egoSetFeet(tx, ty);
gPathStep++;
if (gPathStep >= gPathLen) {
gEgoMoving = false;
gEgoFrameIdx = 0;
return;
}
return;
}
if (adx > ady) {
step = (int16_t)(dx > 0 ? EGO_STEP : -EGO_STEP);
fx = (int16_t)(fx + step);
gEgoDir = (step > 0) ? DIR_E : DIR_W;
} else {
step = (int16_t)(dy > 0 ? EGO_STEP : -EGO_STEP);
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;
}
}
}
// Walks the ego as far as EGO_WALK_SPEED takes it in `elapsed`
// milliseconds, EGO_STEP pixels at a time; what is left over, short of a
// step, carries into the next frame.
static void walkEgo(uint32_t elapsed) {
uint32_t steps;
if (!gEgoMoving) {
gEgoWalkCarry = 0;
return;
}
gEgoWalkCarry += elapsed * EGO_WALK_SPEED;
steps = gEgoWalkCarry / (MS_PER_SECOND * EGO_STEP);
gEgoWalkCarry %= MS_PER_SECOND * EGO_STEP;
for (; steps > 0; steps--) {
advanceEgo();
}
}
/* ===================================================================
* Section 10 -- Verb cursor / mouse input
* =================================================================== */
static const char *verbName(VerbE v) {
switch (v) {
case VERB_WALK:
return "Walk";
case VERB_LOOK:
return "Look";
case VERB_TAKE:
return "Take";
case VERB_USE:
return "Use";
case VERB_TALK:
return "Talk";
default:
return "?";
}
}
/* Hit-test the play-area for a hotspot under (mx,my). Returns the
* hotspot index in the current room, or -1 if none. */
static int hotspotAt(int16_t mx, int16_t my) {
const RoomT *r;
uint8_t i;
if (my >= PLAY_AREA_H) {
return -1;
}
r = &gRooms[gCurRoom];
for (i = 0; i < r->hotspotCount; i++) {
const HotspotT *h;
h = &r->hotspots[i];
if (mx >= h->x && mx < (h->x + h->w) && my >= h->y && my < (h->y + h->h)) {
return (int)i;
}
}
return -1;
}
/* ===================================================================
* Section 11 -- Tiny inline 5x7 bitmap font
*
* Drawing text uses jlDrawPixel directly (set bits only). The
* message bar updates rarely so per-pixel plot is fine.
*
* Each glyph is 5 wide x 7 tall, stored as 7 bytes (one per row),
* with the high 5 bits as pixel pattern. The font covers space,
* '0'..'9', 'A'..'Z', '.', ',', ':', '!', '?', '-', '\'', '"'.
* Other chars render as space.
* =================================================================== */
#define FONT_W 5
#define FONT_H 7
static const uint8_t kFontGlyph[][FONT_H] = {
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0 space
{ 0x70, 0x88, 0x98, 0xA8, 0xC8, 0x88, 0x70 }, // 1 '0'
{ 0x20, 0x60, 0x20, 0x20, 0x20, 0x20, 0x70 }, // 2 '1'
{ 0x70, 0x88, 0x08, 0x10, 0x20, 0x40, 0xF8 }, // 3 '2'
{ 0x70, 0x88, 0x08, 0x30, 0x08, 0x88, 0x70 }, // 4 '3'
{ 0x10, 0x30, 0x50, 0x90, 0xF8, 0x10, 0x10 }, // 5 '4'
{ 0xF8, 0x80, 0xF0, 0x08, 0x08, 0x88, 0x70 }, // 6 '5'
{ 0x30, 0x40, 0x80, 0xF0, 0x88, 0x88, 0x70 }, // 7 '6'
{ 0xF8, 0x08, 0x10, 0x20, 0x40, 0x40, 0x40 }, // 8 '7'
{ 0x70, 0x88, 0x88, 0x70, 0x88, 0x88, 0x70 }, // 9 '8'
{ 0x70, 0x88, 0x88, 0x78, 0x08, 0x10, 0x60 }, // 10 '9'
{ 0x70, 0x88, 0x88, 0xF8, 0x88, 0x88, 0x88 }, // 11 'A'
{ 0xF0, 0x88, 0x88, 0xF0, 0x88, 0x88, 0xF0 }, // 12 'B'
{ 0x70, 0x88, 0x80, 0x80, 0x80, 0x88, 0x70 }, // 13 'C'
{ 0xF0, 0x88, 0x88, 0x88, 0x88, 0x88, 0xF0 }, // 14 'D'
{ 0xF8, 0x80, 0x80, 0xF0, 0x80, 0x80, 0xF8 }, // 15 'E'
{ 0xF8, 0x80, 0x80, 0xF0, 0x80, 0x80, 0x80 }, // 16 'F'
{ 0x70, 0x88, 0x80, 0xB8, 0x88, 0x88, 0x70 }, // 17 'G'
{ 0x88, 0x88, 0x88, 0xF8, 0x88, 0x88, 0x88 }, // 18 'H'
{ 0x70, 0x20, 0x20, 0x20, 0x20, 0x20, 0x70 }, // 19 'I'
{ 0x38, 0x10, 0x10, 0x10, 0x90, 0x90, 0x60 }, // 20 'J'
{ 0x88, 0x90, 0xA0, 0xC0, 0xA0, 0x90, 0x88 }, // 21 'K'
{ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0xF8 }, // 22 'L'
{ 0x88, 0xD8, 0xA8, 0xA8, 0x88, 0x88, 0x88 }, // 23 'M'
{ 0x88, 0xC8, 0xA8, 0x98, 0x88, 0x88, 0x88 }, // 24 'N'
{ 0x70, 0x88, 0x88, 0x88, 0x88, 0x88, 0x70 }, // 25 'O'
{ 0xF0, 0x88, 0x88, 0xF0, 0x80, 0x80, 0x80 }, // 26 'P'
{ 0x70, 0x88, 0x88, 0x88, 0xA8, 0x90, 0x68 }, // 27 'Q'
{ 0xF0, 0x88, 0x88, 0xF0, 0xA0, 0x90, 0x88 }, // 28 'R'
{ 0x70, 0x88, 0x80, 0x70, 0x08, 0x88, 0x70 }, // 29 'S'
{ 0xF8, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20 }, // 30 'T'
{ 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x70 }, // 31 'U'
{ 0x88, 0x88, 0x88, 0x88, 0x88, 0x50, 0x20 }, // 32 'V'
{ 0x88, 0x88, 0x88, 0xA8, 0xA8, 0xD8, 0x88 }, // 33 'W'
{ 0x88, 0x88, 0x50, 0x20, 0x50, 0x88, 0x88 }, // 34 'X'
{ 0x88, 0x88, 0x50, 0x20, 0x20, 0x20, 0x20 }, // 35 'Y'
{ 0xF8, 0x08, 0x10, 0x20, 0x40, 0x80, 0xF8 }, // 36 'Z'
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x60 }, // 37 '.'
{ 0x00, 0x00, 0x00, 0x00, 0x60, 0x60, 0x40 }, // 38 ','
{ 0x00, 0x60, 0x60, 0x00, 0x60, 0x60, 0x00 }, // 39 ':'
{ 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, 0x20 }, // 40 '!'
{ 0x70, 0x88, 0x08, 0x10, 0x20, 0x00, 0x20 }, // 41 '?'
{ 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00 }, // 42 '-'
{ 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 43 '\''
{ 0x50, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 44 '"'
};
static int glyphIdx(char c) {
if (c == ' ') {
return 0;
}
if (c >= '0' && c <= '9') {
return 1 + (c - '0');
}
if (c >= 'A' && c <= 'Z') {
return 11 + (c - 'A');
}
if (c >= 'a' && c <= 'z') {
return 11 + (c - 'a'); // uppercase fallback
}
if (c == '.') {
return 37;
}
if (c == ',') {
return 38;
}
if (c == ':') {
return 39;
}
if (c == '!') {
return 40;
}
if (c == '?') {
return 41;
}
if (c == '-') {
return 42;
}
if (c == '\'') {
return 43;
}
if (c == '"') {
return 44;
}
return 0;
}
static void drawTextLine(int16_t x, int16_t y, const char *text, uint8_t color) {
int16_t cx;
int16_t i;
int idx;
int16_t row;
int16_t col;
uint8_t bits;
cx = x;
for (i = 0; text[i] != '\0'; i++) {
idx = glyphIdx(text[i]);
for (row = 0; row < FONT_H; row++) {
bits = kFontGlyph[idx][row];
for (col = 0; col < FONT_W; col++) {
if (bits & (0x80 >> col)) {
jlDrawPixel(jlgScreen, (int16_t)(cx + col), (int16_t)(y + row), color);
}
}
}
cx = (int16_t)(cx + FONT_W + 1);
if (cx > JL_SURFACE_WIDTH - FONT_W) {
return;
}
}
}
/* ===================================================================
* Section 12 -- UI bars (message + inventory)
*
* Drawn directly on the stage every frame after the play area is
* composed. The bars never get save-undered; they're simple
* fillRects.
* =================================================================== */
static void drawMessageBar(void) {
jlFillRect(jlgScreen, 0, MSG_BAR_Y, JL_SURFACE_WIDTH, MSG_BAR_H, UI_BG);
jlFillRect(jlgScreen, 0, MSG_BAR_Y, JL_SURFACE_WIDTH, 1, UI_DIM);
if (gMessage[0] != '\0') {
drawTextLine(4, MSG_BAR_Y + 2, gMessage, UI_INK);
}
}
static void drawInventoryBar(void) {
char buf[40];
int n;
int16_t x;
jlFillRect(jlgScreen, 0, INV_BAR_Y, JL_SURFACE_WIDTH, INV_BAR_H, UI_BG);
jlFillRect(jlgScreen, 0, INV_BAR_Y, JL_SURFACE_WIDTH, 1, UI_DIM);
// Verb readout at the left.
snprintf(buf, sizeof(buf), "VERB: %s", verbName(gCurVerb));
drawTextLine(4, INV_BAR_Y + 2, buf, UI_HILITE);
// Item icons at the right.
x = JL_SURFACE_WIDTH - (ITEM_ICON_W + 4);
n = 0;
if (gHaveItem[ITEM_LAMP]) {
jlSpriteT *sp;
sp = gLampIsLit ? gLampLitSprite : gItemSprite[ITEM_LAMP];
jlSpriteDraw(jlgScreen, sp, x, INV_BAR_Y + 2);
x = (int16_t)(x - (ITEM_ICON_W + 2));
n++;
}
if (gHaveItem[ITEM_KEY]) {
jlSpriteDraw(jlgScreen, gItemSprite[ITEM_KEY], x, INV_BAR_Y + 2);
x = (int16_t)(x - (ITEM_ICON_W + 2));
n++;
}
(void)n;
}
/* ===================================================================
* Section 13 -- Per-frame compositor
*
* 1. Blit the pristine room background onto the stage.
* 2. Draw the ego.
* 3. Re-draw any prop whose y_bottom is > ego.y_bottom (=> appears
* in front of the ego, completing the "walk-behind" illusion).
* 4. Draw UI bars.
* 5. Draw cursor on top.
* 6. Present.
* =================================================================== */
static void composeFrame(int16_t cursorX, int16_t cursorY) {
const RoomT *r;
int16_t egoBottom;
uint8_t i;
// 1. background
jlSurfaceCopy(jlgScreen, gBgScene[gCurRoom]);
// 2. ego
jlSpriteDraw(jlgScreen, gEgo[gEgoDir][gEgoFrameIdx], gEgoX, gEgoY);
// 3. prop priority redraw
r = &gRooms[gCurRoom];
egoBottom = (int16_t)(gEgoY + EGO_H);
for (i = 0; i < r->propCount; i++) {
const PropT *p;
p = &r->props[i];
if (p->sp == NULL) {
continue;
}
if ((p->y + p->h) > egoBottom) {
jlSpriteDraw(jlgScreen, p->sp, p->x, p->y);
}
}
// 4. UI bars
drawMessageBar();
drawInventoryBar();
/* 5. Cursor: keep it in the play area (and inventory area too).
* The cursor is drawn last so it sits on top of everything. */
{
jlSpriteT *cs;
int16_t cx;
int16_t cy;
cx = (int16_t)(cursorX - 6);
cy = (int16_t)(cursorY - 6);
if (gUsingItem != ITEM_NONE) {
cs = gItemSprite[gUsingItem];
} else {
cs = gCursor[gCurVerb];
}
if (cs != NULL) {
jlSpriteDraw(jlgScreen, cs, cx, cy);
}
}
// 6. flush
jlStagePresent();
}
/* ===================================================================
* Section 14 -- Verb / item interactions
*
* Each hotspot has per-verb behavior. Item-on-hotspot combinations
* are handled when gUsingItem != ITEM_NONE.
* =================================================================== */
static void enterRoom(RoomE room, bool fromEast, bool fromWest);
static void doVerbOnHotspot(VerbE verb, HotspotIdE hs) {
if (gUsingItem != ITEM_NONE) {
// Item-on-hotspot combinations.
if (gUsingItem == ITEM_LAMP && hs == HS_FIRE) {
if (gLampIsLit) {
setMessage("The lamp is already lit.");
} else {
gLampIsLit = true;
setMessage("The lamp catches and burns warmly.");
}
} else if (gUsingItem == ITEM_LAMP && hs == HS_TREE) {
setMessage("Burning the tree would be uncivil.");
} else {
setMessage("Nothing happens.");
}
gUsingItem = ITEM_NONE;
return;
}
switch (verb) {
case VERB_LOOK:
switch (hs) {
case HS_TREE:
setMessage("A tall oak. You see no birds today.");
break;
case HS_FOUNTAIN:
setMessage("A mossy fountain. The water is clear.");
break;
case HS_LAMP_ON_TABLE:
setMessage("A small brass lamp sits on the table.");
break;
case HS_TABLE:
setMessage("A weathered oak table.");
break;
case HS_FIRE:
setMessage("A small fire crackles in the hearth.");
break;
case HS_DOOR_TO_COTTAGE:
setMessage("A path leads east toward a small cottage.");
break;
case HS_DOOR_TO_FOREST:
setMessage("The door leads back to the forest.");
break;
case HS_WINDOW:
setMessage("Through the window you see trees and sky.");
break;
default:
setMessage("Nothing of interest.");
break;
}
break;
case VERB_TAKE:
switch (hs) {
case HS_LAMP_ON_TABLE: {
RoomT *r;
if (gHaveItem[ITEM_LAMP]) {
setMessage("You already have the lamp.");
break;
}
gHaveItem[ITEM_LAMP] = true;
setMessage("You take the small brass lamp.");
// Remove the lamp prop and its hotspot.
r = &gRooms[ROOM_COTTAGE];
r->props[1].sp = NULL;
{
uint8_t k;
for (k = 0; k < r->hotspotCount; k++) {
if (r->hotspots[k].id == HS_LAMP_ON_TABLE) {
r->hotspots[k].id = HS_NONE;
}
}
}
break;
}
case HS_TABLE:
setMessage("The table is too heavy to carry.");
break;
case HS_TREE:
setMessage("You can't pick up a tree.");
break;
case HS_FIRE:
setMessage("That is much too hot to handle.");
break;
default:
setMessage("Better leave that alone.");
break;
}
break;
case VERB_USE:
switch (hs) {
case HS_DOOR_TO_COTTAGE:
enterRoom(ROOM_COTTAGE, false, true);
setMessage("You enter the cottage.");
break;
case HS_DOOR_TO_FOREST:
enterRoom(ROOM_FOREST, true, false);
setMessage("You step back into the forest.");
break;
case HS_LAMP_ON_TABLE:
setMessage("Pick it up first.");
break;
case HS_FIRE:
setMessage("Warm. Try \"use lamp on fire\" with a lamp.");
break;
default:
setMessage("That doesn't seem to do anything.");
break;
}
break;
case VERB_TALK:
switch (hs) {
case HS_TREE:
setMessage("The tree does not reply.");
break;
case HS_FIRE:
setMessage("The fire crackles companionably.");
break;
default:
setMessage("There's no one to talk to here.");
break;
}
break;
case VERB_WALK:
default:
/* WALK falls through to path planning at the call site;
* see handleClick. */
break;
}
}
/* ===================================================================
* Section 15 -- Click handling
* =================================================================== */
static void handleClick(int16_t mx, int16_t my, bool leftClick, bool rightClick) {
int hs;
if (rightClick) {
if (gUsingItem != ITEM_NONE) {
gUsingItem = ITEM_NONE;
setMessage("");
} else {
gCurVerb = (VerbE)((gCurVerb + 1) % VERB_COUNT);
}
return;
}
if (!leftClick) {
return;
}
// Click on inventory bar -> "use this item next".
if (my >= INV_BAR_Y) {
int16_t x;
x = (int16_t)(JL_SURFACE_WIDTH - (ITEM_ICON_W + 4));
if (gHaveItem[ITEM_LAMP]) {
if (mx >= x && mx < (x + ITEM_ICON_W)) {
gUsingItem = ITEM_LAMP;
setMessage("Use the lamp on what?");
return;
}
x = (int16_t)(x - (ITEM_ICON_W + 2));
}
if (gHaveItem[ITEM_KEY]) {
if (mx >= x && mx < (x + ITEM_ICON_W)) {
gUsingItem = ITEM_KEY;
setMessage("Use the key on what?");
return;
}
}
return;
}
// Message bar swallows the click but does nothing.
if (my >= MSG_BAR_Y && my < INV_BAR_Y) {
return;
}
// Inside play area.
hs = hotspotAt(mx, my);
if (hs >= 0) {
HotspotIdE id;
id = gRooms[gCurRoom].hotspots[hs].id;
if (id != HS_NONE) {
if (gCurVerb == VERB_WALK && gUsingItem == ITEM_NONE) {
// "Walk to" hotspot = walk near it.
int16_t tx;
int16_t ty;
tx = (int16_t)(gRooms[gCurRoom].hotspots[hs].x + (gRooms[gCurRoom].hotspots[hs].w / 2));
ty = (int16_t)(gRooms[gCurRoom].hotspots[hs].y + gRooms[gCurRoom].hotspots[hs].h - 4);
if (findPath(gCurRoom, egoFeetX(), egoFeetY(), tx, ty)) {
gEgoMoving = true;
}
} else {
doVerbOnHotspot(gCurVerb, id);
}
return;
}
}
/* Fallback: walk to the click point (gameplay's main verb). */
if (gUsingItem != ITEM_NONE) {
setMessage("Click on an object to use the item on it.");
return;
}
if (findPath(gCurRoom, egoFeetX(), egoFeetY(), mx, my)) {
gEgoMoving = true;
}
}
/* ===================================================================
* Section 16 -- Room transitions
* =================================================================== */
static void enterRoom(RoomE room, bool fromEast, bool fromWest) {
const RoomT *r;
gCurRoom = room;
r = &gRooms[room];
if (fromEast) {
egoSetFeet(r->egoEnterFromE_x, r->egoEnterFromE_y);
} else if (fromWest) {
egoSetFeet(r->egoEnterFromW_x, r->egoEnterFromW_y);
} else {
egoSetFeet(r->egoDefaultX, r->egoDefaultY);
}
gEgoMoving = false;
gEgoFrameIdx = 0;
gPathLen = 0;
gPathStep = 0;
programSkyScb(jlgScreen, room);
}
/* ===================================================================
* Section 17 -- Background-scene staging
*
* We draw each room background ONCE into its own offscreen surface
* at startup. Per-frame, we surfaceCopy the pristine background to
* the stage, then add ego/props/cursor. This avoids per-frame draw
* cost of all the primitives that compose the scene.
* =================================================================== */
static bool buildBackgrounds(void) {
gBgScene[ROOM_FOREST] = jlSurfaceCreate();
gBgScene[ROOM_COTTAGE] = jlSurfaceCreate();
if (gBgScene[ROOM_FOREST] == NULL || gBgScene[ROOM_COTTAGE] == NULL) {
return false;
}
drawForestBackground(gBgScene[ROOM_FOREST]);
drawCottageBackground(gBgScene[ROOM_COTTAGE]);
return true;
}
/* ===================================================================
* Section 18 -- main()
* =================================================================== */
int main(void) {
jlConfigT config;
bool leftPressed;
bool rightPressed;
uint32_t lastMillis;
uint32_t now;
uint32_t elapsed;
/* Sprite codegen budget: 12 ego frames + 5 cursors + 3 props +
* 2 items = 22 sprites. Amiga planar emits 8 shifts/sprite at
* ~1.5 KB each, so we ask for 256 KB to be safe. Chunky ports
* need ~16 KB. */
#ifdef JOEYLIB_PLATFORM_IIGS
// IIgs: the codegen arena is a single attrNoCross Memory Manager
// block and cannot exceed one 64 KB bank; 16 KB covers the chunky
// emitter's needs for this sprite set.
config.codegenBytes = 16UL * 1024;
#else
config.codegenBytes = 256UL * 1024;
#endif
config.noAudio = true;
if (!jlInit(&config)) {
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
return 1;
}
jlgScreen = jlStageGet();
if (jlgScreen == NULL) {
fprintf(stderr, "jlStageGet returned NULL\n");
jlShutdown();
return 1;
}
buildPalettes(jlgScreen);
programSkyScb(jlgScreen, ROOM_FOREST);
if (!buildAllSprites()) {
fprintf(stderr, "buildAllSprites failed: %s\n", jlLastError());
jlShutdown();
return 1;
}
buildPropSprites();
initRoomForest();
initRoomCottage();
resolvePropSprites();
if (!buildBackgrounds()) {
fprintf(stderr, "buildBackgrounds failed: %s\n", jlLastError());
jlShutdown();
return 1;
}
gEgoBak.bytes = (uint8_t *)gEgoBackup;
gCursorBak.bytes = (uint8_t *)gCursorBackup;
gCurVerb = VERB_WALK;
gUsingItem = ITEM_NONE;
gMessage[0] = '\0';
enterRoom(ROOM_FOREST, false, false);
setMessage("Click to walk. Space or right-click cycles verb.");
lastMillis = jlMillisElapsed();
for (;;) {
int16_t mx;
int16_t my;
jlInputPoll();
if (jlKeyPressed(JL_KEY_ESCAPE)) {
break;
}
if (jlKeyPressed(JL_KEY_1)) {
gCurVerb = VERB_WALK;
}
if (jlKeyPressed(JL_KEY_2)) {
gCurVerb = VERB_LOOK;
}
if (jlKeyPressed(JL_KEY_3)) {
gCurVerb = VERB_TAKE;
}
if (jlKeyPressed(JL_KEY_4)) {
gCurVerb = VERB_USE;
}
if (jlKeyPressed(JL_KEY_5)) {
gCurVerb = VERB_TALK;
}
/* SPACE cycles verb -- one-button-mouse fallback for the
* IIgs, where right-click does not exist. Mirrors the
* right-click behaviour: drops any in-progress item-use
* mode first, then advances the verb. */
if (jlKeyPressed(JL_KEY_SPACE)) {
if (gUsingItem != ITEM_NONE) {
gUsingItem = ITEM_NONE;
setMessage("");
} else {
gCurVerb = (VerbE)((gCurVerb + 1) % VERB_COUNT);
}
}
mx = jlMouseX();
my = jlMouseY();
if (mx < 0) {
mx = 0;
}
if (my < 0) {
my = 0;
}
if (mx >= JL_SURFACE_WIDTH) {
mx = JL_SURFACE_WIDTH - 1;
}
if (my >= JL_SURFACE_HEIGHT) {
my = JL_SURFACE_HEIGHT - 1;
}
leftPressed = jlMousePressed(JL_MOUSE_BUTTON_LEFT);
rightPressed = jlMousePressed(JL_MOUSE_BUTTON_RIGHT);
if (leftPressed || rightPressed) {
handleClick(mx, my, leftPressed, rightPressed);
}
now = jlMillisElapsed();
elapsed = now - lastMillis;
lastMillis = now;
if (elapsed > MAX_STEP_MILLIS) {
elapsed = MAX_STEP_MILLIS;
}
walkEgo(elapsed);
if (gMessage[0] != '\0' && (int32_t)(now - gMessageUntil) >= 0) {
gMessage[0] = '\0';
}
composeFrame(mx, my);
gEgoX = clampi(gEgoX, 0, JL_SURFACE_WIDTH - EGO_W);
gEgoY = clampi(gEgoY, 0, PLAY_AREA_H - EGO_H);
}
jlShutdown();
return 0;
}