joeylib2/examples/spacetaxi/stSim.c

1291 lines
39 KiB
C

// Space Taxi -- the game tick ($5F40 main loop), physics, landing,
// collision, crash sequence, fuel and HUD arithmetic.
//
// Every function names the original routine it mirrors. The order of
// calls inside stSimTick is the order of the JSRs in the C64 main
// loop; the two raster waits that split it are where the VIC latched
// the frame, so the sprite snapshot (marshal) and the collision test
// sit at the same places.
#include <string.h>
#include "stSim.h"
// Screen-code strings the engine writes (all ASCII-compatible glyphs).
static const uint8_t kTextBlank[] = { 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0 }; // $6C38
static const uint8_t kHudTemplate[7] = { 0x66, 0x66, 0x66, 0x74, 0x77, 0x74, 0x74 }; // $43B1
// Fuel-pump chirp frequencies while refuelling ($6DFA, indexed by the
// mod-8 pump tick 1..4).
static const uint8_t kPumpFreq[5] = { 0x04, 0x05, 0x07, 0x0A, 0x10 };
static void applyVelocityX(StSimT *sim);
static void applyVelocityY(StSimT *sim);
static uint32_t backgroundRowMask(const StSimT *sim, int16_t px, int16_t py);
static void bitScrollHatch(StSimT *sim);
static void collisionDispatch(StSimT *sim);
static void collisionPhase1(StSimT *sim);
static void collisionPhase2(StSimT *sim);
static StTickResultE collisionPhase3(StSimT *sim);
static void computeCollisions(StSimT *sim);
static void crashStart(StSimT *sim);
static void drawCabIcons(StSimT *sim);
static void drawScreensCount(StSimT *sim);
static void edgeReflect(StSimT *sim);
static void fireButtonEdge(StSimT *sim);
static void fuelBarHud(StSimT *sim);
static void fuelTick(StSimT *sim);
static void hudDraw(StSimT *sim);
static void hudInit(StSimT *sim);
static void landedHandler(StSimT *sim);
static bool levelEndCheck(const StSimT *sim);
static void marshal(StSimT *sim);
static void padDetect(StSimT *sim);
static void padLandingBob(StSimT *sim);
static void passengerArrTick(StSimT *sim);
static void physicsTick(StSimT *sim);
static uint8_t readInput(StSimT *sim);
static void markCell(StSimT *sim, uint16_t cell);
static void setCell(StSimT *sim, uint16_t cell, uint8_t ch);
static void setColorCell(StSimT *sim, uint16_t cell, uint8_t color);
static void spriteMasks(const StSimT *sim, uint8_t idx, uint32_t *rows);
static void spritesOffReset(StSimT *sim);
static void taxiSpawnInit(StSimT *sim);
static void taxiSpriteCelSelect(StSimT *sim);
static uint8_t validateDigit(uint8_t ch);
// $6112 -- add the X velocity to the 17-bit X position (msb:col:frac)
// and refresh the sprite-0 column shadow.
static void applyVelocityX(StSimT *sim) {
uint16_t pos = (uint16_t)(((uint16_t)sim->posXcol << 8) | sim->posXlo);
uint32_t sum = (uint32_t)pos + (uint16_t)sim->velX;
uint8_t msb = (uint8_t)(sim->posXmsb + (uint8_t)(sum >> 16));
if (sim->velX < 0) {
msb++;
}
sim->posXmsb = (uint8_t)(msb & 1u);
sim->posXlo = (uint8_t)sum;
sim->posXcol = (uint8_t)(sum >> 8);
sim->spr[0].msb = sim->posXmsb;
sim->spr[0].col = sim->posXcol;
}
// $6145 -- Y: velocity += accel + gravity, position += velocity.
static void applyVelocityY(StSimT *sim) {
uint16_t pos;
sim->velY = (int16_t)((uint16_t)sim->accelY + sim->gravTemplateY + (uint16_t)sim->velY);
pos = (uint16_t)(((uint16_t)sim->posYrow << 8) | sim->posYlo);
pos = (uint16_t)(pos + (uint16_t)sim->velY);
sim->posYlo = (uint8_t)pos;
sim->posYrow = (uint8_t)(pos >> 8);
sim->spr[0].row = sim->posYrow;
}
// The 24 background bits under a sprite row whose leftmost pixel sits
// at visible pixel (px, py); a set bit is character graphics data.
static uint32_t backgroundRowMask(const StSimT *sim, int16_t px, int16_t py) {
uint32_t window = 0u;
int16_t col;
int16_t firstCol;
uint8_t k;
uint8_t shift;
if (py < 0 || py >= (int16_t)(ST_SCREEN_ROWS * 8u)) {
return 0u;
}
// Floor division so a negative px lands on the column to its left.
firstCol = (int16_t)((px - (px < 0 ? 7 : 0)) / 8);
shift = (uint8_t)(px - firstCol * 8);
for (k = 0u; k < 4u; k++) {
uint8_t byte = 0u;
col = (int16_t)(firstCol + (int16_t)k);
if (col >= 0 && col < (int16_t)ST_SCREEN_COLS) {
byte = sim->charset[sim->screen[ST_CELL((uint16_t)py >> 3, (uint16_t)col)]][py & 7];
}
window = (window << 8) | byte;
}
return (window >> (8u - shift)) & 0xFFFFFFu;
}
// $63D0 -- rotate every row of the transporter hatch glyph one pixel
// right: the animated energy field in the top-wall opening.
static void bitScrollHatch(StSimT *sim) {
uint8_t row;
for (row = 0u; row < 8u; row++) {
uint8_t b = sim->charset[ST_CHAR_TRANSPORTER][row];
sim->charset[ST_CHAR_TRANSPORTER][row] = (uint8_t)((b >> 1) | (b << 7));
}
sim->charDirty[ST_CHAR_TRANSPORTER] = 1u;
}
// $6966 -- latch the frame's collisions and run the crash phases.
static void collisionDispatch(StSimT *sim) {
computeCollisions(sim);
if (sim->collisionPhase != 0u) {
// Phases 1..3 dispatch elsewhere (stSimTick handles 3's exit).
return;
}
stFarePadLightingGate(sim);
if ((sim->spriteBgColl & 1u) == 0u) {
if ((sim->spriteSpriteColl & 1u) == 0u) {
return;
}
if (sim->deathInProgress != 0u) {
return;
}
if ((sim->spriteSpriteColl & 2u) != 0u) {
if ((sim->spriteSpriteColl & 0xF8u) != 0u) {
return;
}
stFareSquashed(sim);
}
if ((sim->spriteSpriteColl & 0xF8u) == 0u) {
return;
}
}
// $6A01: the level's verdict on sprite contact; background contact
// always kills.
sim->hitDispatchResult = stHookHitVerdict(sim);
if ((sim->spriteBgColl & 1u) == 0u) {
if (sim->hitDispatchResult == 0u) {
return;
}
}
crashStart(sim);
}
// $6A72 -- the wreck falls: every second tick sweep the scream, flip
// the debris cel, jitter, reflect off the side walls, drift with the
// old X velocity and accelerate down until row $DA.
static void collisionPhase1(StSimT *sim) {
uint8_t jitter;
uint16_t sum;
uint16_t colFrac;
if ((sim->hitDispatchResult & 0x80u) == 0u) {
sim->phaseTimer--;
if (sim->phaseTimer != 0u) {
return;
}
sim->phaseTimer = sim->phaseReload;
sim->thrustSweep--;
stAudioThrustSweep((uint8_t)(sim->thrustSweep >> 1));
sim->spr[0].ptr ^= 1u;
// $6A98: rng(5) - 3 added to the sprite column (msb follows).
jitter = (uint8_t)(stSimRng(sim, 5u) - 3u);
colFrac = (uint16_t)(((uint16_t)sim->spr[0].msb << 8) | sim->spr[0].col);
colFrac = (uint16_t)(colFrac + (uint16_t)(int16_t)(int8_t)jitter);
sim->spr[0].col = (uint8_t)colFrac;
sim->spr[0].msb = (uint8_t)(colFrac >> 8);
edgeReflect(sim);
applyVelocityX(sim);
// vy += 40 (low byte with carry into the high byte), row += vy hi.
sum = (uint16_t)((uint16_t)(sim->velY & 0xFFu) + 0x28u);
sim->velY = (int16_t)((uint16_t)(((uint16_t)sim->velY & 0xFF00u) + (sum & 0x100u)) | (sum & 0xFFu));
sim->spr[0].row = (uint8_t)(sim->spr[0].row + (uint8_t)((uint16_t)sim->velY >> 8));
if (sim->spr[0].row < 0xDAu) {
return;
}
sim->spr[0].row = 0xDAu;
}
// $6AD6: hit the floor -> phase 2 with a fresh 2-tick cadence.
sim->collisionPhase++;
sim->phaseReload = 2u;
sim->phaseTimer = 2u;
stAudioSfx(stC64SfxProgram(ST_SFX_IMPACT));
}
// $6B24 -- walk the wreck cels $CC..$D1 at a slowing rate, then hide.
static void collisionPhase2(StSimT *sim) {
sim->phaseTimer--;
if (sim->phaseTimer != 0u) {
return;
}
sim->phaseReload++;
sim->phaseTimer = sim->phaseReload;
sim->spr[0].ptr++;
if (sim->spr[0].ptr != 0xD1u) {
return;
}
sim->spr[0].enable = 0u;
sim->collisionPhase++;
sim->phaseTimer = 0x46u;
}
// $6B4C -- 70-tick pause, then the life bookkeeping.
static StTickResultE collisionPhase3(StSimT *sim) {
uint8_t p;
sim->phaseTimer--;
if (sim->phaseTimer != 0u) {
return ST_TICK_CONTINUE;
}
if (sim->demoMode != 0u) {
return ST_TICK_CRASH_DONE;
}
if (sim->stage == ST_STAGE_RIDING) {
hudInit(sim);
if (sim->fareSlotCount != 0u) {
sim->spriteSlots[sim->activeDyingSlot] = 0u;
sim->fareSlotCount--;
if (sim->activeSpriteIdx == 0x0Bu) {
uint8_t k;
for (k = 0u; k < 4u; k++) {
setCell(sim, (uint16_t)(ST_CELL_TRANSPORTER + k), ST_CHAR_TRANSPORTER);
}
} else {
sim->spriteSlots[sim->activeSpriteIdx] = 0u;
sim->fareSlotCount--;
}
}
sim->activeSpriteIdx = 0u;
sim->stage = ST_STAGE_IDLE;
stSimDrawText(sim, 14u, 24u, kTextBlank, 1u);
}
// $6BBA
p = sim->player;
sim->cabs[p]--;
if (sim->cabs[p] == 0u) {
return ST_TICK_PLAYER_OUT;
}
setCell(sim, (uint16_t)(ST_CELL_CAB_ICONS + sim->cabs[p]), ST_CHAR_BLANK);
return ST_TICK_LIFE_LOST;
}
// The VIC's $D01E / $D01F for the displayed frame: pixel overlap of
// enabled sprites with each other and of sprite 0 with the background.
static void computeCollisions(StSimT *sim) {
uint32_t mask[ST_HW_SPRITES][ST_SPRITE_H];
uint8_t live = 0u;
uint8_t ss = 0u;
uint8_t bg = 0u;
uint8_t a;
uint8_t b;
uint8_t r;
for (a = 0u; a < ST_HW_SPRITES; a++) {
if ((sim->frame.enableMask & (uint8_t)(1u << a)) != 0u) {
spriteMasks(sim, a, mask[a]);
live |= (uint8_t)(1u << a);
}
}
if ((live & 1u) != 0u) {
int16_t px = (int16_t)((int16_t)sim->frame.x[0] - ST_SPRITE_X_ORIGIN);
int16_t py = (int16_t)((int16_t)sim->frame.y[0] - ST_SPRITE_Y_ORIGIN);
for (r = 0u; r < ST_SPRITE_H; r++) {
if (mask[0][r] != 0u && (mask[0][r] & backgroundRowMask(sim, px, (int16_t)(py + r))) != 0u) {
bg |= 1u;
break;
}
}
}
for (a = 0u; a < ST_HW_SPRITES; a++) {
if ((live & (uint8_t)(1u << a)) == 0u) {
continue;
}
for (b = (uint8_t)(a + 1u); b < ST_HW_SPRITES; b++) {
int16_t dx;
int16_t dy;
bool hit = false;
if ((live & (uint8_t)(1u << b)) == 0u) {
continue;
}
dx = (int16_t)((int16_t)sim->frame.x[b] - (int16_t)sim->frame.x[a]);
dy = (int16_t)((int16_t)sim->frame.y[b] - (int16_t)sim->frame.y[a]);
if (dx <= -ST_SPRITE_W || dx >= ST_SPRITE_W || dy <= -ST_SPRITE_H || dy >= ST_SPRITE_H) {
continue;
}
for (r = 0u; r < ST_SPRITE_H && !hit; r++) {
int16_t rb = (int16_t)((int16_t)r - dy);
uint32_t mb;
if (rb < 0 || rb >= (int16_t)ST_SPRITE_H) {
continue;
}
mb = mask[b][rb];
if (dx >= 0) {
mb >>= dx;
} else {
mb = (mb << (uint8_t)(-dx)) & 0xFFFFFFu;
}
if ((mask[a][r] & mb) != 0u) {
hit = true;
}
}
if (hit) {
ss |= (uint8_t)((1u << a) | (1u << b));
}
}
}
sim->spriteSpriteColl = ss;
sim->spriteBgColl = bg;
}
// $6A2B -- start the wreck sequence.
static void crashStart(StSimT *sim) {
sim->spr[0].ptr = 0xCCu;
sim->collisionPhase++;
sim->phaseTimer = 2u;
sim->phaseReload = 2u;
stAudioNoise(false);
sim->eventDispatchType = 0u;
sim->activePad = 0u;
sim->spr[2].enable = 0u;
sim->spr[2].row = 0u;
sim->velY = 0x0303;
stAudioSfx(stC64SfxProgram(ST_SFX_CRASH));
sim->thrustSweep = 0xA0u;
if (sim->stage == ST_STAGE_WALK_TO_CAB) {
sim->stage--;
}
}
// $6372 -- one cab icon per spare cab (cabs - 1 of them).
static void drawCabIcons(StSimT *sim) {
uint8_t k = sim->cabs[sim->player];
while (k > 1u) {
k--;
setCell(sim, (uint16_t)(ST_CELL_CAB_ICONS + k), ST_CHAR_CAB_ICON);
}
}
// $6384 -- screens completed, or the finished marker past 25.
static void drawScreensCount(StSimT *sim) {
uint8_t v = sim->levelState;
uint8_t tens = 0u;
if (v >= 0x19u) {
setCell(sim, (uint16_t)(ST_CELL_SCREENS + 0u), 0xCBu);
setCell(sim, (uint16_t)(ST_CELL_SCREENS + 1u), 0xCCu);
setCell(sim, (uint16_t)(ST_CELL_SCREENS + 2u), 0xCDu);
setCell(sim, (uint16_t)(ST_CELL_SCREENS + 3u), 0xCEu);
setCell(sim, (uint16_t)(ST_CELL_SCREENS + 4u), 0xCFu);
return;
}
setCell(sim, (uint16_t)(ST_CELL_SCREENS + 2u), 0xCAu);
setCell(sim, (uint16_t)(ST_CELL_SCREENS + 3u), ST_CHAR_ZERO);
setCell(sim, (uint16_t)(ST_CELL_SCREENS + 4u), ST_CHAR_ZERO);
while (v >= 10u) {
v = (uint8_t)(v - 10u);
tens++;
}
setCell(sim, (uint16_t)(ST_CELL_SCREENS + 1u), (v == 0u) ? ST_CHAR_ZERO : (uint8_t)(ST_CHAR_DIGIT_BASE + v));
setCell(sim, (uint16_t)(ST_CELL_SCREENS + 0u), (tens == 0u) ? ST_CHAR_BLANK : (uint8_t)(ST_CHAR_DIGIT_BASE + tens));
}
// $6AED -- negate the X velocity when the wreck crosses column 23
// leftward or column 65 (in the high half) rightward.
static void edgeReflect(StSimT *sim) {
bool reflect = false;
if (sim->spr[0].msb == 0u) {
if (sim->velX < 0 && sim->spr[0].col < 0x17u) {
reflect = true;
}
} else {
if (sim->velX >= 0 && sim->spr[0].col >= 0x41u) {
reflect = true;
}
}
if (reflect) {
sim->velX = (int16_t)(-sim->velX);
}
}
// $63DD -- FIRE press edge while airborne toggles the landing gear.
static void fireButtonEdge(StSimT *sim) {
if (sim->activePad != 0u) {
return;
}
if (sim->fireWasHeld != 0u) {
sim->fireWasHeld = (uint8_t)(sim->inputMask & 0x10u);
return;
}
if ((sim->inputMask & 0x10u) == 0u) {
return;
}
sim->fireWasHeld = 0x10u;
sim->spr[0].ptr ^= 1u;
stAudioSfx(stC64SfxProgram(ST_SFX_GEAR));
}
// $6D6A -- sprite 2 is the exhaust: two columns left of the cab,
// visible every other tick while a direction is held.
void stSimFlameUpdate(StSimT *sim) {
uint16_t sum;
if (sim->dirMask == 0u) {
stAudioNoise(false);
sim->spr[2].enable = 0u;
return;
}
stAudioNoise(true);
sum = (uint16_t)((uint16_t)sim->spr[0].col + 0xFEu);
sim->spr[2].col = (uint8_t)sum;
sim->spr[2].msb = (uint8_t)(sim->spr[0].msb + 0xFFu + (uint8_t)(sum >> 8));
sim->spr[2].row = sim->spr[0].row;
sim->flameParity ^= 1u;
if (sim->flameParity == 0u) {
sim->spr[2].enable = 0u;
return;
}
sim->spr[2].ptr = stC64FlameCel(sim->dirMask & 0x0Fu);
sim->spr[2].enable = 1u;
}
// $6419 -- the climb/descend indicator: colour RAM cells beside the
// fuel gauge flash red (rising), cyan (level) or yellow (falling).
static void fuelBarHud(StSimT *sim) {
uint16_t cell;
uint8_t color;
setColorCell(sim, ST_CELL(23, 10), 0x0Bu);
setColorCell(sim, ST_CELL(23, 11), 0x0Bu);
setColorCell(sim, ST_CELL(24, 10), 0x0Bu);
setColorCell(sim, ST_CELL(24, 11), 0x0Bu);
sim->fuelBarTick = (uint8_t)((sim->fuelBarTick + 1u) & 7u);
if (sim->fuelBarTick >= 5u) {
return;
}
if (sim->velY < 0) {
color = 0x02u;
cell = ST_CELL(24, 10);
} else if (sim->velY == 0) {
color = 0x03u;
cell = ST_CELL(23, 10);
} else {
color = 0x07u;
cell = ST_CELL(23, 10);
}
setColorCell(sim, cell, color);
setColorCell(sim, (uint16_t)(cell + 1u), color);
}
// $6E23 -- the fuel gauge: half a cell burns every fuelRate ticks in
// the air; on the fuel pad (the extra last pad) it refills at ten
// cents per half cell.
static void fuelTick(StSimT *sim) {
const StLevelT *L = sim->level;
uint8_t x;
if (sim->activePad == L->padCount && sim->activePad != L->specialPad) {
if (sim->screen[ST_CELL_FUEL_LAST] == ST_CHAR_BLANK) {
return;
}
if (!stSimDecrementNumber(sim, ST_CELL_SCORE, 5u)) {
return;
}
sim->padAnimTick = (uint8_t)((sim->padAnimTick + 1u) & 7u);
if (sim->padAnimTick == 0u) {
stAudioSfx(stC64SfxProgram(ST_SFX_CASH));
} else if (sim->padAnimTick < 5u) {
stAudioVoice1Freq(kPumpFreq[sim->padAnimTick]);
}
if (sim->padAnimTick != 0u) {
return;
}
x = sim->fuelCells;
if (sim->screen[ST_CELL_FUEL + x] == ST_CHAR_BLANK) {
x++;
sim->fuelCells = x;
setCell(sim, (uint16_t)(ST_CELL_FUEL + x), ST_CHAR_FUEL_HALF);
return;
}
setCell(sim, (uint16_t)(ST_CELL_FUEL + x), ST_CHAR_BLANK);
if (sim->screen[ST_CELL_FUEL_LAST] == ST_CHAR_BLANK) {
stAudioSfx(stC64SfxProgram(ST_SFX_FUEL_FULL));
}
return;
}
// $6EAA
if (sim->activePad != 0u) {
return;
}
sim->fuelCountdown--;
if (sim->fuelCountdown != 0u) {
return;
}
if (sim->fuelCells < 3u) {
stAudioSfx(stC64SfxProgram(ST_SFX_FUEL_LOW));
}
sim->fuelCountdown = L->fuelRate;
x = sim->fuelCells;
if (sim->screen[ST_CELL_FUEL] == ST_CHAR_FUEL_HALF) {
return;
}
if (sim->screen[ST_CELL_FUEL + x] == ST_CHAR_FUEL_HALF) {
setCell(sim, (uint16_t)(ST_CELL_FUEL + x), ST_CHAR_FUEL_EMPTY);
x--;
sim->fuelCells = x;
return;
}
setCell(sim, (uint16_t)(ST_CELL_FUEL + x), ST_CHAR_FUEL_HALF);
}
// $43E5 -- the fare meter loses a penny every tick.
static void hudDraw(StSimT *sim) {
(void)stSimDecrementNumber(sim, ST_CELL_FARE, 6u);
}
// $43A5 -- blank the fare meter.
static void hudInit(StSimT *sim) {
uint8_t k;
for (k = 0u; k < ST_NUMBER_CHARS; k++) {
setCell(sim, (uint16_t)(ST_CELL_FARE + k), kHudTemplate[k]);
}
}
// $657B -- parked on a pad: wait for UP to be released, then UP takes
// off (velocities cleared, gear retracted).
static void landedHandler(StSimT *sim) {
if (sim->activePadMirror != 0u) {
if ((sim->inputMask & 1u) == 0u) {
sim->activePadMirror = 0u;
}
return;
}
if (sim->deathInProgress != 0u) {
return;
}
if ((sim->inputMask & 1u) == 0u) {
return;
}
sim->velY = 0;
sim->velX = 0;
sim->activePad = 0u;
sim->spr[0].ptr &= 0xFEu;
if (sim->stage == ST_STAGE_WALK_TO_CAB) {
sim->stage--;
}
}
// $6BE7 -- the cab has flown out of the top of the screen.
static bool levelEndCheck(const StSimT *sim) {
return sim->posYrow < 0x1Bu;
}
// $4253 + $4293 -- snapshot the shadow tables into the frame the VIC
// shows next.
static void marshal(StSimT *sim) {
uint8_t i;
uint8_t enable = 0u;
for (i = 0u; i < ST_HW_SPRITES; i++) {
sim->frame.x[i] = (uint16_t)(((uint16_t)(sim->spr[i].msb != 0u ? 1u : 0u) << 8) | sim->spr[i].col);
sim->frame.y[i] = sim->spr[i].row;
sim->frame.ptr[i] = sim->spr[i].ptr;
sim->frame.color[i] = sim->spr[i].color;
if (sim->spr[i].enable != 0u) {
enable |= (uint8_t)(1u << i);
}
}
sim->frame.enableMask = enable;
sim->frame.multiMask = sim->multiColorMask;
}
// $645C -- with the gear down and a slow descent, an exact row match
// inside a pad's X bounds is a landing.
static void padDetect(StSimT *sim) {
int8_t i;
if (sim->activePad != 0u) {
return;
}
if ((sim->spr[0].ptr & 1u) == 0u) {
return;
}
if (((uint16_t)sim->velY >> 8) != 0u) {
return;
}
for (i = (int8_t)(sim->level->padCount - 1u); i >= 0; i--) {
const StPadT *p = &sim->pads[i];
int16_t taxiX = (int16_t)(((int16_t)sim->posXmsb << 8) | sim->posXcol);
int16_t x1 = (int16_t)(((int16_t)p->x1Hi << 8) | p->x1Lo);
int16_t x2 = (int16_t)(((int16_t)p->x2Hi << 8) | p->x2Lo);
if (sim->spr[0].row != p->row) {
continue;
}
if ((int16_t)(taxiX - x1) < 0) {
continue;
}
if ((int16_t)(x2 - taxiX) < 0) {
continue;
}
sim->activePad = (uint8_t)(i + 1);
sim->activePadMirror = sim->activePad;
sim->eventDispatchType = 0u;
if (sim->activePad != sim->activeSpriteIdx) {
if (sim->stage == ST_STAGE_RIDING) {
sim->eventDispatchType = (sim->activeSpriteIdx == 0x0Bu) ? 3u : 2u;
} else if (sim->stage == ST_STAGE_WAIT) {
sim->eventDispatchType = 1u;
}
}
sim->dirMask = 0u;
sim->deathInProgress = 1u;
stSimPadLight(sim, true);
sim->bobTimer = 2u;
if (((uint8_t)sim->velY & 0x80u) == 0u) {
stAudioSfx(stC64SfxProgram(ST_SFX_LAND_SOFT));
return;
}
sim->bobTimer = 8u;
stAudioSfx(stC64SfxProgram(ST_SFX_LAND_HARD));
if (sim->stage == ST_STAGE_RIDING) {
hudInit(sim);
}
return;
}
}
// $6DFF -- the touchdown bob: the sprite dips and rises while the
// timer runs down.
static void padLandingBob(StSimT *sim) {
if (sim->bobTimer == 0u) {
return;
}
sim->bobTimer--;
if ((sim->bobTimer & 1u) != 0u) {
sim->spr[0].row--;
return;
}
sim->spr[0].row++;
if (sim->bobTimer == 0u) {
sim->deathInProgress = 0u;
stSimPadLight(sim, false);
}
}
// $70B1 -- one bonus cab when the score's hundreds digit reaches 3.
static void passengerArrTick(StSimT *sim) {
uint8_t p = sim->player;
if (sim->bonusLatch[p] != 0u) {
return;
}
if (sim->screen[ST_CELL_SCORE + 1u] < 0x6Du) {
return;
}
sim->cabs[p]++;
sim->bonusLatch[p] = sim->cabs[p];
setCell(sim, (uint16_t)(ST_CELL_CAB_ICONS - 1u + sim->cabs[p]), ST_CHAR_CAB_ICON);
}
// $6032 -- input, thrust, gravity, integration.
static void physicsTick(StSimT *sim) {
uint8_t in;
sim->accelX = 0;
sim->accelY = 0;
in = readInput(sim);
in = stHookInput(sim, in);
sim->inputMask = in;
if (sim->activePad != 0u) {
landedHandler(sim);
return;
}
if (sim->screen[ST_CELL_FUEL] == ST_CHAR_MENU_SENTINEL) {
// Out of fuel: only FIRE survives, no thrust at all.
sim->inputMask &= 0x10u;
} else {
if ((sim->inputMask & 0x0Cu) != 0u) {
if ((sim->spr[0].ptr & 1u) != 0u) {
sim->inputMask &= 0x13u;
} else {
sim->accelX = (int16_t)sim->accelTemplateX;
if ((sim->inputMask & 0x08u) == 0u) {
sim->accelX = (int16_t)(-sim->accelX);
}
}
}
if ((sim->inputMask & 0x03u) != 0u) {
sim->accelY = (int16_t)sim->accelTemplateY;
if ((sim->inputMask & 0x02u) == 0u) {
sim->accelY = (int16_t)(-sim->accelY);
}
}
}
sim->dirMask = (uint8_t)(sim->inputMask & 0x0Fu);
sim->velX = (int16_t)((uint16_t)sim->accelX + sim->gravTemplateX + (uint16_t)sim->velX);
applyVelocityX(sim);
applyVelocityY(sim);
}
// $6040 + $48F2 -- the joystick byte, or the recorded demo mask.
static uint8_t readInput(StSimT *sim) {
uint8_t mask;
uint16_t next;
if (sim->demoMode == 0u || sim->postMortem != 0u) {
return sim->rawInput;
}
mask = sim->demoBuf[sim->demoOff];
sim->demoTimer--;
if (sim->demoTimer == 0u) {
next = (uint16_t)(sim->demoOff + 3u);
if (next < sizeof(sim->demoBuf)) {
sim->demoTimer = sim->demoBuf[next];
}
next = (uint16_t)(sim->demoOff + 2u);
if (next < sizeof(sim->demoBuf)) {
sim->demoOff = next;
}
}
return mask;
}
static void markCell(StSimT *sim, uint16_t cell) {
if (sim->cellDirty[cell] != 0u) {
return;
}
sim->cellDirty[cell] = 1u;
if (sim->dirtyCount < (uint8_t)(sizeof(sim->dirtyList) / sizeof(sim->dirtyList[0]))) {
sim->dirtyList[sim->dirtyCount++] = cell;
} else {
sim->dirtyAll = true;
}
}
static void setCell(StSimT *sim, uint16_t cell, uint8_t ch) {
if (sim->screen[cell] != ch) {
sim->screen[cell] = ch;
markCell(sim, cell);
}
}
static void setColorCell(StSimT *sim, uint16_t cell, uint8_t color) {
color = (uint8_t)(color & 0x0Fu);
if (sim->color[cell] != color) {
sim->color[cell] = color;
markCell(sim, cell);
}
}
// 21 rows of 24-bit foreground masks for a displayed sprite. A
// multicolour sprite's 2-bit pairs cover two pixels each.
static void spriteMasks(const StSimT *sim, uint8_t idx, uint32_t *rows) {
const uint8_t *bm = stSimSpriteBitmap(sim, sim->frame.ptr[idx]);
bool multi = (sim->frame.multiMask & (uint8_t)(1u << idx)) != 0u;
uint8_t r;
for (r = 0u; r < ST_SPRITE_H; r++) {
uint32_t bits = 0u;
if (bm != 0) {
bits = ((uint32_t)bm[r * 3u] << 16) | ((uint32_t)bm[r * 3u + 1u] << 8) | bm[r * 3u + 2u];
}
if (multi) {
uint32_t m = 0u;
uint8_t i;
for (i = 0u; i < 12u; i++) {
if ((bits & (3u << (i * 2u))) != 0u) {
m |= (3u << (i * 2u));
}
}
bits = m;
}
rows[r] = bits;
}
}
// $6946 -- all sprites off, latches cleared, nobody on a pad.
static void spritesOffReset(StSimT *sim) {
uint8_t i;
for (i = 0u; i < ST_HW_SPRITES; i++) {
sim->spr[i].enable = 0u;
}
sim->spriteSpriteColl = 0u;
sim->spriteBgColl = 0u;
sim->activePad = 0u;
sim->activeSpriteIdx = 0u;
}
// $6888 -- place the cab at the level's spawn point with a full tank.
static void taxiSpawnInit(StSimT *sim) {
const StLevelT *L = sim->level;
uint8_t k;
sim->spr[0].enable = 1u;
sim->multiColorMask = 0x07u;
sim->spr[0].ptr = 0xC0u;
sim->posYlo = L->spawn[3];
sim->posYrow = L->spawn[4];
sim->posXlo = L->spawn[0];
sim->posXcol = L->spawn[1];
sim->posXmsb = L->spawn[2];
sim->spr[0].msb = L->spawn[2];
sim->spr[0].col = L->spawn[1];
sim->spr[0].row = L->spawn[4];
sim->velX = 0;
sim->velY = 0;
sim->deathInProgress = 0u;
sim->dirMask = 0u;
sim->inputMask = 0u;
sim->activePad = 0u;
sim->collisionPhase = 0u;
sim->padAnimTick = 0u;
stSimPadLight(sim, false);
fuelBarHud(sim);
sim->fuelCells = 11u;
for (k = 0u; k < ST_FUEL_CELLS; k++) {
setCell(sim, (uint16_t)(ST_CELL_FUEL + k), ST_CHAR_BLANK);
}
sim->fuelCountdown = L->fuelRate;
}
// $619B -- LEFT/RIGHT pick the facing pair; bit 0 (the gear) survives.
static void taxiSpriteCelSelect(StSimT *sim) {
uint8_t gear = (uint8_t)(sim->spr[0].ptr & 1u);
uint8_t base;
if ((sim->dirMask & 0x04u) != 0u) {
base = 0xDCu;
} else if ((sim->dirMask & 0x08u) != 0u) {
base = 0xC0u;
} else {
return;
}
sim->spr[0].ptr = (uint8_t)(base | gear);
}
// $4345 -- a HUD glyph's numeric value: blanks and the zero glyph are 0.
static uint8_t validateDigit(uint8_t ch) {
uint8_t v;
if (ch == ST_CHAR_BLANK) {
return 0u;
}
v = (uint8_t)(ch - ST_CHAR_DIGIT_BASE);
if (v == 10u) {
return 0u;
}
return v;
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
// $61FB -- next player's turn; wraps to player 0 when everybody has had
// this screen (the caller then loads the next one).
bool stSimAdvancePlayer(StSimT *sim) {
sim->player++;
if (sim->player == sim->playerCount) {
sim->player = 0u;
return true;
}
return false;
}
// $704E -- keep the current player's score and fare rows.
void stSimArchiveHud(StSimT *sim) {
memcpy(sim->scoreBackup[sim->player], &sim->screen[ST_CELL_SCORE], ST_NUMBER_CHARS);
memcpy(sim->fareBackup[sim->player], &sim->screen[ST_CELL_FARE], ST_NUMBER_CHARS);
}
// $4354 -- add a 7-glyph decimal blob onto the number at dstCell,
// right to left, skipping the decimal point, with leading blanks.
void stSimBcdAdd(StSimT *sim, uint16_t dstCell, const uint8_t *blob) {
int8_t y;
uint8_t carry = 0u;
for (y = 6; y >= 0; y--) {
uint8_t sum;
uint8_t ch;
if (y == 4) {
continue;
}
sum = (uint8_t)(validateDigit(blob[y]) + validateDigit(sim->screen[dstCell + (uint16_t)y]) + carry);
carry = 0u;
if (sum >= 10u) {
carry = 1u;
sum = (uint8_t)(sum - 10u);
}
ch = (uint8_t)(ST_CHAR_DIGIT_BASE + sum);
if (ch == ST_CHAR_DIGIT_BASE) {
ch = ST_CHAR_ZERO;
}
setCell(sim, (uint16_t)(dstCell + (uint16_t)y), ch);
}
// $4393: leading-zero suppression across the integer part.
for (y = 0; y < 3; y++) {
if (sim->screen[dstCell + (uint16_t)y] != ST_CHAR_ZERO) {
break;
}
setCell(sim, (uint16_t)(dstCell + (uint16_t)y), ST_CHAR_BLANK);
}
}
// $440B -- subtract one from the digit at `position` of the number at
// `cell`, borrowing leftward. False when there is nothing to take.
bool stSimDecrementNumber(StSimT *sim, uint16_t cell, uint8_t position) {
uint8_t y = position;
uint8_t ch;
for (;;) {
ch = sim->screen[cell + y];
if (ch == ST_CHAR_BLANK) {
return false;
}
if (ch != ST_CHAR_ZERO) {
break;
}
do {
y--;
} while (y == 4u);
}
y = position;
for (;;) {
ch = sim->screen[cell + y];
if (ch != ST_CHAR_ZERO) {
break;
}
setCell(sim, (uint16_t)(cell + y), (uint8_t)(ST_CHAR_DIGIT_BASE + 9u));
do {
y--;
} while (y == 4u);
}
ch = (uint8_t)(ch - 1u);
setCell(sim, (uint16_t)(cell + y), ch);
if (ch != ST_CHAR_DIGIT_BASE) {
return true;
}
setCell(sim, (uint16_t)(cell + y), ST_CHAR_ZERO);
if (y >= 3u) {
return true;
}
if (y == 0u) {
setCell(sim, cell, ST_CHAR_BLANK);
return true;
}
if (sim->screen[cell + y - 1u] != ST_CHAR_BLANK) {
return true;
}
setCell(sim, (uint16_t)(cell + y), ST_CHAR_BLANK);
return true;
}
// $41C2 -- write a screen-code string (terminated by any byte < 6)
// with one colour.
void stSimDrawText(StSimT *sim, uint8_t col, uint8_t row, const uint8_t *text, uint8_t color) {
uint16_t cell = ST_CELL(row, col);
while (*text >= 6u) {
setCell(sim, cell, *text);
setColorCell(sim, cell, color);
cell++;
text++;
}
}
// $62F0 + $5F18 -- load the screen image and run the level prelude.
void stSimEnterLevel(StSimT *sim, const StLevelT *level) {
uint8_t k;
sim->level = level;
memcpy(sim->pads, level->pads, sizeof(sim->pads));
memcpy(sim->screen, level->screen, ST_SCREEN_CELLS);
memcpy(sim->color, level->color, ST_SCREEN_CELLS);
stSimDirtyAll(sim);
memcpy(sim->charset, stC64Charset(), sizeof(sim->charset));
memset(sim->charDirty, 1, ST_CHARSET_CHARS);
sim->borderColor = level->header[0];
sim->bgColor = level->header[1];
sim->spr[0].color = level->header[7];
sim->spr[1].color = level->header[8];
sim->spriteMc0 = level->header[5];
sim->spriteMc1 = level->header[6];
sim->accelTemplateY = level->accelY;
sim->accelTemplateX = level->accelX;
sim->gravTemplateY = level->gravY;
sim->gravTemplateX = level->gravX;
memset(sim->spriteSlots, 0, sizeof(sim->spriteSlots));
sim->fareSlotCount = 0u;
// $634D: this player's HUD rows come back.
for (k = 0u; k < ST_NUMBER_CHARS; k++) {
setCell(sim, (uint16_t)(ST_CELL_SCORE + k), sim->scoreBackup[sim->player][k]);
setCell(sim, (uint16_t)(ST_CELL_FARE + k), sim->fareBackup[sim->player][k]);
}
for (k = 0u; k < 7u; k++) {
setCell(sim, (uint16_t)(ST_CELL_CAB_ICONS + k), ST_CHAR_BLANK);
}
drawCabIcons(sim);
drawScreensCount(sim);
// Prelude $5F18 (the player-turn bookkeeping ran before the load).
for (k = 0u; k < ST_HW_SPRITES; k++) {
sim->spr[k].enable = 0u;
}
sim->hookLevel = level->levelIndex;
memcpy(sim->hookRam, level->hookData, ST_HOOK_BYTES);
stSimRespawn(sim);
}
// $5F27 -- the respawn half of the prelude.
void stSimRespawn(StSimT *sim) {
spritesOffReset(sim);
stHookPrelude0(sim);
// $6F18 takeoffSetup: from the third screen on the cab starts with
// a passenger aboard who wants pad 1.
sim->eventDispatchType = 0u;
sim->stage = ST_STAGE_IDLE;
if (sim->levelState >= 2u) {
static const uint8_t kPadPlease[] = { 0x50, 0x41, 0x44, 0x20, 0x31, 0x20, 0x20, 0x50, 0x4C, 0x45, 0x41, 0x53, 0x45, 0 };
sim->stage = ST_STAGE_RIDING;
sim->activeSpriteIdx = 1u;
sim->activeDyingSlot = 1u;
stSimDrawText(sim, 14u, 24u, kPadPlease, 1u);
sim->eventDispatchType = 1u;
}
taxiSpawnInit(sim);
stHookPrelude1(sim);
// $6906 framePresent: the start-of-screen jingle ends with the SID
// (and the demo RNG) reset.
marshal(sim);
stAudioSilence();
sim->spriteSpriteColl = 0u;
sim->spriteBgColl = 0u;
sim->rngT1 = 0x06u;
sim->rngT2 = 0x17u;
}
// $5EC4 + $4207 + $48AD -- a fresh game (or the attract demo).
void stSimNewGame(StSimT *sim, uint8_t playerCount, bool demo) {
uint8_t p;
uint8_t keepBuf[sizeof(sim->demoBuf)];
uint8_t keepParity = sim->flameParity;
uint8_t keepWave = sim->waveIdx;
uint8_t keepWalk = sim->walkParity;
// $5EC4 clears the game state but the playback buffer, the flame
// parity, the wave index and the walk parity all survive.
memcpy(keepBuf, sim->demoBuf, sizeof(keepBuf));
memset(sim, 0, sizeof(*sim));
memcpy(sim->demoBuf, keepBuf, sizeof(keepBuf));
sim->flameParity = keepParity;
sim->waveIdx = keepWave;
sim->walkParity = keepWalk;
sim->stage0Rng = 0x64u;
sim->decayReload = 3u;
sim->playerCount = playerCount;
sim->player = 0u;
sim->playersDone = 0u;
sim->levelState = 0u;
sim->demoMode = demo ? 1u : 0u;
sim->rngHost = 0x2545F491u;
for (p = 0u; p < ST_MAX_PLAYERS; p++) {
sim->cabs[p] = ST_CABS_PER_PLAYER;
memcpy(sim->scoreBackup[p], kHudTemplate, ST_NUMBER_CHARS);
memcpy(sim->fareBackup[p], kHudTemplate, ST_NUMBER_CHARS);
}
sim->rngT1 = 0x06u;
sim->rngT2 = 0x17u;
}
// Everything must be repainted.
void stSimDirtyAll(StSimT *sim) {
memset(sim->cellDirty, 1, ST_SCREEN_CELLS);
sim->dirtyCount = 0u;
sim->dirtyAll = true;
}
// $6866 / $6877 -- the pad indicator cells beside the HUD.
void stSimPadLight(StSimT *sim, bool on) {
if (on) {
setColorCell(sim, ST_CELL(23, 28), 0x0Bu);
setColorCell(sim, ST_CELL(23, 29), 0x0Bu);
setColorCell(sim, ST_CELL(24, 28), 0x02u);
setColorCell(sim, ST_CELL(24, 29), 0x02u);
} else {
setColorCell(sim, ST_CELL(24, 28), 0x0Bu);
setColorCell(sim, ST_CELL(24, 29), 0x0Bu);
setColorCell(sim, ST_CELL(23, 28), 0x07u);
setColorCell(sim, ST_CELL(23, 29), 0x07u);
}
}
// $401B -- one character into screen RAM.
void stSimPutChar(StSimT *sim, uint8_t col, uint8_t row, uint8_t ch) {
setCell(sim, ST_CELL(row, col), ch);
}
// $401E -- one colour into colour RAM.
void stSimPutColor(StSimT *sim, uint8_t col, uint8_t row, uint8_t color) {
setColorCell(sim, ST_CELL(row, col), color);
}
// $4080 -- 1..n. The demo walks a 64-byte table so its rides replay
// exactly; a real game reads the SID noise oscillator, which a host
// LCG stands in for.
uint8_t stSimRng(StSimT *sim, uint8_t n) {
uint8_t r;
uint16_t product;
if (sim->demoMode != 0u) {
sim->rngT1 = (uint8_t)((sim->rngT1 + 1u) & 0x3Fu);
sim->rngT2++;
r = (uint8_t)(stC64RngByte(sim->rngT1) + sim->rngT2);
} else {
sim->rngHost = sim->rngHost * 1103515245u + 12345u;
r = (uint8_t)(sim->rngHost >> 16);
}
product = (uint16_t)((uint16_t)n * (uint16_t)r);
return (uint8_t)((product >> 8) + 1u);
}
// The score glyphs as pennies (for the high-score table).
uint32_t stSimScorePennies(const StSimT *sim) {
uint32_t v = 0u;
uint8_t k;
for (k = 0u; k < ST_NUMBER_CHARS; k++) {
if (k == 4u) {
continue;
}
v = v * 10u + validateDigit(sim->screen[ST_CELL_SCORE + k]);
}
return v;
}
// The boot-time content of the playback buffer.
void stSimSeedDemoBuffer(StSimT *sim, const uint8_t *image, uint16_t len) {
if (len > sizeof(sim->demoBuf)) {
len = (uint16_t)sizeof(sim->demoBuf);
}
memcpy(sim->demoBuf, image, len);
}
// $0902 / $4740 -- load a recording over the buffer and rewind.
void stSimSetDemoStream(StSimT *sim, const uint8_t *stream, uint16_t len) {
if (len > sizeof(sim->demoBuf)) {
len = (uint16_t)sizeof(sim->demoBuf);
}
memcpy(sim->demoBuf, stream, len);
sim->demoOff = 0u;
sim->demoTimer = sim->demoBuf[1];
}
// Bitmap for a block pointer: the standard set, or the level's own.
const uint8_t *stSimSpriteBitmap(const StSimT *sim, uint8_t ptr) {
uint8_t k;
if (ptr >= ST_SPRITE_PTR_FIRST && ptr <= ST_SPRITE_PTR_LAST) {
return stC64SpriteBitmap((uint8_t)(ptr - ST_SPRITE_PTR_FIRST));
}
if (sim->level != 0) {
for (k = 0u; k < sim->level->spriteCount; k++) {
if (sim->level->sprites[k].ptr == ptr) {
return sim->level->sprites[k].bitmap;
}
}
}
return 0;
}
// $5F40 -- one iteration of the main loop.
StTickResultE stSimTick(StSimT *sim) {
StTickResultE result = ST_TICK_CONTINUE;
if (sim->collisionPhase == 0u) {
physicsTick(sim);
fireButtonEdge(sim);
stSimFlameUpdate(sim);
padDetect(sim);
taxiSpriteCelSelect(sim);
if (levelEndCheck(sim)) {
stAudioNoise(false);
stAudioSilence();
stSimArchiveHud(sim);
return ST_TICK_LEVEL_EXIT;
}
padLandingBob(sim);
fuelTick(sim);
// $61BD passengerEventDraw only re-voices the pickup/drop-off
// messages already drawn by the fare machine.
}
stFarePostTickGate(sim);
stFareStageDispatch(sim);
stHookPerTick2(sim);
marshal(sim);
bitScrollHatch(sim);
stHookPerTick3(sim);
fuelBarHud(sim);
if (sim->collisionPhase == 0u) {
collisionDispatch(sim);
} else if (sim->collisionPhase == 1u) {
computeCollisions(sim);
collisionPhase1(sim);
} else if (sim->collisionPhase == 2u) {
computeCollisions(sim);
collisionPhase2(sim);
} else {
computeCollisions(sim);
result = collisionPhase3(sim);
if (result != ST_TICK_CONTINUE) {
return result;
}
}
hudDraw(sim);
// $4FCB runStopWatcher: any joystick input ends the demo.
if (sim->demoMode != 0u && (sim->rawInput & 0x1Fu) != 0u) {
return ST_TICK_DEMO_INPUT;
}
passengerArrTick(sim);
return ST_TICK_CONTINUE;
}