1060 lines
36 KiB
C
1060 lines
36 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 <joey/file.h>
|
|
|
|
#include "spacetaxi.h"
|
|
#include "stSongData.h"
|
|
#include "stDemoStreams.h"
|
|
|
|
#define ST_TICK_HZ 30u
|
|
#define ST_MAX_CATCHUP_TICKS 4u
|
|
// Screens A..X, then screen Y -- the 25th, " MYSTERY SCREEN !!! " -- which
|
|
// $5167 loads once the 24-hour shifts have cleared all of A..X. Files are
|
|
// levels/level01.dat .. level25.dat, so Y is index 24.
|
|
#define ST_LEVEL_COUNT 24u // A..X, the screens a shift sweeps
|
|
#define ST_SCREEN_Y 24u // the 25th screen ($5171 LDA #$59)
|
|
#define ST_LEVEL_FILES 25u // level01.dat .. level25.dat
|
|
// "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
|
|
|
|
// The immortal cabbies ($4A89 image, $4E35 screen).
|
|
#define ST_HS_ENTRIES 10u
|
|
#define ST_HS_NAME_CHARS 15u
|
|
#define ST_HS_ENTRY_BYTES 24u // score[7] 0 name[15] shift
|
|
#define ST_HS_POINT_AT 4u // "dddd.dd"
|
|
#define ST_HS_GREET_DIGIT_AT 23u // "CONGRATULATIONS CABBIE n,"
|
|
#define ST_HS_NAME_COL 12u // $0664: row 15, column 12
|
|
#define ST_HS_NAME_ROW 15u
|
|
#define ST_HS_CURSOR '%' // $4F4D flips it against blank
|
|
#define ST_HS_BLINK_TICKS 3u // $4101 x $1E between flips, ~55 ms
|
|
#define ST_HS_SAVE_NAME "IMMORTAL"
|
|
|
|
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,
|
|
ST_STATE_JINGLE, // a $44CF tune sounding; sim frozen until it ends
|
|
ST_STATE_NAME_ENTRY // $4E35: a top-ten cabbie types a name
|
|
} 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;
|
|
StStateE afterJingle; // where ST_STATE_JINGLE returns to
|
|
bool gameOverTune; // $5C24: song 5 started after the wait
|
|
uint8_t hsSlot; // $4D51 table slot being named
|
|
uint8_t hsLen; // $4D2C characters typed
|
|
uint8_t hsCursor; // $4F4D the blinking cursor's current glyph
|
|
uint8_t hsBlinkTicks; // ticks toward the next cursor flip
|
|
bool paused; // $4FCB: SHIFT is holding the game
|
|
} StGameT;
|
|
|
|
// The immortal cabbies ($4A89): ten entries of a right-aligned "dddd.dd"
|
|
// score, a name of up to fifteen letters and the shift it was earned on.
|
|
// The C64 keeps this table on its own disk (the $9948 IEC routines load
|
|
// it at boot and save it after every entry); the port keeps the same
|
|
// 240-byte image in a save file. Shipped contents as the dump has them.
|
|
typedef struct {
|
|
uint8_t score[ST_NUMBER_CHARS + 1u]; // ASCII, NUL-terminated
|
|
uint8_t name[ST_HS_NAME_CHARS + 1u]; // ASCII, space-padded, NUL-terminated
|
|
uint8_t shift; // 1..5
|
|
} StHighScoreT;
|
|
|
|
static StHighScoreT gHighScores[ST_HS_ENTRIES] = {
|
|
{ "3877.56", "MICHAEL PLATE ", 4u },
|
|
{ "1809.51", "MICHAEL PLATE ", 2u },
|
|
{ "1179.87", "ANDREAS PLATE ", 4u },
|
|
{ " 892.65", "MICHAEL PLATE ", 1u },
|
|
{ " 685.37", "ANDREAS PLATE ", 1u },
|
|
{ " 629.45", "MICHAEL PLATE ", 4u },
|
|
{ " 569.50", "MICHAEL PLATE ", 1u },
|
|
{ " 504.97", "ANDREAS PLATE ", 2u },
|
|
{ " 498.77", "ANDREAS PLATE ", 5u },
|
|
{ " 490.85", "ANDREAS PLATE ", 2u },
|
|
};
|
|
|
|
// 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 (the table) and $4E35 (the name entry) screens.
|
|
static const uint8_t kTextImmortal[] = "THE IMMORTAL CABBIES";
|
|
static const uint8_t kTextShift[] = "SHIFT";
|
|
static const uint8_t kTextReturnTitle[] = "PRESS FIRE TO RETURN TO TITLE PAGE";
|
|
static const uint8_t kTextCongrats[] = "CONGRATULATIONS CABBIE ,";
|
|
static const uint8_t kTextRanks[] = "YOUR SCORE RANKS YOU AS ONE OF";
|
|
static const uint8_t kTextTopTen[] = "THE TOP TEN CABBIES OF ALL TIME";
|
|
static const uint8_t kTextEnterName[] = "ENTER YOUR NAME BELOW";
|
|
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;
|
|
static jlSurfaceT *gStage;
|
|
// 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, uint8_t border, uint8_t bg);
|
|
static void enterTitle(void);
|
|
static void gameOverEnter(void);
|
|
static void gameOverFinish(void);
|
|
static void highScoreDraw(void);
|
|
static uint8_t highScoreInsert(const uint8_t *score);
|
|
static void highScoreLoad(void);
|
|
static void highScoreSave(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 bool serviceJingle(void);
|
|
static void runCabbiesMenu(uint8_t ticks);
|
|
static void runDemoEnd(void);
|
|
static void runIntro(uint8_t ticks);
|
|
static void runNameEntry(uint8_t ticks);
|
|
static bool nameEntryStart(void);
|
|
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, uint8_t border, uint8_t bg) {
|
|
memset(sim->screen, ST_CHAR_SPACE, ST_SCREEN_CELLS);
|
|
memset(sim->color, 0, ST_SCREEN_CELLS);
|
|
stSimDirtyAll(sim);
|
|
sim->bgColor = bg;
|
|
sim->borderColor = border;
|
|
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");
|
|
}
|
|
stRenderLevelCels(gStage, 0); // the last level's cels go
|
|
stTitleEnter(&gSim, &gLevel);
|
|
stRenderSceneChanged(&gSim);
|
|
stRenderTitleLogo(&gSim);
|
|
stAudioNoise(false);
|
|
stAudioSilence();
|
|
jlMusicStop();
|
|
gGame.state = ST_STATE_TITLE;
|
|
gGame.fireArmed = false;
|
|
}
|
|
|
|
|
|
// $6BD0 -- one more player done; everybody done -> title.
|
|
static void gameOverFinish(void) {
|
|
gSim.playersDone++;
|
|
if (gSim.playersDone == gSim.playerCount) {
|
|
enterTitle();
|
|
} else {
|
|
startNewScreen();
|
|
}
|
|
}
|
|
|
|
|
|
// $4C1F -- the immortal cabbies: ten rows of shift, "$", score and name,
|
|
// each entry drawn only while its last score digit is a digit (a blank
|
|
// row is an unused slot). Song 7 plays under it until FIRE ($4D04).
|
|
static void highScoreDraw(void) {
|
|
uint8_t i;
|
|
|
|
blankScreen(&gSim, 3u, 14u);
|
|
stSimDrawText(&gSim, 9u, 1u, kTextImmortal, 6u);
|
|
stSimDrawText(&gSim, 32u, 3u, kTextShift, 15u);
|
|
for (i = 0u; i < ST_HS_ENTRIES; i++) {
|
|
const StHighScoreT *e = &gHighScores[i];
|
|
uint8_t row = (uint8_t)(4u + i * 2u);
|
|
if (e->score[ST_NUMBER_CHARS - 1u] < '0') {
|
|
continue;
|
|
}
|
|
stSimPutChar(&gSim, 34u, row, (uint8_t)('0' + e->shift));
|
|
stSimPutColor(&gSim, 34u, row, 15u);
|
|
stSimPutChar(&gSim, 4u, row, '$');
|
|
stSimPutColor(&gSim, 4u, row, 1u);
|
|
stSimDrawText(&gSim, 5u, row, e->score, 1u);
|
|
stSimDrawText(&gSim, 15u, row, e->name, 1u);
|
|
}
|
|
stSimDrawText(&gSim, 3u, 24u, kTextReturnTitle, 0u);
|
|
stAudioMusic(ST_SONG_SCORES, false); // $4C29
|
|
}
|
|
|
|
|
|
// $4D92 -- where a score belongs in the table, or ST_HS_ENTRIES when it
|
|
// does not make it. The table is only opened to scores of $50.00 and up
|
|
// (second character a digit, or third character 5 or more). The scores
|
|
// are right-aligned text, so a character compare orders them; a tie goes
|
|
// above the older entry. The new entry is opened with a blank name.
|
|
static uint8_t highScoreInsert(const uint8_t *score) {
|
|
uint8_t slot;
|
|
uint8_t k;
|
|
|
|
if (score[1] < '0' && score[2] < '5') {
|
|
return ST_HS_ENTRIES;
|
|
}
|
|
for (slot = 0u; slot < ST_HS_ENTRIES; slot++) {
|
|
const uint8_t *old = gHighScores[slot].score;
|
|
for (k = 0u; k < ST_NUMBER_CHARS; k++) {
|
|
if (score[k] != old[k]) {
|
|
break;
|
|
}
|
|
}
|
|
if (k == ST_NUMBER_CHARS || score[k] > old[k]) {
|
|
break;
|
|
}
|
|
}
|
|
if (slot == ST_HS_ENTRIES) {
|
|
return ST_HS_ENTRIES;
|
|
}
|
|
for (k = (uint8_t)(ST_HS_ENTRIES - 1u); k > slot; k--) {
|
|
gHighScores[k] = gHighScores[k - 1u];
|
|
}
|
|
memcpy(gHighScores[slot].score, score, ST_NUMBER_CHARS + 1u);
|
|
memset(gHighScores[slot].name, ' ', ST_HS_NAME_CHARS);
|
|
gHighScores[slot].name[ST_HS_NAME_CHARS] = 0u;
|
|
gHighScores[slot].shift = 0u;
|
|
return slot;
|
|
}
|
|
|
|
|
|
// $23E5 -> $9948 at boot: the table from disk, when there is one. The
|
|
// image is the C64's own: per entry score[7], 0, name[15], shift.
|
|
static void highScoreLoad(void) {
|
|
uint8_t image[ST_HS_ENTRIES * ST_HS_ENTRY_BYTES];
|
|
uint8_t i;
|
|
|
|
if (jlSaveRead(ST_HS_SAVE_NAME, image, sizeof(image)) != sizeof(image)) {
|
|
return;
|
|
}
|
|
for (i = 0u; i < ST_HS_ENTRIES; i++) {
|
|
const uint8_t *e = &image[i * ST_HS_ENTRY_BYTES];
|
|
if (e[ST_NUMBER_CHARS] != 0u) {
|
|
ST_LOG("spacetaxi: ! %s is not a score table", ST_HS_SAVE_NAME);
|
|
return;
|
|
}
|
|
}
|
|
for (i = 0u; i < ST_HS_ENTRIES; i++) {
|
|
const uint8_t *e = &image[i * ST_HS_ENTRY_BYTES];
|
|
memcpy(gHighScores[i].score, e, ST_NUMBER_CHARS);
|
|
gHighScores[i].score[ST_NUMBER_CHARS] = 0u;
|
|
memcpy(gHighScores[i].name, &e[ST_NUMBER_CHARS + 1u], ST_HS_NAME_CHARS);
|
|
gHighScores[i].name[ST_HS_NAME_CHARS] = 0u;
|
|
gHighScores[i].shift = e[ST_HS_ENTRY_BYTES - 1u];
|
|
}
|
|
}
|
|
|
|
|
|
// $4E11 -> $9948 after an entry: the table back to disk.
|
|
static void highScoreSave(void) {
|
|
uint8_t image[ST_HS_ENTRIES * ST_HS_ENTRY_BYTES];
|
|
uint8_t i;
|
|
|
|
for (i = 0u; i < ST_HS_ENTRIES; i++) {
|
|
uint8_t *e = &image[i * ST_HS_ENTRY_BYTES];
|
|
memcpy(e, gHighScores[i].score, ST_NUMBER_CHARS);
|
|
e[ST_NUMBER_CHARS] = 0u;
|
|
memcpy(&e[ST_NUMBER_CHARS + 1u], gHighScores[i].name, ST_HS_NAME_CHARS);
|
|
e[ST_HS_ENTRY_BYTES - 1u] = gHighScores[i].shift;
|
|
}
|
|
if (!jlSaveWrite(ST_HS_SAVE_NAME, image, sizeof(image))) {
|
|
ST_LOG("spacetaxi: ! could not save %s", ST_HS_SAVE_NAME);
|
|
}
|
|
}
|
|
|
|
|
|
// $4D52 -- after a player's GAME OVER: their score as "dddd.dd" text, and
|
|
// if it ranks, the $4E35 screen: red border, grey field, the cabbie's
|
|
// number in the greeting, the score at the top, song 6, and a blinking
|
|
// cursor where the name goes. Returns false when the score did not rank
|
|
// and the game moves straight on.
|
|
static bool nameEntryStart(void) {
|
|
uint8_t score[ST_NUMBER_CHARS + 1u];
|
|
uint8_t greet[sizeof(kTextCongrats)];
|
|
uint8_t k;
|
|
|
|
for (k = 0u; k < ST_NUMBER_CHARS; k++) {
|
|
uint8_t ch = gSim.scoreBackup[gSim.player][k];
|
|
if (ch == ST_CHAR_BLANK) {
|
|
score[k] = ' ';
|
|
} else {
|
|
uint8_t d = (uint8_t)(ch - ST_CHAR_DIGIT_BASE); // $4345
|
|
score[k] = (uint8_t)('0' + ((d == 10u) ? 0u : d));
|
|
}
|
|
}
|
|
score[ST_HS_POINT_AT] = '.';
|
|
score[ST_NUMBER_CHARS] = 0u;
|
|
gGame.hsSlot = highScoreInsert(score);
|
|
if (gGame.hsSlot == ST_HS_ENTRIES) {
|
|
return false;
|
|
}
|
|
blankScreen(&gSim, 2u, 15u);
|
|
while (jlInputGetChar() >= 0) { // $0277 = 0: nothing typed earlier counts
|
|
}
|
|
memcpy(greet, kTextCongrats, sizeof(greet));
|
|
greet[ST_HS_GREET_DIGIT_AT] = (uint8_t)('1' + gSim.player);
|
|
stSimDrawText(&gSim, 7u, 3u, greet, 0u);
|
|
stSimDrawText(&gSim, 5u, 5u, kTextRanks, 0u);
|
|
stSimDrawText(&gSim, 4u, 7u, kTextTopTen, 0u);
|
|
stSimDrawText(&gSim, 9u, 9u, kTextEnterName, 0u);
|
|
stSimDrawText(&gSim, 16u, 0u, score, 6u);
|
|
stSimPutChar(&gSim, 15u, 0u, '$');
|
|
stSimPutColor(&gSim, 15u, 0u, 6u);
|
|
stAudioMusic(ST_SONG_NAME_ENTRY, false); // $4EB6
|
|
gGame.hsLen = 0u;
|
|
gGame.hsCursor = ST_HS_CURSOR;
|
|
gGame.hsBlinkTicks = 0u;
|
|
gGame.state = ST_STATE_NAME_ENTRY;
|
|
return true;
|
|
}
|
|
|
|
|
|
// $4ED8 -- the entry loop: the cursor flips between "%" and blank, then
|
|
// the keyboard: letters, space, ".", "!" and "," go in (fifteen at most),
|
|
// DEL takes one back, RETURN keeps the name (a "*" when none was typed),
|
|
// stamps the shift, stops the tune and saves the table.
|
|
static void runNameEntry(uint8_t ticks) {
|
|
StHighScoreT *e = &gHighScores[gGame.hsSlot];
|
|
uint8_t col = (uint8_t)(ST_HS_NAME_COL + gGame.hsLen);
|
|
int c;
|
|
|
|
gGame.hsBlinkTicks = (uint8_t)(gGame.hsBlinkTicks + ticks);
|
|
if (gGame.hsBlinkTicks >= ST_HS_BLINK_TICKS) {
|
|
gGame.hsBlinkTicks = 0u;
|
|
gGame.hsCursor ^= ST_HS_CURSOR ^ ' ';
|
|
stSimPutChar(&gSim, col, ST_HS_NAME_ROW, gGame.hsCursor);
|
|
stSimPutColor(&gSim, col, ST_HS_NAME_ROW, 6u);
|
|
}
|
|
while ((c = jlInputGetChar()) >= 0) {
|
|
if (c >= 'a' && c <= 'z') {
|
|
c = c - 'a' + 'A';
|
|
}
|
|
if (c == JL_CHAR_RETURN) {
|
|
stSimPutChar(&gSim, col, ST_HS_NAME_ROW, ' ');
|
|
if (gGame.hsLen == 0u) {
|
|
e->name[0] = '*';
|
|
}
|
|
e->shift = gGame.variation;
|
|
jlMusicStop(); // $44E1
|
|
highScoreSave(); // $4E11
|
|
gameOverFinish();
|
|
return;
|
|
}
|
|
if (c == JL_CHAR_BACKSPACE || c == JL_CHAR_DELETE) {
|
|
if (gGame.hsLen != 0u) {
|
|
stSimPutChar(&gSim, col, ST_HS_NAME_ROW, ' ');
|
|
gGame.hsLen--;
|
|
col--;
|
|
e->name[gGame.hsLen] = ' ';
|
|
}
|
|
continue;
|
|
}
|
|
if (c != ' ' && c != '.' && c != '!' && c != ',' && (c < 'A' || c > 'Z')) {
|
|
continue;
|
|
}
|
|
if (gGame.hsLen == ST_HS_NAME_CHARS) {
|
|
continue;
|
|
}
|
|
e->name[gGame.hsLen] = (uint8_t)c;
|
|
stSimPutChar(&gSim, col, ST_HS_NAME_ROW, (uint8_t)c);
|
|
stSimPutColor(&gSim, col, ST_HS_NAME_ROW, 6u);
|
|
gGame.hsLen++;
|
|
col++;
|
|
}
|
|
}
|
|
|
|
|
|
// $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.gameOverTune = false;
|
|
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_FILES) {
|
|
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;
|
|
}
|
|
stRenderLevelCels(gStage, &gLevel);
|
|
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);
|
|
}
|
|
// $5022/$5167: the 24-hour shifts play screen Y after A..X, and only
|
|
// then is the shift over -- which is also what lights the "finished"
|
|
// marker in the screens-completed field (levelState reaches 25).
|
|
if (sim->levelState == ST_LEVEL_COUNT) {
|
|
return ST_SCREEN_Y;
|
|
}
|
|
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)) {
|
|
jlMusicStop(); // $4709
|
|
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;
|
|
serviceJingle();
|
|
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) {
|
|
if (serviceJingle()) {
|
|
return; // $70D4 bonus cab: the tune blocks
|
|
}
|
|
continue;
|
|
}
|
|
if (gGame.demo) {
|
|
runDemoEnd();
|
|
return;
|
|
}
|
|
switch (r) {
|
|
case ST_TICK_LEVEL_EXIT:
|
|
startNewScreen();
|
|
return;
|
|
case ST_TICK_LIFE_LOST:
|
|
stSimRespawn(&gSim);
|
|
stRenderSceneChanged(&gSim);
|
|
if (serviceJingle()) {
|
|
return; // $6906 on the new cab
|
|
}
|
|
break;
|
|
case ST_TICK_PLAYER_OUT:
|
|
gameOverEnter();
|
|
return;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Start a tune the sim asked for and park the loop in ST_STATE_JINGLE
|
|
// until it ends. Returns true when a tune was started, so callers can
|
|
// stop ticking the sim for this frame -- the C64's $44CF blocks there.
|
|
static bool serviceJingle(void) {
|
|
uint8_t song;
|
|
|
|
if (gSim.jingleRequest == 0u) {
|
|
return false;
|
|
}
|
|
song = (uint8_t)(gSim.jingleRequest - 1u);
|
|
gSim.jingleRequest = 0u;
|
|
stAudioJingle(song);
|
|
if (!stAudioJingleActive()) {
|
|
return false; // no audio: nothing to wait for
|
|
}
|
|
gGame.afterJingle = gGame.state;
|
|
gGame.state = ST_STATE_JINGLE;
|
|
return true;
|
|
}
|
|
|
|
|
|
// $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) {
|
|
highScoreDraw();
|
|
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, 12u, 15u); // $558E: grey border, light-grey field
|
|
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, 0u, 0u);
|
|
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, 0u, 0u);
|
|
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);
|
|
// After the scene change, which turns it back off.
|
|
stRenderIntroStars();
|
|
// $459C: the intro's tune plays under the fly-in, not blocking; the
|
|
// C64 leaves it to the IRQ player and stops it at $4709.
|
|
stAudioMusic(ST_SONG_INTRO, true);
|
|
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;
|
|
|
|
// The codegen arena holds every compiled cel, the baked ones too: the
|
|
// 37 cab/exhaust/passenger cels plus a level's hook cels (level I has
|
|
// 30). On the IIgs that is ~230 KB of 65816 code in 64 KB banks, and
|
|
// the arena grants as many banks as the machine has (a 1 MB machine
|
|
// gets one; whatever does not fit draws interpreted). The 68k ports'
|
|
// cels are far larger (9-13 KB each) and their JIT is fast, so they
|
|
// keep a modest arena and compile at level entry.
|
|
#if defined(JOEYLIB_PLATFORM_IIGS) || defined(JOEYLIB_PLATFORM_DOS)
|
|
config.codegenBytes = 256UL * 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;
|
|
}
|
|
gStage = stage;
|
|
memset(&gGame, 0, sizeof(gGame));
|
|
memset(&gSim, 0, sizeof(gSim));
|
|
stRenderInit(stage);
|
|
stRenderLoading(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);
|
|
highScoreLoad(); // $23E5
|
|
enterTitle();
|
|
gGame.lastFrame = (uint8_t)jlFrameCount();
|
|
|
|
for (;;) {
|
|
uint8_t ticks;
|
|
|
|
jlInputPoll();
|
|
// $4FCB runStopWatcher, on the game tick, the intro and the menus:
|
|
// SHIFT held freezes everything with the sound gated off and any
|
|
// tune stopped (the intro's restarts on release); RUN/STOP -- ESC
|
|
// here -- abandons the game for the title, and on the title it
|
|
// leaves the program (the C64 had nowhere else to go).
|
|
if (gGame.state != ST_STATE_JINGLE && gGame.state != ST_STATE_GAME_OVER && gGame.state != ST_STATE_NAME_ENTRY) {
|
|
if (jlKeyDown(KEY_LSHIFT) || jlKeyDown(KEY_RSHIFT)) {
|
|
if (!gGame.paused) {
|
|
gGame.paused = true;
|
|
stAudioSilence();
|
|
stAudioNoise(false);
|
|
jlMusicStop();
|
|
}
|
|
stRenderFrame(stage, &gSim);
|
|
jlAudioFrameTick();
|
|
continue;
|
|
}
|
|
if (gGame.paused) {
|
|
gGame.paused = false;
|
|
gGame.lastFrame = (uint8_t)jlFrameCount(); // the held time is not owed
|
|
gGame.paceAcc = 0u;
|
|
if (gGame.state == ST_STATE_INTRO) {
|
|
stAudioMusic(ST_SONG_INTRO, true); // $46B5
|
|
}
|
|
}
|
|
if (jlKeyPressed(KEY_ESCAPE)) {
|
|
if (gGame.state == ST_STATE_TITLE) {
|
|
break;
|
|
}
|
|
jlMusicStop(); // $5000
|
|
gGame.demo = false;
|
|
enterTitle();
|
|
}
|
|
}
|
|
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_JINGLE:
|
|
// The C64 spins in $44D5 until the tune ends. A joystick
|
|
// press ends the attract demo here too ($4FE2 stops the
|
|
// music on input).
|
|
if (gGame.demo && (joyMask() & 0x1Fu) != 0u) {
|
|
jlMusicStop();
|
|
runDemoEnd();
|
|
} else if (!stAudioJingleActive()) {
|
|
gGame.state = gGame.afterJingle;
|
|
}
|
|
break;
|
|
case ST_STATE_GAME_OVER:
|
|
if (gGame.waitTicks > ticks) {
|
|
gGame.waitTicks = (uint16_t)(gGame.waitTicks - ticks);
|
|
} else if (!gGame.gameOverTune) {
|
|
// $5C24: after the GAME OVER! wait, song 5 plays out.
|
|
gGame.gameOverTune = true;
|
|
stAudioJingle(ST_SONG_GAME_OVER);
|
|
} else if (stAudioJingleActive()) {
|
|
// still sounding
|
|
} else if (!nameEntryStart()) {
|
|
gameOverFinish();
|
|
}
|
|
break;
|
|
case ST_STATE_NAME_ENTRY:
|
|
runNameEntry(ticks);
|
|
break;
|
|
default:
|
|
enterTitle();
|
|
break;
|
|
}
|
|
stRenderFrame(stage, &gSim);
|
|
jlAudioFrameTick();
|
|
}
|
|
|
|
stAudioShutdown();
|
|
stRenderShutdown();
|
|
jlShutdown();
|
|
return 0;
|
|
}
|