// 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 #include #include /* =================================================================== * 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; }