700 lines
22 KiB
C
700 lines
22 KiB
C
// Space Taxi (JoeyLib port) -- front-end flow and frame pacing.
|
|
//
|
|
// Build: see make/{dos,amiga,atarist,iigs}.mk -- target is
|
|
// `EXAMPLE=spacetaxi`.
|
|
//
|
|
// The game runs in the host-independent simulation (stSim.c and
|
|
// friends); this file is the C64's outer flow around it: the title
|
|
// with its intro, the attract-demo rotation, the two GAME VARIATIONS
|
|
// menus, the level-name intro screen, the players' turns, game over
|
|
// and the return to the title ($5EC4 / $48AD / $4741 / $5010 / $61FB).
|
|
//
|
|
// Pacing: the C64 runs one game tick per two video frames (its main
|
|
// loop waits on the raster twice), 30 Hz on NTSC. Every port steps the
|
|
// simulation from its own vertical blank count: exactly one tick per
|
|
// two frames on 60 Hz and 50 Hz displays (the latter is the PAL C64's
|
|
// own cadence), and three ticks per seven frames on the 70 Hz VGA
|
|
// mode, so motion is a steady multiple of the display and never
|
|
// keyed to a wall clock.
|
|
|
|
#include <string.h>
|
|
|
|
#include "spacetaxi.h"
|
|
#include "stDemoStreams.h"
|
|
|
|
// Diagnostics. On the IIgs the logger drags in vsnprintf and an 8 KB
|
|
// ring buffer that the bank-0 BSS budget cannot spare, and the game has
|
|
// no need of them there, so logging compiles to nothing on the 65816
|
|
// and stays on the other ports.
|
|
#if defined(__W65816__)
|
|
#define ST_LOG_RESET() ((void)0)
|
|
#define ST_LOG(...) ((void)0)
|
|
#else
|
|
#define ST_LOG_RESET() jlLogReset()
|
|
#define ST_LOG(...) jlLogF(__VA_ARGS__)
|
|
#endif
|
|
|
|
#define ST_TICK_HZ 30u
|
|
#define ST_MAX_CATCHUP_TICKS 4u
|
|
#define ST_LEVEL_COUNT 24u
|
|
// "GET READY" shows over the old screen while the C64 cycles the
|
|
// leaving-passenger sprites (three busy waits, about half a second).
|
|
#define ST_GET_READY_TICKS 15u
|
|
// $5FBB: the GAME OVER pause is six full busy waits, about two seconds.
|
|
#define ST_GAME_OVER_TICKS 60u
|
|
// Menu joystick repeat delay ($4101 with X = $82).
|
|
#define ST_MENU_REPEAT_TICKS 8u
|
|
|
|
typedef enum {
|
|
ST_STATE_TITLE = 0,
|
|
ST_STATE_HIGHSCORES,
|
|
ST_STATE_ABOUT,
|
|
ST_STATE_CABBIES,
|
|
ST_STATE_SHIFT,
|
|
ST_STATE_GET_READY,
|
|
ST_STATE_INTRO,
|
|
ST_STATE_PLAYING,
|
|
ST_STATE_GAME_OVER
|
|
} StStateE;
|
|
|
|
typedef struct {
|
|
StStateE state;
|
|
bool demo; // the attract demo owns PLAYING
|
|
uint8_t demoRotation; // $5143
|
|
uint8_t variation; // $7221: 1..5
|
|
uint8_t usedLevels[ST_LEVEL_COUNT]; // $5254 (random shift)
|
|
uint8_t pendingLevel; // $716B as an index
|
|
uint16_t waitTicks;
|
|
uint8_t menuSel; // cabbies 0..3 / shift 0..4
|
|
uint8_t menuRepeat;
|
|
bool fireArmed; // $445D: FIRE must be released first
|
|
uint8_t lastFrame;
|
|
uint16_t paceAcc;
|
|
} StGameT;
|
|
|
|
// Menu texts ($5347..$545D) in screen codes.
|
|
static const uint8_t kTextGameVariations[] = "GAME VARIATIONS";
|
|
static const uint8_t kTextCabbies[] = "SELECT NUMBER OF CABBIES:";
|
|
static const uint8_t kTextDigits[] = "1 2 3 4";
|
|
static const uint8_t kTextLeftRight[] = "USE JOYSTICK: LEFT OR RIGHT,";
|
|
static const uint8_t kTextFire[] = "FIRE TO SELECT";
|
|
static const uint8_t kTextSelectShift[] = "SELECT SHIFT:";
|
|
static const uint8_t *kTextShifts[5] = {
|
|
(const uint8_t *)"1. MORNING SHIFT - BEGINNER",
|
|
(const uint8_t *)"2. DAY SHIFT - INTERMEDIATE",
|
|
(const uint8_t *)"3. NIGHT SHIFT - EXPERT",
|
|
(const uint8_t *)"4. STANDARD 24 HOUR SHIFT",
|
|
(const uint8_t *)"5. RANDOM 24 HOUR SHIFT",
|
|
};
|
|
static const uint8_t kTextUpDown[] = "USE JOYSTICK: UP OR DOWN,";
|
|
static const uint8_t kTextGetReady[] = " GET READY FOR A RIDE ";
|
|
static const uint8_t kTextOnTheTaxi[] = " ON THE SPACE TAXI! ";
|
|
static const uint8_t kTextGameOver[] = " GAME OVER! ";
|
|
static const uint8_t kTextBlank12[] = " ";
|
|
// $4C1F / $556A screens.
|
|
static const uint8_t kTextImmortal[] = "THE IMMORTAL CABBIES";
|
|
static const uint8_t *kHighScores[8] = {
|
|
(const uint8_t *)"3877.56 MICHAEL PLATE",
|
|
(const uint8_t *)"1809.51 MICHAEL PLATE",
|
|
(const uint8_t *)"1179.87 ANDREAS PLATE",
|
|
(const uint8_t *)" 892.65 MICHAEL PLATE",
|
|
(const uint8_t *)" 685.37 ANDREAS PLATE",
|
|
(const uint8_t *)" 629.45 MICHAEL PLATE",
|
|
(const uint8_t *)" 569.50 MICHAEL PLATE",
|
|
(const uint8_t *)" 504.97 ANDREAS PLATE",
|
|
};
|
|
static const uint8_t *kAboutLines[7] = {
|
|
(const uint8_t *)"THIS PROGRAM DEVELOPED AND WRITTEN BY",
|
|
(const uint8_t *)"JOHN F. KUTCHER",
|
|
(const uint8_t *)"11/21/65",
|
|
(const uint8_t *)"CURRENTLY, AS OF JANUARY 1984,",
|
|
(const uint8_t *)"HE IS ATTENDING JOHNS HOPKINS",
|
|
(const uint8_t *)"UNIVERSITY IN BALTIMORE, MD",
|
|
(const uint8_t *)"ALSO TRY RESCUE SQUAD BY JOHN KUTCHER",
|
|
};
|
|
static const uint8_t kTextReturn[] = "PRESS FIRE TO RETURN";
|
|
|
|
static StGameT gGame;
|
|
static StSimT gSim;
|
|
// One level buffer serves both gameplay and the title screen: the title
|
|
// is another scene, and gameplay never needs it at the same time, so it
|
|
// is loaded into gLevel on each return to the title. (Keeping a second
|
|
// StLevelT cost ~3.7 KB of static state the IIgs bank-0 BSS can't spare.)
|
|
static StLevelT gLevel;
|
|
|
|
|
|
static void blankScreen(StSimT *sim);
|
|
static void enterTitle(void);
|
|
static void gameOverEnter(void);
|
|
static bool joyFire(void);
|
|
static uint8_t joyMask(void);
|
|
static bool loadLevel(uint8_t index);
|
|
static uint8_t nextLevelForShift(void);
|
|
static uint8_t paceTicks(void);
|
|
static void runCabbiesMenu(uint8_t ticks);
|
|
static void runDemoEnd(void);
|
|
static void runIntro(uint8_t ticks);
|
|
static void runPlaying(uint8_t ticks);
|
|
static void runShiftMenu(uint8_t ticks);
|
|
static void runTitle(uint8_t ticks);
|
|
static void shiftMenuColor(uint8_t row, uint8_t color);
|
|
static void showCabbiesMenu(void);
|
|
static void showShiftMenu(void);
|
|
static void startDemoScreen(void);
|
|
static void startIntro(void);
|
|
static void startNewScreen(void);
|
|
|
|
|
|
// $40CA -- spaces everywhere, black.
|
|
static void blankScreen(StSimT *sim) {
|
|
memset(sim->screen, ST_CHAR_SPACE, ST_SCREEN_CELLS);
|
|
memset(sim->color, 0, ST_SCREEN_CELLS);
|
|
stSimDirtyAll(sim);
|
|
sim->bgColor = 0u;
|
|
sim->borderColor = 0u;
|
|
sim->frame.enableMask = 0u;
|
|
stRenderSceneChanged(sim);
|
|
}
|
|
|
|
|
|
// $477A -- back to the title, intro from the top.
|
|
static void enterTitle(void) {
|
|
gSim.demoMode = 0u;
|
|
if (!stLevelLoad(&gLevel, "levels/title.dat")) {
|
|
ST_LOG("spacetaxi: ! title load FAILED");
|
|
}
|
|
stTitleEnter(&gSim, &gLevel);
|
|
stRenderSceneChanged(&gSim);
|
|
stAudioNoise(false);
|
|
stAudioSilence();
|
|
gGame.state = ST_STATE_TITLE;
|
|
gGame.fireArmed = false;
|
|
}
|
|
|
|
|
|
// $5FBB -- "GAME OVER!" over the play field, sprites hidden.
|
|
static void gameOverEnter(void) {
|
|
stSimDrawText(&gSim, 14u, 10u, kTextBlank12, 1u);
|
|
stSimDrawText(&gSim, 14u, 11u, kTextGameOver, 1u);
|
|
stSimDrawText(&gSim, 14u, 12u, kTextBlank12, 1u);
|
|
gSim.frame.enableMask = 0u;
|
|
stAudioNoise(false);
|
|
stAudioSilence();
|
|
gGame.waitTicks = ST_GAME_OVER_TICKS;
|
|
gGame.state = ST_STATE_GAME_OVER;
|
|
}
|
|
|
|
|
|
static bool joyFire(void) {
|
|
return jlKeyDown(KEY_SPACE) || jlJoyDown(JOYSTICK_0, JOY_BUTTON_0);
|
|
}
|
|
|
|
|
|
// The C64's EOR'd $DC00: bit0 UP, 1 DOWN, 2 LEFT, 3 RIGHT, 4 FIRE, bit 7 set.
|
|
static uint8_t joyMask(void) {
|
|
int8_t jx = jlJoystickX(JOYSTICK_0);
|
|
int8_t jy = jlJoystickY(JOYSTICK_0);
|
|
uint8_t mask = 0x80u;
|
|
|
|
if (jlKeyDown(KEY_UP) || jy < -32) {
|
|
mask |= 0x01u;
|
|
}
|
|
if (jlKeyDown(KEY_DOWN) || jy > 32) {
|
|
mask |= 0x02u;
|
|
}
|
|
if (jlKeyDown(KEY_LEFT) || jx < -32) {
|
|
mask |= 0x04u;
|
|
}
|
|
if (jlKeyDown(KEY_RIGHT) || jx > 32) {
|
|
mask |= 0x08u;
|
|
}
|
|
if (joyFire()) {
|
|
mask |= 0x10u;
|
|
}
|
|
return mask;
|
|
}
|
|
|
|
|
|
static bool loadLevel(uint8_t index) {
|
|
char path[32];
|
|
|
|
if (index >= ST_LEVEL_COUNT) {
|
|
return false;
|
|
}
|
|
// Hand-rolled "levels/levelNN.dat" (NN = index+1, 01..24) so the
|
|
// binary does not drag in the whole formatted-output machinery.
|
|
{
|
|
uint8_t n = (uint8_t)(index + 1u);
|
|
memcpy(path, "levels/level", 12u);
|
|
path[12] = (char)('0' + n / 10u);
|
|
path[13] = (char)('0' + n % 10u);
|
|
memcpy(path + 14, ".dat", 5u);
|
|
}
|
|
if (!stLevelLoad(&gLevel, path)) {
|
|
ST_LOG("spacetaxi: ! cannot load %s", path);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
// $5010 -- which screen comes next for the chosen shift. Returns 0xFF
|
|
// when the shift is over.
|
|
static uint8_t nextLevelForShift(void) {
|
|
StSimT *sim = &gSim;
|
|
|
|
if (gGame.variation <= 3u) {
|
|
// $517D: eight screens per shift, A..H / I..P / Q..X.
|
|
if (sim->levelState == 8u) {
|
|
return 0xFFu;
|
|
}
|
|
return (uint8_t)((gGame.variation - 1u) * 8u + sim->levelState);
|
|
}
|
|
if (sim->levelState >= ST_LEVEL_COUNT) {
|
|
return 0xFFu;
|
|
}
|
|
if (gGame.variation == 4u) {
|
|
return sim->levelState;
|
|
}
|
|
// $521A: random order without repeats.
|
|
{
|
|
uint8_t x = (uint8_t)(stSimRng(sim, ST_LEVEL_COUNT) - 1u);
|
|
while (gGame.usedLevels[x] != 0u) {
|
|
x = (x == 0u) ? (uint8_t)(ST_LEVEL_COUNT - 1u) : (uint8_t)(x - 1u);
|
|
}
|
|
gGame.usedLevels[x] = 1u;
|
|
return x;
|
|
}
|
|
}
|
|
|
|
|
|
// Game ticks due this rendered frame, from the vertical blank count:
|
|
// one tick per two frames on 50/60 Hz displays, three per seven on
|
|
// the 70 Hz VGA mode. jlFrameCount is 8-bit-wrapped so a stall of any
|
|
// length just clamps to the catch-up cap.
|
|
static uint8_t paceTicks(void) {
|
|
uint16_t hz = jlFrameHz();
|
|
uint8_t now = (uint8_t)jlFrameCount();
|
|
uint8_t frames = (uint8_t)(now - gGame.lastFrame);
|
|
uint8_t ticks = 0u;
|
|
|
|
gGame.lastFrame = now;
|
|
if (frames > 16u) {
|
|
frames = 16u;
|
|
}
|
|
if (hz == 50u || hz == 60u) {
|
|
gGame.paceAcc += (uint16_t)(frames * 35u);
|
|
hz = 70u;
|
|
} else {
|
|
gGame.paceAcc += (uint16_t)(frames * ST_TICK_HZ);
|
|
}
|
|
// Ticks not run this frame stay owed (a slow frame catches up over
|
|
// the next ones); a stall longer than the cap's worth is forgiven.
|
|
while (gGame.paceAcc >= hz && ticks < ST_MAX_CATCHUP_TICKS) {
|
|
gGame.paceAcc = (uint16_t)(gGame.paceAcc - hz);
|
|
ticks++;
|
|
}
|
|
if (gGame.paceAcc > (uint16_t)(hz * ST_MAX_CATCHUP_TICKS)) {
|
|
gGame.paceAcc = (uint16_t)(hz * ST_MAX_CATCHUP_TICKS);
|
|
}
|
|
return ticks;
|
|
}
|
|
|
|
|
|
// $52F7 -- LEFT/RIGHT cycle 1..4, FIRE confirms.
|
|
static void runCabbiesMenu(uint8_t ticks) {
|
|
uint8_t mask = joyMask();
|
|
|
|
if (!gGame.fireArmed) {
|
|
if ((mask & 0x10u) == 0u) {
|
|
gGame.fireArmed = true;
|
|
}
|
|
return;
|
|
}
|
|
if ((mask & 0x10u) != 0u) {
|
|
stSimPutColor(&gSim, (uint8_t)(28u + gGame.menuSel * 2u), 2u, 1u);
|
|
gSim.playerCount = (uint8_t)(gGame.menuSel + 1u);
|
|
showShiftMenu();
|
|
return;
|
|
}
|
|
if (gGame.menuRepeat > 0u) {
|
|
gGame.menuRepeat = (uint8_t)(gGame.menuRepeat - ((ticks < gGame.menuRepeat) ? ticks : gGame.menuRepeat));
|
|
return;
|
|
}
|
|
if ((mask & 0x0Cu) != 0u) {
|
|
stSimPutColor(&gSim, (uint8_t)(28u + gGame.menuSel * 2u), 2u, 6u);
|
|
if ((mask & 0x08u) != 0u) {
|
|
gGame.menuSel = (uint8_t)((gGame.menuSel + 1u) & 3u);
|
|
} else {
|
|
gGame.menuSel = (uint8_t)((gGame.menuSel - 1u) & 3u);
|
|
}
|
|
stSimPutColor(&gSim, (uint8_t)(28u + gGame.menuSel * 2u), 2u, 3u);
|
|
gGame.menuRepeat = ST_MENU_REPEAT_TICKS;
|
|
}
|
|
}
|
|
|
|
|
|
// The demo crashed, flew out or was interrupted: $5EC4 -> title.
|
|
static void runDemoEnd(void) {
|
|
gGame.demo = false;
|
|
enterTitle();
|
|
}
|
|
|
|
|
|
// $4666 loop: two frames per game tick; joystick ends a demo.
|
|
static void runIntro(uint8_t ticks) {
|
|
uint8_t k;
|
|
|
|
if (gGame.demo && (joyMask() & 0x1Fu) != 0u) {
|
|
runDemoEnd();
|
|
return;
|
|
}
|
|
for (k = 0u; k < (uint8_t)(ticks * 2u); k++) {
|
|
if (stIntroStep(&gSim)) {
|
|
stSimEnterLevel(&gSim, &gLevel);
|
|
stRenderSceneChanged(&gSim);
|
|
if (gGame.demo) {
|
|
const StDemoEntryT *e = &kDemoRotation[(uint8_t)(gGame.demoRotation - 1u) & 3u];
|
|
stSimSetDemoStream(&gSim, e->stream, e->length);
|
|
}
|
|
gGame.state = ST_STATE_PLAYING;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// The main loop proper, one $5F40 iteration per tick.
|
|
static void runPlaying(uint8_t ticks) {
|
|
uint8_t k;
|
|
|
|
gSim.rawInput = joyMask();
|
|
for (k = 0u; k < ticks; k++) {
|
|
StTickResultE r = stSimTick(&gSim);
|
|
stAudioTick();
|
|
if (r == ST_TICK_CONTINUE) {
|
|
continue;
|
|
}
|
|
if (gGame.demo) {
|
|
runDemoEnd();
|
|
return;
|
|
}
|
|
switch (r) {
|
|
case ST_TICK_LEVEL_EXIT:
|
|
startNewScreen();
|
|
return;
|
|
case ST_TICK_LIFE_LOST:
|
|
stSimRespawn(&gSim);
|
|
stRenderSceneChanged(&gSim);
|
|
break;
|
|
case ST_TICK_PLAYER_OUT:
|
|
gameOverEnter();
|
|
return;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// $54F8 -- UP/DOWN pick the shift, FIRE confirms and starts the game.
|
|
static void runShiftMenu(uint8_t ticks) {
|
|
uint8_t mask = joyMask();
|
|
|
|
if (!gGame.fireArmed) {
|
|
if ((mask & 0x10u) == 0u) {
|
|
gGame.fireArmed = true;
|
|
}
|
|
return;
|
|
}
|
|
if ((mask & 0x10u) != 0u) {
|
|
shiftMenuColor(gGame.menuSel, 1u);
|
|
gGame.variation = (uint8_t)(gGame.menuSel + 1u);
|
|
// $5EC4 + $5333: the game starts; the first advance wraps to
|
|
// player 0 and loads the first screen.
|
|
stSimNewGame(&gSim, gSim.playerCount, false);
|
|
gSim.player = (uint8_t)(gSim.playerCount - 1u);
|
|
memset(gGame.usedLevels, 0, sizeof(gGame.usedLevels));
|
|
gGame.demo = false;
|
|
startNewScreen();
|
|
return;
|
|
}
|
|
if (gGame.menuRepeat > 0u) {
|
|
gGame.menuRepeat = (uint8_t)(gGame.menuRepeat - ((ticks < gGame.menuRepeat) ? ticks : gGame.menuRepeat));
|
|
return;
|
|
}
|
|
if ((mask & 0x03u) != 0u) {
|
|
shiftMenuColor(gGame.menuSel, 6u);
|
|
if ((mask & 0x02u) != 0u) {
|
|
gGame.menuSel = (uint8_t)((gGame.menuSel + 1u) % 5u);
|
|
} else {
|
|
gGame.menuSel = (uint8_t)((gGame.menuSel + 4u) % 5u);
|
|
}
|
|
shiftMenuColor(gGame.menuSel, 3u);
|
|
gGame.menuRepeat = ST_MENU_REPEAT_TICKS;
|
|
}
|
|
}
|
|
|
|
|
|
// $47E4 -- the title loop: intro, then FIRE / UP / DOWN, else the demo.
|
|
static void runTitle(uint8_t ticks) {
|
|
uint8_t mask = joyMask();
|
|
uint8_t k;
|
|
|
|
if (!gGame.fireArmed) {
|
|
if ((mask & 0x10u) == 0u) {
|
|
gGame.fireArmed = true;
|
|
}
|
|
}
|
|
for (k = 0u; k < ticks; k++) {
|
|
if (stTitleTick(&gSim)) {
|
|
// $4818: intro over -> the attract demo (demo mode on).
|
|
startDemoScreen();
|
|
return;
|
|
}
|
|
}
|
|
if ((mask & 0x10u) != 0u && gGame.fireArmed) {
|
|
showCabbiesMenu();
|
|
return;
|
|
}
|
|
if ((mask & 0x01u) != 0u) {
|
|
uint8_t i;
|
|
blankScreen(&gSim);
|
|
stSimDrawText(&gSim, 10u, 1u, kTextImmortal, 1u);
|
|
for (i = 0u; i < 8u; i++) {
|
|
stSimDrawText(&gSim, 9u, (uint8_t)(4u + i * 2u), kHighScores[i], 1u);
|
|
}
|
|
stSimDrawText(&gSim, 10u, 22u, kTextReturn, 1u);
|
|
gGame.state = ST_STATE_HIGHSCORES;
|
|
gGame.fireArmed = false;
|
|
return;
|
|
}
|
|
if ((mask & 0x02u) != 0u) {
|
|
static const uint8_t kAboutCols[7] = { 1u, 12u, 15u, 4u, 5u, 6u, 1u };
|
|
static const uint8_t kAboutRows[7] = { 3u, 5u, 7u, 10u, 12u, 14u, 18u };
|
|
uint8_t i;
|
|
blankScreen(&gSim);
|
|
for (i = 0u; i < 7u; i++) {
|
|
stSimDrawText(&gSim, kAboutCols[i], kAboutRows[i], kAboutLines[i], 1u);
|
|
}
|
|
stSimDrawText(&gSim, 10u, 22u, kTextReturn, 1u);
|
|
gGame.state = ST_STATE_ABOUT;
|
|
gGame.fireArmed = false;
|
|
}
|
|
}
|
|
|
|
|
|
// $554F -- colour one shift line (row 10 + 2*sel, 35 cells from col 2).
|
|
static void shiftMenuColor(uint8_t sel, uint8_t color) {
|
|
uint8_t col;
|
|
|
|
for (col = 2u; col < 37u; col++) {
|
|
stSimPutColor(&gSim, col, (uint8_t)(10u + sel * 2u), color);
|
|
}
|
|
}
|
|
|
|
|
|
// $5295 -- GAME VARIATIONS page 1.
|
|
static void showCabbiesMenu(void) {
|
|
blankScreen(&gSim);
|
|
stSimDrawText(&gSim, 13u, 0u, kTextGameVariations, 5u);
|
|
stSimDrawText(&gSim, 1u, 2u, kTextCabbies, 7u);
|
|
stSimDrawText(&gSim, 5u, 4u, kTextLeftRight, 11u);
|
|
stSimDrawText(&gSim, 19u, 5u, kTextFire, 11u);
|
|
stSimDrawText(&gSim, 28u, 2u, kTextDigits, 6u);
|
|
gGame.menuSel = 0u;
|
|
stSimPutColor(&gSim, 28u, 2u, 3u);
|
|
gGame.state = ST_STATE_CABBIES;
|
|
gGame.fireArmed = false;
|
|
gGame.menuRepeat = 0u;
|
|
}
|
|
|
|
|
|
// $545E -- GAME VARIATIONS page 2.
|
|
static void showShiftMenu(void) {
|
|
uint8_t i;
|
|
|
|
blankScreen(&gSim);
|
|
stSimDrawText(&gSim, 1u, 8u, kTextSelectShift, 7u);
|
|
for (i = 0u; i < 5u; i++) {
|
|
stSimDrawText(&gSim, 2u, (uint8_t)(10u + i * 2u), kTextShifts[i], 6u);
|
|
}
|
|
stSimDrawText(&gSim, 5u, 21u, kTextUpDown, 11u);
|
|
stSimDrawText(&gSim, 19u, 22u, kTextFire, 11u);
|
|
gGame.menuSel = 0u;
|
|
shiftMenuColor(0u, 3u);
|
|
gGame.state = ST_STATE_SHIFT;
|
|
gGame.fireArmed = false;
|
|
gGame.menuRepeat = 0u;
|
|
}
|
|
|
|
|
|
// $48AD + $61FB + $50C6 -- the next demo screen: "GET READY" over the
|
|
// title, then the level intro.
|
|
static void startDemoScreen(void) {
|
|
const StDemoEntryT *e = &kDemoRotation[gGame.demoRotation & 3u];
|
|
|
|
gGame.demoRotation++;
|
|
gGame.demo = true;
|
|
stSimNewGame(&gSim, 1u, true);
|
|
gSim.player = 0u;
|
|
(void)stSimAdvancePlayer(&gSim);
|
|
gSim.levelState++;
|
|
if (!loadLevel(e->levelIndex)) {
|
|
enterTitle();
|
|
return;
|
|
}
|
|
stSimDrawText(&gSim, 10u, 8u, kTextGetReady, 5u);
|
|
stSimDrawText(&gSim, 10u, 10u, kTextOnTheTaxi, 5u);
|
|
gSim.frame.enableMask &= 0x07u;
|
|
gGame.waitTicks = ST_GET_READY_TICKS;
|
|
gGame.state = ST_STATE_GET_READY;
|
|
}
|
|
|
|
|
|
// $62F0 -> $4523 -- the level-name screen for gLevel.
|
|
static void startIntro(void) {
|
|
stIntroEnter(&gSim, &gLevel);
|
|
stRenderSceneChanged(&gSim);
|
|
gGame.state = ST_STATE_INTRO;
|
|
}
|
|
|
|
|
|
// $61FB -- the next player's turn on this screen, or the next screen
|
|
// once everybody has had it; players out of cabs are skipped.
|
|
static void startNewScreen(void) {
|
|
uint8_t guard = 0u;
|
|
|
|
for (;;) {
|
|
if (stSimAdvancePlayer(&gSim)) {
|
|
uint8_t next = nextLevelForShift();
|
|
if (next == 0xFFu) {
|
|
// $602F: the shift is over.
|
|
enterTitle();
|
|
return;
|
|
}
|
|
gGame.pendingLevel = next;
|
|
if (!loadLevel(next)) {
|
|
enterTitle();
|
|
return;
|
|
}
|
|
gSim.levelState++;
|
|
}
|
|
if (gSim.cabs[gSim.player] != 0u) {
|
|
break;
|
|
}
|
|
guard++;
|
|
if (guard > (uint8_t)(ST_MAX_PLAYERS * 2u)) {
|
|
enterTitle();
|
|
return;
|
|
}
|
|
}
|
|
startIntro();
|
|
}
|
|
|
|
|
|
int main(void) {
|
|
jlConfigT config;
|
|
jlSurfaceT *stage;
|
|
|
|
#if defined(JOEYLIB_PLATFORM_IIGS)
|
|
// One 64 KB bank: enough for the cab, exhaust and passenger cels.
|
|
config.codegenBytes = 60UL * 1024;
|
|
#else
|
|
config.codegenBytes = 160UL * 1024;
|
|
#endif
|
|
config.audioBytes = 32UL * 1024;
|
|
if (!jlInit(&config)) {
|
|
return 1;
|
|
}
|
|
ST_LOG_RESET();
|
|
ST_LOG("spacetaxi: build=%s %s", __DATE__, __TIME__);
|
|
stage = jlStageGet();
|
|
if (stage == NULL) {
|
|
jlShutdown();
|
|
return 1;
|
|
}
|
|
memset(&gGame, 0, sizeof(gGame));
|
|
memset(&gSim, 0, sizeof(gSim));
|
|
stRenderInit(stage);
|
|
stAudioInit();
|
|
// Boot: the playback buffer as the C64 leaves it, and the state a
|
|
// fresh machine has when the first title intro starts ($4092 + the
|
|
// title's own sparkle/walk history from the dump).
|
|
stSimNewGame(&gSim, 1u, false);
|
|
stSimSeedDemoBuffer(&gSim, kDemoBufferInit, ST_DEMO_BUFFER_BYTES);
|
|
gSim.waveIdx = 3u;
|
|
stRenderPrewarm(stage, &gSim);
|
|
enterTitle();
|
|
gGame.lastFrame = (uint8_t)jlFrameCount();
|
|
|
|
for (;;) {
|
|
uint8_t ticks;
|
|
|
|
jlInputPoll();
|
|
if (jlKeyPressed(KEY_ESCAPE)) {
|
|
break;
|
|
}
|
|
ticks = paceTicks();
|
|
switch (gGame.state) {
|
|
case ST_STATE_TITLE:
|
|
runTitle(ticks);
|
|
break;
|
|
case ST_STATE_HIGHSCORES:
|
|
case ST_STATE_ABOUT:
|
|
if (!gGame.fireArmed) {
|
|
gGame.fireArmed = !joyFire();
|
|
} else if (joyFire()) {
|
|
enterTitle();
|
|
}
|
|
break;
|
|
case ST_STATE_CABBIES:
|
|
runCabbiesMenu(ticks);
|
|
break;
|
|
case ST_STATE_SHIFT:
|
|
runShiftMenu(ticks);
|
|
break;
|
|
case ST_STATE_GET_READY:
|
|
if (gGame.demo && (joyMask() & 0x1Fu) != 0u) {
|
|
runDemoEnd();
|
|
break;
|
|
}
|
|
if (gGame.waitTicks > ticks) {
|
|
gGame.waitTicks = (uint16_t)(gGame.waitTicks - ticks);
|
|
} else {
|
|
gGame.waitTicks = 0u;
|
|
startIntro();
|
|
}
|
|
break;
|
|
case ST_STATE_INTRO:
|
|
runIntro(ticks);
|
|
break;
|
|
case ST_STATE_PLAYING:
|
|
runPlaying(ticks);
|
|
break;
|
|
case ST_STATE_GAME_OVER:
|
|
if (gGame.waitTicks > ticks) {
|
|
gGame.waitTicks = (uint16_t)(gGame.waitTicks - ticks);
|
|
} else {
|
|
// $6BD0: one more player done; everybody done -> title.
|
|
gSim.playersDone++;
|
|
if (gSim.playersDone == gSim.playerCount) {
|
|
enterTitle();
|
|
} else {
|
|
startNewScreen();
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
enterTitle();
|
|
break;
|
|
}
|
|
stRenderFrame(stage, &gSim);
|
|
jlAudioFrameTick();
|
|
}
|
|
|
|
stAudioShutdown();
|
|
stRenderShutdown();
|
|
jlShutdown();
|
|
return 0;
|
|
}
|