joeylib2/examples/spacetaxi/stPassenger.c

217 lines
7.2 KiB
C

// Space Taxi -- passenger AI.
//
// A passenger has three states:
// 1. Waiting on a pad (active, !onboard) - cycles walk animation.
// Boards when the taxi lands on their pad.
// 2. In the cab (active, onboard) - invisible; followed by
// the taxi until it touches the destination pad.
// 3. Done (!active) - waiting to be replaced
// by the next fare slot.
//
// When a passenger lands at their destination, the next fare in the
// level's fareCount is spawned. When the level's fares are exhausted,
// the level is done.
//
// Scoring is flat-rate per the C64 original ($43B9 BCD blob added at
// success-stage 6 / $6742): 500 points per delivered fare. The earlier
// "tip by patience" formula was a port-side invention.
#include <stddef.h>
#include "spacetaxi.h"
JOEYLIB_SEGMENT("STAXI")
#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
// against an all-blank HUD: the only HUD position modified is index 3
// (the ones-digit of the integer part in the C64's DDDD.DD layout),
// where digit '5' gets added. So a basic fare delivered = +5 score.
// See stuff/spacetaxi/trace.py and the run output for proof.
//
// Two other blobs exist but their callers haven't been traced:
// $43C1 = +95 (called from $5D03 after a per-passenger counter
// decrement -- looks like a bonus event)
// $43C9 = +50 (called from $5C82 in the $5BBB-dispatched anim --
// possibly a "fare-on-takeoff" or "tip" bonus)
#define ST_FARE_SCORE 5u
// Per-port passenger queue cursor. Reset by stPassengerReset whenever a
// new level (or restart-after-game-over) needs to begin from fare 0.
static uint8_t gNextFareIdx;
static uint8_t gWalkTick;
static uint8_t effectiveFareTarget(const StGameT *game);
static void spawnNextFare(StGameT *game);
// Apply the title-screen fare-count selector ($7213 in the C64) as an
// upper cap on this level's fare count. The level data sets the
// maximum (e.g. 8 for a long delivery chain); player picks 1..4 on
// the title to control session length. The lesser of the two wins.
static uint8_t effectiveFareTarget(const StGameT *game) {
uint8_t levelMax = game->level.fareCount;
uint8_t sel = game->fareTarget;
if (sel == 0u) {
return levelMax;
}
return (levelMax < sel) ? levelMax : sel;
}
static void spawnNextFare(StGameT *game) {
const StFareT *fare;
const StPadT *pad;
StPassengerT *slot;
uint8_t i;
if (gNextFareIdx >= effectiveFareTarget(game)) {
return;
}
// Find an idle slot.
slot = NULL;
for (i = 0u; i < ST_MAX_PASSENGERS; i++) {
if (!game->passengers[i].active) {
slot = &game->passengers[i];
break;
}
}
if (slot == NULL) {
return;
}
fare = &game->level.fares[gNextFareIdx++];
if (fare->spawnPad >= game->level.padCount) {
return;
}
pad = &game->level.pads[fare->spawnPad];
slot->active = true;
slot->onboard = false;
slot->currentPad = fare->spawnPad;
slot->destPad = fare->destPad;
slot->walkPhase = 0u;
slot->walkDir = 0u;
slot->x = (int16_t)(pad->tileX * ST_TILE_PIXELS);
// Pad's tileY is the landing-surface row (top of the pad block).
// Place the passenger so their feet sit on that surface --
// top-left Y is (tileY - passengerHeightInTiles) * tilePixels.
slot->y = (int16_t)((pad->tileY - ST_PASSENGER_H_TILES) * ST_TILE_PIXELS);
}
void stPassengerReset(StGameT *game) {
uint8_t i;
gNextFareIdx = 0u;
gWalkTick = 0u;
for (i = 0u; i < ST_MAX_PASSENGERS; i++) {
game->passengers[i].active = false;
}
// Seed the first fare for the level. Subsequent fares spawn on
// successful drop-off via spawnNextFare inside stPassengerTick.
spawnNextFare(game);
}
void stPassengerTick(StGameT *game) {
StPassengerT *p;
StTaxiT *t = &game->taxi;
uint8_t i;
bool atDest;
// Walk-cel cycling: advance every ST_WALK_TICKS_PER_CEL host frames
// for all waiting passengers in lockstep. Also step horizontally
// one pixel per cel advance so passengers walk back-and-forth
// across their pad. The C64 game's passenger AI was never fully
// traced -- this is a simplification giving each waiting fare
// some idle motion instead of standing in place.
if (++gWalkTick >= ST_WALK_TICKS_PER_CEL) {
gWalkTick = 0u;
for (i = 0u; i < ST_MAX_PASSENGERS; i++) {
StPassengerT *p = &game->passengers[i];
const StPadT *pad;
int16_t padXMin;
int16_t padXMax;
if (!p->active || p->onboard) {
continue;
}
p->walkPhase = (uint8_t)((p->walkPhase + 1u) % ST_WALK_FRAMES);
if (p->currentPad >= game->level.padCount) {
continue;
}
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) {
continue; // 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;
}
}
}
}
for (i = 0u; i < ST_MAX_PASSENGERS; i++) {
p = &game->passengers[i];
if (!p->active) {
continue;
}
if (!p->onboard) {
// Board if the taxi has landed on their pad.
if (t->landed && t->onPad == p->currentPad) {
p->onboard = true;
stAudioSfxPickup();
}
} else {
// Onboard: deliver to destination pad.
if (p->destPad >= game->level.padCount) {
p->active = false;
continue;
}
atDest = t->landed &&
t->onPad < game->level.padCount &&
t->onPad == p->destPad;
if (atDest) {
game->score += ST_FARE_SCORE;
stAudioSfxDropoff();
p->active = false;
spawnNextFare(game);
}
}
}
// If every fare is exhausted AND no active passenger remains,
// the level is complete.
if (gNextFareIdx >= effectiveFareTarget(game)) {
bool anyActive = false;
for (i = 0u; i < ST_MAX_PASSENGERS; i++) {
if (game->passengers[i].active) {
anyActive = true;
break;
}
}
if (!anyActive) {
game->state = ST_STATE_LEVEL_DONE;
}
}
}