joeylib2/examples/spacetaxi/stPassenger.c

241 lines
8 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_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;
}
}