// 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_H_PX 16 #define ST_PASSENGER_H_TILES (ST_PASSENGER_H_PX / ST_TILE_PIXELS) // The waiting passenger's in-place wave (and every beam/walk step) // advances every 3 game ticks -- the C64's $715E == 3 reload. Since // stPassengerTick is now called once per fixed game tick (~30 Hz), // this is a plain tick counter, in lockstep with the physics. #define ST_WAVE_STEP_TICKS 3u // 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 gWaveTicks; // game ticks since the last wave/step advance // 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 bool arrivedAtTarget(const StPassengerT *p); 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 walkStride(StPassengerT *p); // Walk arrival test. The C64 aligns the stride with AND #$FE and // compares ((x ^ target) & ~1) == 0; the port's +/-2 window is the // same idea, robust to an odd captured taxi X. static bool arrivedAtTarget(const StPassengerT *p) { int16_t d = (int16_t)(p->x - p->targetX); return d >= -2 && d <= 2; } 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); } // One +/-2 px walk stride ($6D66 rightward, $6D68 leftward), with // the stride cel alternation on waitPhase's low bit. static void walkStride(StPassengerT *p) { if (p->targetX > p->x) { p->x = (int16_t)(p->x + 2); } else if (p->targetX < p->x) { p->x = (int16_t)(p->x - 2); } p->waitPhase ^= 1u; } 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->phase = ST_PASS_BEAM_IN; p->waitPhase = 0u; p->destPad = 0u; placeOnPad(p, &game->level, padIdx); } void stPassengerReset(StGameT *game) { uint8_t i; gWaveTicks = 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; } // One shared step clock (the C64's 3-game-tick cadence, $715E == 3) // drives every phase: beams, wave, and walk strides. Called once // per fixed game tick, so this is a plain tick count. { bool stepDue = (++gWaveTicks >= ST_WAVE_STEP_TICKS); if (stepDue) { gWaveTicks = 0u; } switch (p->phase) { case ST_PASS_BEAM_IN: // Materialize at standX: cels $CB..$C7 (5 steps). if (stepDue && ++p->waitPhase >= 5u) { p->phase = ST_PASS_WAIT; p->waitPhase = 0u; } break; case ST_PASS_WAIT: // Stand still and wave through the $66D6 cycle. if (stepDue) { p->waitPhase = (uint8_t)((p->waitPhase + 1u) & 3u); } if (t->landed && t->onPad == p->currentPad) { // $6811 captures the taxi X ONCE at landing; the // walk aims there even if the cab lifts off. p->targetX = (int16_t)(t->x >> ST_SUBPIXEL_SHIFT); p->phase = ST_PASS_WALK_TO_CAB; p->waitPhase = 0u; } break; case ST_PASS_WALK_TO_CAB: if (stepDue) { walkStride(p); if (arrivedAtTarget(p)) { if (t->landed && t->onPad == p->currentPad) { p->phase = ST_PASS_BOARD_OUT; p->waitPhase = 0u; } else { // Cab left early: walk home and wait again. // (Not byte-traced -- the C64 cab cannot // normally leave mid-walk; this just keeps // the state machine sane if ours does.) p->targetX = (int16_t)game->level.pads[p->currentPad].standX; p->phase = ST_PASS_WALK_TO_PAD; p->waitPhase = 0u; } } } break; case ST_PASS_BOARD_OUT: // Shrink into the cab: cels $C7..$CB (5 steps). if (stepDue && ++p->waitPhase >= 5u) { p->onboard = true; p->phase = ST_PASS_RIDING; p->waitPhase = 0u; pickDestination(game, p); stAudioSfxPickup(); } break; case ST_PASS_RIDING: // Deliver by landing on the destination pad. (UP-PLEASE // fares deliver on the transporter fly-up instead; see // stPassengerTransporterExit.) if (p->destPad != ST_DEST_TRANSPORTER && t->landed && t->onPad < game->level.padCount && t->onPad == p->destPad) { p->onboard = false; p->phase = ST_PASS_DROP_IN; p->waitPhase = 0u; p->x = (int16_t)(t->x >> ST_SUBPIXEL_SHIFT); p->targetX = (int16_t)game->level.pads[p->destPad].standX; p->currentPad = p->destPad; p->y = (int16_t)((game->level.pads[p->destPad].tileY - ST_PASSENGER_H_TILES) * ST_TILE_PIXELS); } break; case ST_PASS_DROP_IN: // Materialize beside the cab: cels $CB..$C7. if (stepDue && ++p->waitPhase >= 5u) { p->phase = ST_PASS_WALK_TO_PAD; p->waitPhase = 0u; } break; case ST_PASS_WALK_TO_PAD: if (stepDue) { walkStride(p); if (arrivedAtTarget(p)) { p->phase = ST_PASS_DROP_OUT; p->waitPhase = 0u; } } break; case ST_PASS_DROP_OUT: // Shrink out at standX -- fare complete. if (stepDue && ++p->waitPhase >= 5u) { deliverFare(game, p); } break; } } } 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; } }