joeylib2/examples/adventure/adventure.c

2328 lines
75 KiB
C

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