256 lines
9.2 KiB
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;
|
|
}
|