318 lines
12 KiB
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;
|
|
}
|
|
}
|