joeylib2/examples/spacetaxi/spacetaxi.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;
}