// Space Taxi -- passenger AI (dynamic fare model). // // The C64 game has NO static per-level fare list (verified: the prior // port's "circular A->B->..->A" fares were fabricated). Both the SPAWN // pad and the DELIVERY destination are chosen at RUNTIME by RNG: // // * padHoverSetup ($6537) picks a random free pad in [1..padCount] for // the passenger to appear on, and again for where they want to go. // * When all pads are consumed (single-pad screens, notably level A), // the destination becomes the "UP PLEASE!" sentinel ($0B, $6CB8): // the fare is delivered by flying UP through the top-wall // transporter opening. // * "PAD n PLEASE" is drawn from the runtime destination index+'0' // ($671E: gActiveSpriteIdx + $30); pads are numeric, not lettered. // // This port keeps ONE active fare at a time (spawn -> board -> deliver -> // spawn next), a faithful reduction of the C64's multi-slot queue. The // RNG here is a small port-side LCG (the exact C64 RNG sequence isn't // observable and depends on live seed state); it only needs to pick a // plausible free pad each fare. // // Scoring is flat-rate per the C64 original ($43B9 BCD blob added at // success-stage 6 / $6742): a delivered fare adds ST_FARE_SCORE. #include #include "spacetaxi.h" #define ST_PASSENGER_W_PX 16 #define ST_PASSENGER_H_PX 16 #define ST_PASSENGER_H_TILES (ST_PASSENGER_H_PX / ST_TILE_PIXELS) #define ST_WALK_FRAMES 4u #define ST_WALK_TICKS_PER_CEL 6u // Verified via emulator trace of $4354 (BCD-add) with the $43B9 blob: // a basic delivered fare adds '5' at the ones digit of the DDDD.DD HUD // score. See stuff/spacetaxi/trace.py. #define ST_FARE_SCORE 5u // Spawn cadence (C64 idle ticker $660B): while no fare is active, each // frame rolls rand%100 and spawns on roll < 3, else counts a 100-frame // cap down to a forced spawn -- a fresh fare appears within ~1-2 seconds. #define ST_SPAWN_ROLL_THRESH 3u #define ST_SPAWN_CAP_FRAMES 100 static uint32_t gRng; static uint8_t gWalkTick; // Frames until the next fare is forced to spawn (mirrors gDeathStage0Rng). static int16_t gSpawnCountdown; // Pad deliveries completed on the current screen. Destination pads are // "used up" as fares are delivered (C64 gSpriteSlots/gFareSlotCount); once // only the last pad would remain free, the next fare wants "UP PLEASE!" // (fly up to the next screen), which is what makes each screen finite. static uint8_t gDeliveredThisScreen; static void deliverFare(StGameT *game, StPassengerT *p); static void pickDestination(StGameT *game, StPassengerT *p); static void placeOnPad(StPassengerT *p, const StLevelT *level, uint8_t padIdx); static uint8_t rngPad(uint8_t n); static void spawnPassenger(StGameT *game); static void walkPassenger(StGameT *game, StPassengerT *p); static void deliverFare(StGameT *game, StPassengerT *p) { game->score += ST_FARE_SCORE; stAudioSfxDropoff(); p->active = false; gDeliveredThisScreen++; // The next fare appears after the spawn cadence (see stPassengerTick), // not instantly. gSpawnCountdown = ST_SPAWN_CAP_FRAMES; } // On boarding, choose where the fare wants to go. Destination pads get // used up as the screen's fares are delivered: once delivering would leave // no fresh pad (single-pad screens, or after padCount-1 deliveries), the // fare wants "UP PLEASE!" -- the player flies up to the next screen. static void pickDestination(StGameT *game, StPassengerT *p) { uint8_t pc = game->level.padCount; uint8_t d; if (pc <= 1u || gDeliveredThisScreen >= (uint8_t)(pc - 1u)) { p->destPad = ST_DEST_TRANSPORTER; return; } d = rngPad(pc); if (d == p->currentPad) { d = (uint8_t)((d + 1u) % pc); } p->destPad = d; } // Stand the passenger at the pad's real stand column (pad->standX, the // C64 slot byte6 = gPassenger1Col). pad->tileY is the contact-surface // row; the fare's feet sit on it, so the sprite top-left is two tiles up. static void placeOnPad(StPassengerT *p, const StLevelT *level, uint8_t padIdx) { const StPadT *pad = &level->pads[padIdx]; p->currentPad = padIdx; p->x = (int16_t)pad->standX; p->y = (int16_t)((pad->tileY - ST_PASSENGER_H_TILES) * ST_TILE_PIXELS); } // Small LCG -> uniform pad index in [0, n-1]. n==0 guarded to 0. static uint8_t rngPad(uint8_t n) { if (n == 0u) { return 0u; } gRng = gRng * 1103515245u + 12345u; return (uint8_t)(((gRng >> 16) & 0x7FFFu) % n); } static void spawnPassenger(StGameT *game) { StPassengerT *p = &game->passengers[0]; uint8_t padIdx; if (game->level.padCount == 0u) { return; } padIdx = rngPad(game->level.padCount); p->active = true; p->onboard = false; p->walkPhase = 0u; p->walkDir = 0u; p->destPad = 0u; placeOnPad(p, &game->level, padIdx); } // Idle back-and-forth walk across the pad while waiting to be picked up. // The C64 passenger AI wasn't fully traced; this gives each waiting fare // some motion instead of standing still. static void walkPassenger(StGameT *game, StPassengerT *p) { const StPadT *pad; int16_t padXMin; int16_t padXMax; p->walkPhase = (uint8_t)((p->walkPhase + 1u) % ST_WALK_FRAMES); if (p->currentPad >= game->level.padCount) { return; } pad = &game->level.pads[p->currentPad]; padXMin = (int16_t)(pad->tileX * ST_TILE_PIXELS); padXMax = (int16_t)((pad->tileX + pad->tileW) * ST_TILE_PIXELS - ST_PASSENGER_W_PX); if (padXMax <= padXMin) { return; // pad too narrow to walk on } if (p->walkDir == 0u) { p->x++; if (p->x >= padXMax) { p->x = padXMax; p->walkDir = 1u; } } else { p->x--; if (p->x <= padXMin) { p->x = padXMin; p->walkDir = 0u; } } } void stPassengerReset(StGameT *game) { uint8_t i; gWalkTick = 0u; gDeliveredThisScreen = 0u; gSpawnCountdown = ST_SPAWN_CAP_FRAMES; // Deterministic per-screen seed: varies the pad sequence level to // level without needing a wall-clock source. gRng = 0x2545F491u ^ ((uint32_t)game->levelIndex * 2654435761u); for (i = 0u; i < ST_MAX_PASSENGERS; i++) { game->passengers[i].active = false; } // The screen starts empty; the first fare appears after the spawn // cadence in stPassengerTick. } void stPassengerTick(StGameT *game) { StPassengerT *p = &game->passengers[0]; StTaxiT *t = &game->taxi; if (!p->active) { // Idle: a fresh fare appears after the C64 spawn cadence. Never // spawn mid-crash (the C64 gates the idle ticker on // gCollisionPhase == 0). if (t->crashTicks == 0u) { if (gSpawnCountdown > 0) { gSpawnCountdown--; } if (rngPad(100u) < ST_SPAWN_ROLL_THRESH || gSpawnCountdown <= 0) { spawnPassenger(game); } } return; } if (!p->onboard) { // Walk in place while waiting. if (++gWalkTick >= ST_WALK_TICKS_PER_CEL) { gWalkTick = 0u; walkPassenger(game, p); } // Board when the taxi has landed on their pad. if (t->landed && t->onPad == p->currentPad) { p->onboard = true; pickDestination(game, p); stAudioSfxPickup(); } } else if (p->destPad != ST_DEST_TRANSPORTER) { // Deliver by landing on the destination pad. (UP-PLEASE fares are // delivered on the transporter fly-up; see stPassengerTransporterExit.) if (t->landed && t->onPad < game->level.padCount && t->onPad == p->destPad) { deliverFare(game, p); } } } void stPassengerTransporterExit(StGameT *game) { StPassengerT *p = &game->passengers[0]; if (p->active && p->onboard && p->destPad == ST_DEST_TRANSPORTER) { game->score += ST_FARE_SCORE; stAudioSfxDropoff(); p->active = false; } }