joeylib2/examples/spacetaxi/spacetaxi.c

256 lines
9.2 KiB
C

// 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 <stdio.h>
#include <string.h>
#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;
}