// 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 #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; } } }