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