// Space Taxi (JoeyLib port) -- shared types and decls. // // Architecture (all four JoeyLib targets): // // tilemap 40x25 cells, each cell = (tileIndex, paletteSlot). // jlTilePaste blits the tile bank into the stage one cell // at a time at level boot, then it's static until the // scene changes. No per-frame redraw of the tilemap. // // taxi Single jlSpriteT with multiple cels (thrust frames, // facing variants). Save-under + restore-under each frame // so we don't repaint the tilemap behind it. // // passenger Up to 2 simultaneous sprites: one waiting on a pad, // one already in the cab (or none). Same save/restore // discipline as the taxi. // // audio One ~3-voice event stream rendered per platform: // SB+PSG-synth (DOS), PT 4-voice (Amiga), YM2149 (ST), // Ensoniq DOC channels (IIgs). The dispatch lives in // stAudio.c with one entry point per voice command. // // input Joystick port 2 conventionally on the C64: direction = // the four cardinals (thrust), FIRE = the LANDING GEAR // toggle while airborne ($63DD: press edge EORs $7197 // bit 0 with an SFX; $6462 requires gear down to land; // $6190 strips left/right thrust while gear is down). // Keyboard fallback: arrows + space for hosts without // joysticks. // // Level data lives in a small custom .dat per level (see // assets/levels/format.md). Tile bitmaps and sprite cels are PNG // authored externally and baked to native .tbk / .spr blobs at // build time via tools/assetbake/assetbake.py. #ifndef SPACETAXI_H #define SPACETAXI_H #include #define ST_TILEMAP_W 40u #define ST_TILEMAP_H 25u #define ST_TILE_PIXELS 8u // The tilemap stores raw C64 screen-RAM character codes. The empty // (non-colliding) playfield cell is the space character, $20. Every // other code is non-blank background -- on the C64 it latches a sprite- // background collision ($D01F). romToLevel.py uses BG=0x20 as the empty // marker when extracting screen RAM. #define ST_TILE_EMPTY 0x20u // Display field is 320x200 (JoeyLib's SURFACE_WIDTH x SURFACE_HEIGHT). // 40 tiles x 8 px = 320, 25 tiles x 8 px = 200. The bottom 3 rows are // the HUD band (score / lives / level / current-fare strip). The top // 22 rows are the playfield where the taxi moves. // Full C64 screen height. The original title uses all 25 rows // (frame at 0, 12, 24 + content). For gameplay the bottom 3 rows // are HUD territory -- HUD draws over the tilemap there. #define ST_PLAYFIELD_ROWS 25u #define ST_HUD_ROW 22u #define ST_HUD_ROW_COUNT 3u // Maximum stuff. Tuned for fitting the smallest target (IIgs): // 10 pads is the highest count in any canonical Space Taxi level // (D and M each have 10 pads; everything else <= 9) // Exactly ONE fare exists at a time. Verified: the C64 spawns a new // fare only while its state machine (gDeathStage) is idle, so // waiting-vs-carrying is the single passenger's `onboard` flag -- NOT // two passengers. (A prior "waiting + carrying = 2 sprites" note was // a misconception / false second source of truth.) #define ST_MAX_PADS 10u #define ST_MAX_PASSENGERS 1u // Passenger destination sentinel: "UP PLEASE!" -- the fare wants to be // flown UP through the top-wall transporter opening rather than to a // landing pad. This is the C64's $0B destination ($6CB8), used on // single-pad screens (and when all pads are consumed). Any real pad // index is 0..ST_MAX_PADS-1, so 0xFE can't collide. #define ST_DEST_TRANSPORTER 0xFEu // The transporter opening is a fixed 4-column gap ($67 tiles) in the top // wall, at columns 18..21 in every canonical Space Taxi screen (verified // across the extracted tilemaps). Flying the taxi up through it exits the // screen. #define ST_TRANSPORTER_COL_LO 18u #define ST_TRANSPORTER_COL_HI 22u // exclusive #define ST_TILE_TRANSPORTER 0x67u // Fixed-point taxi physics: position and velocity are int16_t in // units of 1/16 px, so the taxi can drift fractionally and the // thrust/gravity terms are integers without losing precision over // the whole field. (22 rows x 8 px x 16 = 2816 < 32767 so int16 // is fine.) // Match the C64 fixed-point scale: 8-bit sub-pixel (256 sub-units per // pixel) so the C64's accel=14 and gravity=1 are usable directly // (14/256 px/frame initial accel; constant 1/256 px/frame gravity). // Position needs int32_t since a 320-wide playfield * 256 sub-units // overflows int16_t. #define ST_SUBPIXEL_SHIFT 8 #define ST_SUBPIXEL (1 << ST_SUBPIXEL_SHIFT) // Taxi cels are the real C64 art (extractSprites.py): the $C0/$C1 // right-facing and $DC/$DD left-facing pairs with the $6A95 per-frame // low-bit flicker; the engine flame is the separate direction-indexed // sprite-2 cel set. Selection lives in stRender.c. typedef enum { ST_STATE_TITLE = 0, ST_STATE_HIGHSCORES, // UP on title -> "THE IMMORTAL CABBIES" ($4C1F) ST_STATE_INSTRUCTIONS, // DOWN on title -> author/about screen ($556A) ST_STATE_OPTIONS, // "GAME VARIATIONS" menu ($5295): pick cabbie count ST_STATE_DEMO, // rolling attract demo (recorded input playback) ST_STATE_PLAYING, ST_STATE_LEVEL_DONE, ST_STATE_GAME_OVER } StGameStateE; typedef enum { ST_DIR_RIGHT = 0, ST_DIR_LEFT = 1 } StFacingE; typedef struct { uint8_t tileX; // landing surface left edge (tile coord) uint8_t tileY; // landing surface row (tile coord) uint8_t tileW; // pad width in tiles uint8_t standX; // passenger stand X (screen px; slot byte6-24) } StPadT; typedef struct { char name[24]; // level display name ("UP & DOWN", etc.) uint8_t tileBankId; // which tile asset (0 = default bank) uint8_t musicId; // UNUSED in C64 Space Taxi -- the // game has no per-level background // music; gameplay is silent except // for SFX. Title plays song 8, score // screen plays song 6 or 7 (see // MECHANICS.md "Sound (SID)"). Kept // here as scaffolding for a possible // future "level-entry jingle" event. uint8_t bgColor; // background palette slot uint8_t borderColor; // border palette slot (for HUD if used) uint8_t taxiSpawnTileX; uint8_t taxiSpawnTileY; // Per-level physics templates. Mirror the C64 templates at // $7D8F/$7D91 (Y/X accel) and $7D93/$7D95 (Y/X gravity). Accels // are unsigned magnitudes; gravities are int8 so a level can pull // upward (e.g. canonical level K = -7). Hand-authored levels can // leave them zero; loader substitutes per-level defaults below. uint8_t xAccel; uint8_t yAccel; int8_t xGrav; int8_t yGrav; // VIC color block from C64 $7D00-$7D08, mapped to $D020-$D028 by // $62F0 (scene-load). borderColor/bgColor above are $7D00/$7D01. // bgColor1/2/3 and spriteMc0/1 only matter in VIC multicolor mode // which the JoeyLib port doesn't reproduce -- they're stored so // the .dat format stays a faithful capture of $7D00-$7D08 but the // runtime ignores them. sprite0Color/sprite1Color drive the cab // and flame placeholder colors when no sprite asset is authored. uint8_t bgColor1; // $D022 (multicolor only, unused) uint8_t bgColor2; // $D023 (multicolor only, unused) uint8_t bgColor3; // $D024 (multicolor only, unused) uint8_t spriteMc0; // $D025 sprite multicolor 0 (unused) uint8_t spriteMc1; // $D026 sprite multicolor 1 (unused) uint8_t sprite0Color; // $D027 sprite 0 (taxi) uint8_t sprite1Color; // $D028 sprite 1 (flame) uint8_t padCount; StPadT pads[ST_MAX_PADS]; // Tilemap: tile-index per cell (row-major). uint8_t tilemap[ST_TILEMAP_W * ST_PLAYFIELD_ROWS]; // Palette slot per cell -- which surface palette index a cell uses. uint8_t colormap[ST_TILEMAP_W * ST_PLAYFIELD_ROWS]; } StLevelT; typedef struct { // 16-bit-fixed-point position (8 bits sub-pixel + 8 bits pixel), // but stored in int32_t so a 320-wide playfield fits without // wrap. `x >> ST_SUBPIXEL_SHIFT` is the pixel column. int32_t x; int32_t y; int16_t vx; // velocity accumulator (sub-pixel/frame) int16_t vy; StFacingE facing; bool thrusting; // any directional input held this frame int8_t thrustDx; // -1 left, 0 neutral, +1 right int8_t thrustDy; // -1 up, 0 neutral, +1 down // Held-direction bit mask, mirroring $716A's encoding (the index // into the $6DB0 flame cel table): 1=UP 2=DOWN 4=LEFT 8=RIGHT. // With gear down the left/right bits are stripped before this is // built, exactly like $6190 AND #$13 feeding $60D6. uint8_t dirMask; // Landing gear ($7197 bit 0). Toggled by a FIRE press edge while // airborne, retracted on takeoff ($65B0), REQUIRED down to land // ($6462) -- gear-up contact with a pad is a crash. bool gearDown; bool fireHeld; // previous-frame fire state (edge detect) bool landed; // sitting on a pad uint8_t onPad; // pad index if landed (0xFF if none) // Death-animation countdown. >0 means crashed -- engine keeps the // cab integrating under gravity (no explosion visual; the C64 // just lets the cab fall, see VERIFIED.md "Stage 1 $665F death // branch"). Reaches 0 -> respawn (or game-over). Mirrors the // C64's $6B24/$6B4C phase-2/3 timing. uint8_t crashTicks; // Crash phase split ($6A72 fall vs $6B2A impact walk): false while // the debris tumbles, true once it hits the floor. bool crashImpacted; // Transporter shrink-warp progress (0 = not warping; 1..64 = the // takeoff/shrink animation $5C91/$5CB9). During the warp the taxi // recedes up through the opening; the screen advances when it ends. uint8_t warpFrame; } StTaxiT; // Passenger lifecycle phases (see the field comments in StPassengerT // for the C64 sequences each one mirrors). typedef enum { ST_PASS_BEAM_IN = 0, // materialize at standX ($CB..$C7) ST_PASS_WAIT, // stand + wave ($66D6 cycle) ST_PASS_WALK_TO_CAB, // walk to the captured taxi X ST_PASS_BOARD_OUT, // shrink into the cab ($C7..$CB) ST_PASS_RIDING, // aboard, invisible ST_PASS_DROP_IN, // delivery: materialize beside the cab ST_PASS_WALK_TO_PAD, // walk to the destination standX ST_PASS_DROP_OUT // shrink out -- fare complete } StPassPhaseE; typedef struct { bool active; bool onboard; // in the cab (true) or waiting at pad (false) uint8_t currentPad; // where they are if waiting uint8_t destPad; int16_t x; // pixel position int16_t y; // Passenger lifecycle, per the traced sequences: materialize // ($CB..$C7 grow-in), wave in place ($66D6 cycle -- he NEVER // walks while waiting), walk to the landed cab ($6811 captures // the taxi X once; $C4/$C5 leftward, $C2/$C3 rightward, +/-2 px // per 3-tick step), shrink into the cab ($C7..$CB), ride, and // the delivery mirror (materialize at the cab, walk to standX, // shrink out). StPassPhaseE phase; int16_t targetX; // walk destination (captured once) // Step counter within the current phase; in ST_PASS_WAIT it is // the 0..3 wave-cycle index, in the walk phases its low bit is // the stride cel alternation. uint8_t waitPhase; } StPassengerT; typedef struct { StGameStateE state; StLevelT level; StTaxiT taxi; StPassengerT passengers[ST_MAX_PASSENGERS]; uint32_t score; uint8_t lives; uint8_t levelIndex; // Attract-demo playback state (ST_STATE_DEMO): the recorded RLE // input stream and its cursor, plus the crash-finished signal the // engine raises instead of decrementing lives ($6B57). const uint8_t *demoStream; uint16_t demoLen; uint16_t demoOff; uint8_t demoTimer; uint8_t demoMask; bool demoEnded; // Player-selectable fare quota (1..4). Mirrors C64 $7213 (the // GAME-VARIATIONS "cabbies" count), set on the title screen via // LEFT/RIGHT joystick ($5310-$5328). This is the GLOBAL number of // fares to deliver before the game completes -- NOT per-level. uint8_t fareTarget; } StGameT; // Public entry points (one per source file). bool stLevelLoad(StLevelT *out, const char *path); void stRenderInit(jlSurfaceT *stage); void stRenderShutdown(void); // Blit the level's static tile art into the stage. Called once per // scene change; falls back to colored solid tiles per index-range // when no tile bank asset is loaded. void stRenderLevel(jlSurfaceT *stage, const StLevelT *level); void stRenderFrame(jlSurfaceT *stage, const StGameT *game); // Tell the renderer the current game.level contents changed. Required // after stLevelLoad even when the StLevelT pointer is unchanged -- // stRenderLevel's dirty-cache compares pointers, not contents. void stRenderLevelChanged(void); void stRenderTitleAdvance(void); bool stRenderTitleIntroDone(void); void stRenderTitleReset(void); // Draw an ASCII string into the stage at tile coords (bx, by) using // the loaded font asset. No-op if the font asset failed to load. void stRenderDrawText(jlSurfaceT *stage, uint8_t bx, uint8_t by, const char *s); void stEngineReset(StGameT *game); void stEngineTick(StGameT *game); // True when the taxi has flown up into the top-wall transporter opening // (used to trigger a screen exit). bool stEngineInTransporter(const StGameT *game); // Begin the transporter shrink-warp (taxi.warpFrame 0 -> 1). void stEngineStartWarp(StGameT *game); // Advance the shrink-warp one frame; returns true when it completes. bool stEngineWarpTick(StGameT *game); // Current shrink step 0..7 (0 = full size, 7 = smallest) for rendering. uint8_t stEngineWarpStep(const StGameT *game); void stPassengerReset(StGameT *game); void stPassengerTick(StGameT *game); // Called when the taxi exits up through the transporter: delivers a // carried "UP PLEASE" fare (if any) before the screen advances. void stPassengerTransporterExit(StGameT *game); void stAudioInit(void); void stAudioShutdown(void); void stAudioFrameTick(void); // call once per host frame; counts down SFX void stAudioPlayMusic(uint8_t musicId); void stAudioStopMusic(void); void stAudioSfxThrust(bool on); void stAudioSfxGear(void); void stAudioSfxLand(void); void stAudioSfxPickup(void); void stAudioSfxDropoff(void); void stAudioSfxCrash(void); void stHudDraw(jlSurfaceT *stage, const StGameT *game); #endif