217 lines
7.2 KiB
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;
|
|
}
|
|
}
|
|
}
|