joeylib2/examples/spacetaxi/stFare.c

451 lines
14 KiB
C

// Space Taxi -- the passenger / fare state machine ($65C1 dispatch on
// $7163, the $67E3 post-tick gate and the pad reservation table).
//
// One passenger at a time: they beam in on a free pad, wave, walk to a
// cab parked on their pad, shrink into it, ride, and reverse the whole
// sequence at the destination. Every step runs on the shared 3-tick
// decay timer. Pads get reserved as fares use them; once every fare
// pad has been used the destination becomes "UP PLEASE" and the top
// hatch opens.
#include "stSim.h"
// Screen-code strings ($6C1E..$6C7D). All ASCII-compatible glyphs.
static const uint8_t kTextPadPlease[] = { 0x50, 0x41, 0x44, 0x20, 0x20, 0x20, 0x50, 0x4C, 0x45, 0x41, 0x53, 0x45, 0 }; // $6C1E "PAD PLEASE"
static const uint8_t kTextUpPlease[] = { 0x20, 0x55, 0x50, 0x20, 0x50, 0x4C, 0x45, 0x41, 0x53, 0x45, 0x21, 0x20, 0 }; // $6C2B " UP PLEASE! "
static const uint8_t kTextBlank[] = { 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0 }; // $6C38
static const uint8_t kTextHeyTaxi[] = { 0x20, 0x48, 0x45, 0x59, 0x2C, 0x20, 0x54, 0x41, 0x58, 0x49, 0x21, 0x20, 0 }; // $6C45 " HEY, TAXI! "
static const uint8_t kTextThanks[] = { 0x20, 0x20, 0x20, 0x54, 0x48, 0x41, 0x4E, 0x4B, 0x53, 0x20, 0x20, 0x20, 0 }; // $6C5F " THANKS "
// Speech cue strings fed to the sample player one character at a time.
static const uint8_t kSpeechHeyTaxi[] = { 0x48, 0x54, 0 }; // $6C6C "HT"
static const uint8_t kSpeechUp[] = { 0x55, 0x3F, 0 }; // $6C6F "U?"
static const uint8_t kSpeechPad[] = { 0x50, 0x58, 0x3F, 0 }; // $6C72 "PX?" (X = digit)
static const uint8_t kSpeechThanks[] = { 0x21, 0 }; // $6C76 "!"
static const uint8_t kSpeechHey[] = { 0x48, 0 }; // $6C78 "H"
// Waiting wave cycle ($66D6) and the base-fare blob ($43B9 = +5.00).
static const uint8_t kWaveCels[4] = { 0xC6, 0xC7, 0xD9, 0xC7 };
static const uint8_t kFareBlob[7] = { 0x66, 0x66, 0x66, 0x6F, 0x77, 0x74, 0x74 };
static void beamIn(StSimT *sim);
static bool decayStep(StSimT *sim);
static void idleTicker(StSimT *sim);
static void lightOnAndLatch(StSimT *sim);
static void newFare(StSimT *sim);
static void stageBoard(StSimT *sim);
static void stageCleanup(StSimT *sim);
static void stageDropIn(StSimT *sim);
static void walkToTarget(StSimT *sim);
// $665F -- the passenger grows in ($CB down to $C7); a crash during
// it cancels the fare.
static void beamIn(StSimT *sim) {
if (!decayStep(sim)) {
return;
}
sim->spr[1].ptr--;
if (sim->spr[1].ptr != 0xC7u) {
return;
}
if (sim->collisionPhase != 0u) {
sim->stage = ST_STAGE_LEAVE;
sim->spr[1].ptr = 0xC7u;
sim->spriteSlots[sim->activeSpriteIdx] = 0u;
sim->activeSpriteIdx = 0u;
sim->fareSlotCount--;
return;
}
sim->stage++;
stSimDrawText(sim, 14u, 24u, kTextHeyTaxi, 1u);
stFareBoardingSound(sim, kSpeechHeyTaxi);
// $43D1: the meter starts at $10.00..$19.00.
{
uint8_t k;
static const uint8_t kTemplate[7] = { 0x66, 0x66, 0x66, 0x74, 0x77, 0x74, 0x74 };
for (k = 0u; k < ST_NUMBER_CHARS; k++) {
stSimPutChar(sim, (uint8_t)(32u + k), 24u, kTemplate[k]);
}
stSimPutChar(sim, 35u, 24u, (uint8_t)(ST_CHAR_DIGIT_BASE + stSimRng(sim, 10u)));
stSimPutChar(sim, 34u, 24u, (uint8_t)(ST_CHAR_DIGIT_BASE + 1u));
}
}
// The shared 3-tick cadence: true on the tick a step is due.
static bool decayStep(StSimT *sim) {
sim->decayTimer--;
if (sim->decayTimer != 0u) {
return false;
}
sim->decayTimer = sim->decayReload;
return true;
}
// $660B -- a new fare turns up on a 3% roll per tick or when the
// 100-tick timer runs out, never mid-crash.
static void idleTicker(StSimT *sim) {
if (stSimRng(sim, 100u) >= 3u) {
sim->stage0Rng--;
if (sim->stage0Rng != 0u) {
return;
}
}
if (sim->collisionPhase != 0u) {
return;
}
newFare(sim);
}
// $6808 -- the pad lights up and stays "busy".
static void lightOnAndLatch(StSimT *sim) {
sim->deathInProgress = 1u;
stSimPadLight(sim, true);
}
// $6620 -- pick a free pad, stand the passenger there, start beaming.
static void newFare(StSimT *sim) {
uint8_t x;
const StPadT *pad;
sim->stage0Rng = 0x64u;
x = stFareChooseDestination(sim);
// $6628: X-1 selects the slot; a garbage X (all pads used) reads
// past the table on the C64 -- clamp to the last pad here.
if (x == 0u || x > ST_MAX_PADS) {
x = ST_MAX_PADS;
}
pad = &sim->pads[x - 1u];
sim->spr[1].msb = pad->passMsb;
sim->spr[1].col = pad->passCol;
sim->spr[1].row = pad->row;
sim->spr[1].enable = 1u;
sim->stage = ST_STAGE_BEAM_IN;
sim->decayTimer = sim->decayReload;
sim->spr[1].ptr = 0xCBu;
stAudioSpeech((uint8_t)(stSimRng(sim, 3u) + 2u));
}
// $66DD -- the passenger shrinks into the cab, then picks where to go.
static void stageBoard(StSimT *sim) {
if (!decayStep(sim)) {
return;
}
sim->spr[1].ptr++;
if (sim->spr[1].ptr != 0xCCu) {
return;
}
sim->spr[1].enable = 0u;
sim->stage++;
sim->activeDyingSlot = sim->activePad;
(void)stFareChooseDestination(sim);
if (sim->activeSpriteIdx == 0x0Bu) {
return;
}
stSimDrawText(sim, 14u, 24u, kTextPadPlease, 1u);
stSimPutChar(sim, 18u, 24u, (uint8_t)(sim->activeSpriteIdx + 0x30u));
{
uint8_t speech[4];
speech[0] = kSpeechPad[0];
speech[1] = (uint8_t)(sim->activeSpriteIdx + 0x30u);
speech[2] = kSpeechPad[2];
speech[3] = 0u;
stFareBoardingSound(sim, speech);
}
}
// $67C6 -- back to idle.
static void stageCleanup(StSimT *sim) {
sim->deathInProgress = 0u;
sim->stage = ST_STAGE_IDLE;
stSimPadLight(sim, false);
stSimDrawText(sim, 14u, 24u, kTextBlank, 1u);
}
// $6742 -- the passenger materialises beside the cab; the fare pays
// out: $5.00 plus whatever is left on the meter (twice the base when
// no pads are reserved).
static void stageDropIn(StSimT *sim) {
if (!decayStep(sim)) {
return;
}
sim->spr[1].ptr--;
if (sim->spr[1].ptr != 0xC7u) {
return;
}
sim->stage++;
stSimDrawText(sim, 14u, 24u, kTextThanks, 1u);
stFareBoardingSound(sim, kSpeechThanks);
stSimBcdAdd(sim, ST_CELL_SCORE, kFareBlob);
stSimBcdAdd(sim, ST_CELL_SCORE, &sim->screen[ST_CELL_FARE]);
{
static const uint8_t kTemplate[7] = { 0x66, 0x66, 0x66, 0x74, 0x77, 0x74, 0x74 };
uint8_t k;
for (k = 0u; k < ST_NUMBER_CHARS; k++) {
stSimPutChar(sim, (uint8_t)(32u + k), 24u, kTemplate[k]);
}
}
if (sim->fareSlotCount != 0u) {
return;
}
stSimBcdAdd(sim, ST_CELL_SCORE, kFareBlob);
}
// $67A6 -- the passenger shrinks away at the stand (also intro stage 4).
void stFareLeaveStep(StSimT *sim) {
if (!decayStep(sim)) {
return;
}
sim->spr[1].ptr++;
if (sim->spr[1].ptr != 0xCCu) {
return;
}
sim->spr[1].enable = 0u;
sim->stage++;
}
// $66B7 -- waving on the pad (also the title's sparkle cycle).
void stFareWaveStep(StSimT *sim) {
if (!decayStep(sim)) {
return;
}
sim->waveIdx = (uint8_t)((sim->waveIdx + 1u) & 3u);
sim->spr[1].ptr = kWaveCels[sim->waveIdx];
}
// $6D0D -- one two-pixel stride toward the hover target, cel
// alternating each step (also the title's walk-in).
void stFareWalkStep(StSimT *sim) {
int16_t passX;
int16_t targetX;
uint16_t sum;
sim->walkParity ^= 1u;
passX = (int16_t)(((int16_t)sim->spr[1].msb << 8) | sim->spr[1].col);
targetX = (int16_t)(((int16_t)sim->hoverXFrac << 8) | sim->hoverXCol);
if ((int16_t)(passX - targetX) < 0) {
// $6D30: walk right, cels $C2/$C3.
sim->spr[1].ptr = (uint8_t)(0xC2u + sim->walkParity);
sum = (uint16_t)((uint16_t)sim->spr[1].col + 2u);
sim->spr[1].col = (uint8_t)sum;
sim->spr[1].msb = (uint8_t)(sim->spr[1].msb + (uint8_t)(sum >> 8));
return;
}
// $6D4B: walk left, cels $C4/$C5.
sim->spr[1].ptr = (uint8_t)(0xC4u + sim->walkParity);
sum = (uint16_t)((uint16_t)sim->spr[1].col + 0xFEu);
sim->spr[1].col = (uint8_t)sum;
sim->spr[1].msb = (uint8_t)(sim->spr[1].msb + 0xFFu + (uint8_t)(sum >> 8));
}
// $6CE8 -- stages 3 and 7: walk until the column matches (even
// pixels), then stand and advance.
static void walkToTarget(StSimT *sim) {
if (!decayStep(sim)) {
return;
}
if ((sim->spr[1].col & 0xFEu) == (sim->hoverXCol & 0xFEu)) {
sim->spr[1].ptr = 0xC7u;
sim->stage++;
return;
}
stFareWalkStep(sim);
}
// ---------------------------------------------------------------------------
// Public
// ---------------------------------------------------------------------------
// $6C7F -- the passenger speaks: one sample per character.
void stFareBoardingSound(StSimT *sim, const uint8_t *speech) {
(void)sim;
stAudioSilence();
stAudioNoise(false);
while (*speech != 0u) {
stAudioSpeech(*speech);
speech++;
}
}
// $6537 -- reserve a free fare pad by RNG (linear probe on collision),
// or "UP PLEASE" once every fare pad is taken. Returns the pad number.
uint8_t stFareChooseDestination(StSimT *sim) {
uint8_t special = sim->level->specialPad;
uint8_t x;
if (sim->fareSlotCount == special) {
stFareUpPlease(sim);
return special;
}
x = stSimRng(sim, special);
while (sim->spriteSlots[x] != 0u) {
if (x == special) {
x = 1u;
} else {
x++;
}
}
sim->fareSlotCount++;
sim->activeSpriteIdx = x;
sim->spriteSlots[x] = 1u;
return x;
}
// $6EF0 -- while the passenger walks (to or from the cab) the pad stays
// lit only while the two sprites touch.
void stFarePadLightingGate(StSimT *sim) {
if (sim->stage != ST_STAGE_WALK_TO_CAB && sim->stage != ST_STAGE_WALK_TO_PAD && sim->stage != ST_STAGE_LEAVE) {
return;
}
if (sim->activePad == 0u) {
return;
}
sim->deathInProgress = (uint8_t)(sim->spriteSpriteColl & 2u);
stSimPadLight(sim, sim->deathInProgress != 0u);
}
// $67E3 -- landed on the fare's pad: hand the passenger over.
void stFarePostTickGate(StSimT *sim) {
if (sim->activePad == 0u) {
return;
}
if (sim->activePad != sim->activeSpriteIdx) {
return;
}
switch (sim->stage) {
case ST_STAGE_IDLE:
case ST_STAGE_BEAM_IN:
case ST_STAGE_WALK_TO_CAB:
case ST_STAGE_WALK_TO_PAD:
return;
case ST_STAGE_WAIT:
// $6811: remember where the cab parked.
sim->stage++;
sim->hoverXFrac = sim->spr[0].msb;
sim->hoverXCol = sim->spr[0].col;
lightOnAndLatch(sim);
return;
case ST_STAGE_RIDING: {
const StPadT *pad;
if (sim->bobTimer != 0u) {
return;
}
sim->stage++;
sim->spr[1].enable = 1u;
sim->spr[1].msb = sim->spr[0].msb;
sim->spr[1].col = sim->spr[0].col;
sim->spr[1].row = sim->spr[0].row;
sim->spr[1].ptr = 0xCBu;
sim->decayTimer = sim->decayReload;
pad = &sim->pads[sim->activeSpriteIdx - 1u];
sim->hoverXFrac = pad->passMsb;
sim->hoverXCol = pad->passCol;
lightOnAndLatch(sim);
return;
}
default:
lightOnAndLatch(sim);
return;
}
}
// $69B4 -- the cab touched the passenger: he vanishes, the fare is
// lost and the meter is docked ten dollars.
void stFareSquashed(StSimT *sim) {
if (sim->stage >= ST_STAGE_LEAVE) {
return;
}
if (sim->stage < ST_STAGE_DROP_IN) {
sim->spriteSlots[sim->activeSpriteIdx] = 0u;
sim->activeSpriteIdx = 0u;
sim->fareSlotCount--;
stSimDrawText(sim, 14u, 24u, kTextBlank, 1u);
}
sim->stage = ST_STAGE_LEAVE;
stFareBoardingSound(sim, kSpeechHey);
// $43F3: a hundred dime decrements.
{
uint8_t k;
for (k = 0u; k < 100u; k++) {
(void)stSimDecrementNumber(sim, ST_CELL_SCORE, 5u);
}
// $43F3 counts down in the input-mask byte and leaves it zero.
sim->inputMask = 0u;
}
sim->spr[1].ptr = 0xC7u;
{
static const uint8_t kTemplate[7] = { 0x66, 0x66, 0x66, 0x74, 0x77, 0x74, 0x74 };
uint8_t k;
for (k = 0u; k < ST_NUMBER_CHARS; k++) {
stSimPutChar(sim, (uint8_t)(32u + k), 24u, kTemplate[k]);
}
}
}
// $65C1 -- run the current stage.
void stFareStageDispatch(StSimT *sim) {
switch (sim->stage) {
case ST_STAGE_IDLE:
idleTicker(sim);
break;
case ST_STAGE_BEAM_IN:
beamIn(sim);
break;
case ST_STAGE_WAIT:
stFareWaveStep(sim);
break;
case ST_STAGE_WALK_TO_CAB:
case ST_STAGE_WALK_TO_PAD:
walkToTarget(sim);
break;
case ST_STAGE_BOARD:
stageBoard(sim);
break;
case ST_STAGE_RIDING:
sim->deathInProgress = 0u;
stSimPadLight(sim, false);
break;
case ST_STAGE_DROP_IN:
stageDropIn(sim);
break;
case ST_STAGE_LEAVE:
stFareLeaveStep(sim);
break;
case ST_STAGE_CLEANUP:
stageCleanup(sim);
break;
default:
sim->stage = ST_STAGE_IDLE;
break;
}
}
// $6CB8 -- no fare pad left: the passenger wants to leave the screen,
// so the top hatch opens.
void stFareUpPlease(StSimT *sim) {
uint8_t k;
sim->activeSpriteIdx = 0x0Bu;
stSimDrawText(sim, 14u, 24u, kTextUpPlease, 1u);
for (k = 0u; k < 4u; k++) {
stSimPutChar(sim, (uint8_t)(18u + k), 0u, ST_CHAR_SPACE);
}
stFareBoardingSound(sim, kSpeechUp);
}