joeylib2/examples/spacetaxi/stPassenger.c

318 lines
12 KiB
C

// 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 <stddef.h>
#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;
}
}