// 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); }