2328 lines
75 KiB
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;
|
|
}
|