// Space Taxi (JoeyLib port) -- main loop and game state machine. // // Build: see make/{dos,amiga,atarist,iigs}.mk -- target is // `EXAMPLE=spacetaxi`. // // Runtime layout: // 1. jlInit + jlStageGet // 2. stRenderInit loads tile and sprite asset banks from disk // 3. State machine: title -> level-intro -> playing -> done // 4. Each frame: // - jlInputPoll (read joystick + keyboard) // - stEngineTick (taxi physics) // - stPassengerTick (passenger AI) // - stRenderFrame (sprite save/restore + draw + HUD) // - jlAudioFrameTick // - jlWaitVBL #include #include #include "spacetaxi.h" static void applyInput(StGameT *game); static bool loadLevelByIndex(StGameT *game, uint8_t idx); // All 24 canonical Space Taxi levels (A..X), extracted from the C64 // ROM via stuff/spacetaxi/romToLevel.py. level01 = scene 0 = "A", // level24 = scene 23 = "X". static const char *gLevelPaths[] = { "DATA/levels/level01.dat", "DATA/levels/level02.dat", "DATA/levels/level03.dat", "DATA/levels/level04.dat", "DATA/levels/level05.dat", "DATA/levels/level06.dat", "DATA/levels/level07.dat", "DATA/levels/level08.dat", "DATA/levels/level09.dat", "DATA/levels/level10.dat", "DATA/levels/level11.dat", "DATA/levels/level12.dat", "DATA/levels/level13.dat", "DATA/levels/level14.dat", "DATA/levels/level15.dat", "DATA/levels/level16.dat", "DATA/levels/level17.dat", "DATA/levels/level18.dat", "DATA/levels/level19.dat", "DATA/levels/level20.dat", "DATA/levels/level21.dat", "DATA/levels/level22.dat", "DATA/levels/level23.dat", "DATA/levels/level24.dat", }; #define ST_LEVEL_COUNT (sizeof(gLevelPaths) / sizeof(gLevelPaths[0])) static void applyInput(StGameT *game) { StTaxiT *t = &game->taxi; int8_t jx; int8_t jy; bool up; bool down; bool left; bool right; // Joystick port 2 is the canonical Space Taxi control. Keyboard // is the fallback so the same binary is testable on hosts without // a stick. jlJoystickX/Y return -127..127; treat anything past // 1/4 deflection as "held in that direction". jx = jlJoystickX(JOYSTICK_0); jy = jlJoystickY(JOYSTICK_0); up = jlKeyDown(KEY_UP) || jy < -32; down = jlKeyDown(KEY_DOWN) || jy > 32; left = jlKeyDown(KEY_LEFT) || jx < -32; right = jlKeyDown(KEY_RIGHT) || jx > 32; // C64 Space Taxi gameplay only acts on the 4 directional bits of // $DC00; the fire button is masked out except on menu/title // screens. (Confirmed via disassembly at $6040-$60D6 in mem0801: // the main input handler only loads X/Y velocity templates when // L/R or U/D are held, fire is never tested in this path.) // Menu fire-to-advance is handled by main()'s state switch. t->thrustDx = (int8_t)((right ? 1 : 0) - (left ? 1 : 0)); t->thrustDy = (int8_t)((down ? 1 : 0) - (up ? 1 : 0)); t->thrusting = (up || down || left || right); // Facing is purely cosmetic (sprite cel selection). Update when // horizontal thrust is commanded; keep last facing otherwise so // a stopped cab stays facing where it was. if (left) { t->facing = ST_DIR_LEFT; } else if (right) { t->facing = ST_DIR_RIGHT; } } static bool loadLevelByIndex(StGameT *game, uint8_t idx) { if (idx >= ST_LEVEL_COUNT) { return false; } if (!stLevelLoad(&game->level, gLevelPaths[idx])) { return false; } game->levelIndex = idx; stRenderLevelChanged(); return true; } int main(void) { jlConfigT config; jlSurfaceT *stage; StGameT game; // Sprite codegen arena: after Phase 11 (shared-walker rewrite of // the planar sprite path), sprites no longer consume arena bytes // -- the per-cel work happens inside halSpriteDrawPlanes / // halSpriteSavePlanes / halSpriteRestorePlanes as static lib code. // 32 KB is plenty for whatever other codegen needs are around. config.codegenBytes = 32UL * 1024; config.maxSurfaces = 4; // stage + work config.audioBytes = 32UL * 1024; // music + SFX if (!jlInit(&config)) { fprintf(stderr, "jlInit: %s\n", jlLastError()); return 1; } jlLogReset(); jlLogF("spacetaxi: build=%s %s", __DATE__, __TIME__); stage = jlStageGet(); if (stage == NULL) { jlLogF("spacetaxi: ! jlStageGet returned NULL"); jlShutdown(); return 1; } memset(&game, 0, sizeof(game)); game.state = ST_STATE_TITLE; game.lives = 5; game.levelIndex = 0; game.fareTarget = 1; // C64 default: 1 fare per game (see $48AF). jlLogF("spacetaxi: stRenderInit ..."); stRenderInit(stage); jlLogF("spacetaxi: stAudioInit ..."); stAudioInit(); // Load the title-screen tilemap. raw.bin captured the C64 game // at the title screen, so its screen RAM at $0400 IS the title // (the big SPACE TAXI letters, JOHN F. BUTCHER credit, joystick // instructions, etc.). romToLevel.py emits title.dat from that // capture. Falls through to a black field if the asset is // missing. jlLogF("spacetaxi: stLevelLoad title.dat ..."); if (stLevelLoad(&game.level, "DATA/levels/title.dat")) { jlLogF("spacetaxi: title loaded, tilebank=%u", (unsigned)game.level.tileBankId); stRenderLevelChanged(); } else { jlLogF("spacetaxi: title load FAILED"); } jlLogFlush(); for (;;) { jlInputPoll(); if (jlKeyPressed(KEY_ESCAPE)) { break; } switch (game.state) { case ST_STATE_TITLE: { // C64 title only listens for UP / DOWN / FIRE per the // baked joystick instructions ("UP FOR HIGH SCORES, // DOWN FOR INSTRUCTIONS, FIRE BUTTON TO BEGIN"). The // fare-count selector ($5310-$5328) lives on a separate // options/menu scene at $5295 (header strings) and // $52E1/$533C (the "1 2 3 4" digit row), not the main // title. UP / DOWN / options screen are TODO; for now // the title only honors FIRE -> begin. if (jlKeyPressed(KEY_SPACE) || jlJoyPressed(JOYSTICK_0, JOY_BUTTON_0)) { game.score = 0; game.lives = 5; game.levelIndex = 0; if (!loadLevelByIndex(&game, 0)) { jlLogF("spacetaxi: ! cannot load level 0 (DATA/levels/level01.dat)"); jlLogFlush(); goto shutdown; } stEngineReset(&game); // C64 goes straight from title to gameplay; no // intermediate "PRESS FIRE TO START" screen. game.state = ST_STATE_PLAYING; } break; } case ST_STATE_PLAYING: { bool wasLanded = game.taxi.landed; applyInput(&game); stEngineTick(&game); stPassengerTick(&game); // Edge-trigger SFX from state deltas. stAudioSfxThrust(game.taxi.thrusting); if (!wasLanded && game.taxi.landed) { stAudioSfxLand(); } break; } case ST_STATE_LEVEL_DONE: // No music to stop -- gameplay is silent. The C64 plays // a score-screen jingle (song 7 / song 6) between levels; // not yet wired up here. if (loadLevelByIndex(&game, (uint8_t)(game.levelIndex + 1))) { stEngineReset(&game); game.state = ST_STATE_PLAYING; } else { // ran out of levels -- back to title (player wins) if (stLevelLoad(&game.level, "DATA/levels/title.dat")) { stRenderLevelChanged(); } game.state = ST_STATE_TITLE; } break; case ST_STATE_GAME_OVER: if (jlKeyPressed(KEY_SPACE)) { // Restore the title tilemap so the press-start // overlay isn't sitting on top of the last-played // level's art. if (stLevelLoad(&game.level, "DATA/levels/title.dat")) { stRenderLevelChanged(); } game.state = ST_STATE_TITLE; } break; default: game.state = ST_STATE_TITLE; break; } // Silence continuous thrust SFX whenever we're not actively // playing (title, level-intro, game-over, level-done). if (game.state != ST_STATE_PLAYING) { stAudioSfxThrust(false); } stRenderFrame(stage, &game); stAudioFrameTick(); jlAudioFrameTick(); // stRenderFrame already does jlWaitVBL right before its // jlStagePresent (sync-on-present), so an extra wait here // would just slow the loop to half framerate. } shutdown: stAudioShutdown(); stRenderShutdown(); jlShutdown(); return 0; }