340 lines
13 KiB
C
340 lines
13 KiB
C
// Space Taxi -- taxi physics.
|
|
//
|
|
// Faithful translation of the C64's $6032 per-frame physics routine
|
|
// (see MECHANICS.md "Per-frame algorithm" and "Velocity model
|
|
// (CORRECTED)"). Acceleration-based: stick deflection sets an INSTANT
|
|
// per-frame acceleration; gravity is a constant acceleration always
|
|
// added (Y, signed -- level K is anti-gravity); both feed a persistent
|
|
// velocity accumulator that integrates into position each frame.
|
|
// Release the stick and velocity persists -- the cab drifts.
|
|
//
|
|
// Edge handling MATCHES the C64's $6AED behaviour: vx reflects at
|
|
// column 23 going left or column 65 going right. The cab BOUNCES off
|
|
// the screen edges rather than stopping.
|
|
//
|
|
// Crash detection: any contact with a solid non-pad tile crashes the
|
|
// cab. Any contact with a pad-surface tile is a successful landing.
|
|
// The C64 game does the same via sprite-bg hardware collision plus
|
|
// the $7D75 trampoline predicate; we substitute explicit pad-vs-wall
|
|
// tile classification.
|
|
//
|
|
// Per-level physics templates ($7D8F-$7D96 in the C64; xAccel/yAccel/
|
|
// xGrav/yGrav fields on StLevelT in the port) make every level feel
|
|
// different -- accel ranges $0E..$40, Y-gravity ranges $F9 (-7,
|
|
// anti-gravity on level K) to $06 (heavy on levels D/E/H/M/etc.).
|
|
|
|
#include <string.h>
|
|
|
|
#include "spacetaxi.h"
|
|
|
|
JOEYLIB_SEGMENT("STAXI")
|
|
|
|
// Velocity safety clamp. With ST_SUBPIXEL = 256, ST_MAX_VX = 512 caps
|
|
// the cab at ~2 px/frame which is in line with the C64 max effective
|
|
// velocity (the C64's int16 accumulator integrates accel=14..64 over
|
|
// many frames; left uncapped it'd wrap, but real-game effective speeds
|
|
// stay in the hundreds-of-sub-units range). Earlier 127 capped at
|
|
// 0.5 px/frame, making the cab feel glued to molasses.
|
|
#define ST_MAX_VX 512
|
|
#define ST_MAX_VY 512
|
|
|
|
// Initial downward velocity injected at crash. The C64 sets $714F =
|
|
// $03 and $714E = $03 at $6A4E-$6A53 -- a 16-bit vy of $0303 = 771
|
|
// sub-units (~3 px/frame). The port's ST_MAX_VY caps the effective
|
|
// max, so we just start at that ceiling for a strong opening fall;
|
|
// gravity (the level's own yGrav) then integrates normally during
|
|
// the death anim. On anti-gravity levels (K = -7) the initial high
|
|
// downward velocity dominates for many frames before being slowed
|
|
// or reversed -- matches the C64's behavior on those levels.
|
|
#define ST_CRASH_FALL_VY ST_MAX_VY
|
|
|
|
#define ST_TAXI_W_PX 24
|
|
#define ST_TAXI_H_PX 24
|
|
|
|
// Death-animation duration. Matches the C64 sequence at $6A72/$6B24/
|
|
// $6B4C: phase-1 keeps integrating Y velocity until the cab falls to
|
|
// the floor (row >= $DA = 218), then phase-2 walks the sprite cels
|
|
// $CC..$D1 with a rate that slows each step, then phase-3 holds for
|
|
// 70 frames before the finalize (DEC lives, blank sprite). 120 host
|
|
// frames here covers the full "fall + sit" sequence at the port's
|
|
// physics scale. The cab keeps falling under gravity during the
|
|
// countdown so the visual matches the original "watch it drop".
|
|
#define ST_CRASH_ANIM_FRAMES 120u
|
|
|
|
// Edge reflection at the visible playfield edges. The C64's $6AED
|
|
// thresholds (col 23 left / col 65 in X-MSB high half) are in VIC
|
|
// sprite-X coordinates where X=24 is the left visible column -- so
|
|
// "col 23" means "cab leftedge at visible left edge". Our port has
|
|
// no VIC-X offset (X=0 IS the visible left edge), so the equivalent
|
|
// is "bounce when the cab tries to leave the visible playfield".
|
|
|
|
|
|
static bool isSolidAt(const StLevelT *level, int16_t px, int16_t py);
|
|
static bool onLandingPad(const StLevelT *level, int16_t px, int16_t py, uint8_t *outPad);
|
|
static void reflectAtEdges(StTaxiT *t);
|
|
static void respawnTaxi(StGameT *game);
|
|
|
|
|
|
static bool isSolidAt(const StLevelT *level, int16_t px, int16_t py) {
|
|
int16_t tx = (int16_t)(px / ST_TILE_PIXELS);
|
|
int16_t ty = (int16_t)(py / ST_TILE_PIXELS);
|
|
uint8_t tile;
|
|
|
|
if (tx < 0 || tx >= (int16_t)ST_TILEMAP_W) {
|
|
return true; // off-screen sides treated as walls
|
|
}
|
|
if (ty < 0 || ty >= (int16_t)ST_PLAYFIELD_ROWS) {
|
|
return true; // top/bottom out-of-field treated as walls
|
|
}
|
|
tile = level->tilemap[ty * ST_TILEMAP_W + tx];
|
|
// Tile-index convention (authored by the level designer):
|
|
// 0 = empty space
|
|
// 1..63 = solid (walls, platforms, structure)
|
|
// 64..127 = landing-pad surfaces (also solid for collision)
|
|
// 128+ = decorative non-solid (lights, signs, etc.)
|
|
return tile != 0u && tile < 128u;
|
|
}
|
|
|
|
|
|
static bool onLandingPad(const StLevelT *level, int16_t px, int16_t py, uint8_t *outPad) {
|
|
uint8_t i;
|
|
uint8_t tx;
|
|
uint8_t ty;
|
|
|
|
tx = (uint8_t)(px / ST_TILE_PIXELS);
|
|
ty = (uint8_t)(py / ST_TILE_PIXELS);
|
|
for (i = 0u; i < level->padCount; i++) {
|
|
const StPadT *p = &level->pads[i];
|
|
if (ty == p->tileY && tx >= p->tileX && tx < (uint8_t)(p->tileX + p->tileW)) {
|
|
if (outPad != NULL) {
|
|
*outPad = i;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
static void reflectAtEdges(StTaxiT *t) {
|
|
// C64 $6AED behavior translated to the port's 0..319 visible
|
|
// playfield: when vx is negative and the cab leftedge reaches 0,
|
|
// negate vx. Same on the right when leftedge reaches (320 - W).
|
|
// The cab visually touches the screen edge and bounces back;
|
|
// matches the C64's "screen-edge bumper" behavior. Y has no
|
|
// bounce -- the cab just stops at top/bottom (no real level
|
|
// throws the cab against those edges).
|
|
int32_t maxX = ((int32_t)ST_TILEMAP_W * ST_TILE_PIXELS - ST_TAXI_W_PX) * ST_SUBPIXEL;
|
|
int32_t maxY = ((int32_t)ST_PLAYFIELD_ROWS * ST_TILE_PIXELS - ST_TAXI_H_PX) * ST_SUBPIXEL;
|
|
|
|
if (t->x < 0 && t->vx < 0) {
|
|
t->vx = (int16_t)(-t->vx);
|
|
t->x = 0;
|
|
}
|
|
if (t->x > maxX && t->vx > 0) {
|
|
t->vx = (int16_t)(-t->vx);
|
|
t->x = maxX;
|
|
}
|
|
if (t->y < 0) { t->y = 0; t->vy = 0; }
|
|
if (t->y > maxY) { t->y = maxY; t->vy = 0; }
|
|
}
|
|
|
|
|
|
static void respawnTaxi(StGameT *game) {
|
|
StTaxiT *t = &game->taxi;
|
|
int32_t spawnX;
|
|
int32_t spawnY;
|
|
uint8_t i;
|
|
|
|
spawnX = (int32_t)game->level.taxiSpawnTileX * ST_TILE_PIXELS * ST_SUBPIXEL;
|
|
spawnY = (int32_t)game->level.taxiSpawnTileY * ST_TILE_PIXELS * ST_SUBPIXEL;
|
|
t->x = spawnX;
|
|
t->y = spawnY;
|
|
t->vx = 0;
|
|
t->vy = 0;
|
|
t->landed = false;
|
|
t->onPad = 0xFFu;
|
|
t->thrusting = false;
|
|
// Boot any in-flight passenger out of the cab; a respawn doesn't
|
|
// keep the fare. Waiting passengers (still on a pad) survive.
|
|
for (i = 0u; i < ST_MAX_PASSENGERS; i++) {
|
|
if (game->passengers[i].active && game->passengers[i].onboard) {
|
|
game->passengers[i].active = false;
|
|
game->passengers[i].onboard = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void stEngineReset(StGameT *game) {
|
|
StTaxiT *t = &game->taxi;
|
|
const StLevelT *L = &game->level;
|
|
|
|
memset(t, 0, sizeof(*t));
|
|
t->x = (int32_t)L->taxiSpawnTileX * ST_TILE_PIXELS * ST_SUBPIXEL;
|
|
t->y = (int32_t)L->taxiSpawnTileY * ST_TILE_PIXELS * ST_SUBPIXEL;
|
|
t->facing = ST_DIR_RIGHT;
|
|
t->onPad = 0xFFu;
|
|
|
|
// Delegated to stPassenger.c so the fare-cursor (gNextFareIdx) and
|
|
// the seed-spawn share one code path. Previously stEngineReset
|
|
// spawned fares[0] inline without advancing the cursor, which then
|
|
// caused spawnNextFare to re-spawn fare 0 instead of fare 1.
|
|
stPassengerReset(game);
|
|
}
|
|
|
|
|
|
void stEngineTick(StGameT *game) {
|
|
StTaxiT *t = &game->taxi;
|
|
const StLevelT *L = &game->level;
|
|
int32_t nx;
|
|
int32_t ny;
|
|
int16_t ax;
|
|
int16_t ay;
|
|
int16_t centerX;
|
|
int16_t feetY;
|
|
uint8_t landingPad;
|
|
|
|
// Death animation: mirror the C64 sequence -- the cab keeps
|
|
// FALLING through phase 1 ($6A72) until it hits the floor, then
|
|
// sits there through phases 2/3 before respawn. No input accepted,
|
|
// no further crash checks (otherwise hitting the floor on the way
|
|
// down would re-trigger). Mute thrust input so applyInput's
|
|
// per-frame joystick read can't keep the SFX going.
|
|
//
|
|
// Uses the LEVEL's normal gravity during integration -- the C64
|
|
// doesn't override gravity at crash, it just injects a high
|
|
// downward vy at crash entry and then lets normal physics run.
|
|
// On level K (anti-gravity = -7) the high initial vy dominates
|
|
// for many frames before being slowed/reversed; matches C64.
|
|
if (t->crashTicks > 0u) {
|
|
int32_t maxY = ((int32_t)ST_PLAYFIELD_ROWS * ST_TILE_PIXELS
|
|
- ST_TAXI_H_PX) * ST_SUBPIXEL;
|
|
|
|
t->thrusting = false;
|
|
t->vy = (int16_t)(t->vy + (int16_t)L->yGrav);
|
|
if (t->vy > ST_MAX_VY) { t->vy = ST_MAX_VY; }
|
|
if (t->vy < -ST_MAX_VY) { t->vy = -ST_MAX_VY; }
|
|
t->y += t->vy;
|
|
if (t->y > maxY) {
|
|
t->y = maxY;
|
|
t->vy = 0;
|
|
}
|
|
t->crashTicks--;
|
|
if (t->crashTicks == 0u) {
|
|
if (game->lives > 0u) {
|
|
game->lives--;
|
|
}
|
|
if (game->lives == 0u) {
|
|
game->state = ST_STATE_GAME_OVER;
|
|
} else {
|
|
respawnTaxi(game);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Step 1: instant acceleration from stick.
|
|
ax = (int16_t)((int16_t)t->thrustDx * (int16_t)L->xAccel);
|
|
ay = (int16_t)((int16_t)t->thrustDy * (int16_t)L->yAccel);
|
|
if (!t->thrusting) {
|
|
ax = 0;
|
|
ay = 0;
|
|
}
|
|
|
|
// Step 2: integrate accel + per-level gravity into velocity.
|
|
// Gravity is int8 signed so a level can pull upward (level K).
|
|
t->vx = (int16_t)(t->vx + ax + (int16_t)L->xGrav);
|
|
t->vy = (int16_t)(t->vy + ay + (int16_t)L->yGrav);
|
|
|
|
if (t->thrusting) {
|
|
// Thrust-flame cel cycling. The asset (genPlaceholderArt.py)
|
|
// authors 3 thrust-flame variants at cels 1..3. Advance the
|
|
// counter every frame; cel selection in stRender divides by
|
|
// ST_THRUST_CEL_TICKS so the visible animation runs at ~10 Hz.
|
|
t->thrustFrame++;
|
|
if (t->thrustFrame >= (uint8_t)(ST_THRUST_CEL_COUNT * ST_THRUST_CEL_TICKS)) {
|
|
t->thrustFrame = 0u;
|
|
}
|
|
} else {
|
|
t->thrustFrame = 0u;
|
|
}
|
|
|
|
// Safety clamp on the velocity accumulator -- without this a long
|
|
// fall under gravity (or sustained one-way thrust against no
|
|
// collision) lets vx/vy wrap int16_t and reverse sign.
|
|
if (t->vx > ST_MAX_VX) { t->vx = ST_MAX_VX; }
|
|
if (t->vx < -ST_MAX_VX) { t->vx = -ST_MAX_VX; }
|
|
if (t->vy > ST_MAX_VY) { t->vy = ST_MAX_VY; }
|
|
if (t->vy < -ST_MAX_VY) { t->vy = -ST_MAX_VY; }
|
|
|
|
nx = t->x + t->vx;
|
|
ny = t->y + t->vy;
|
|
|
|
// Wall collision (X-axis): sample the cab's leading edge at three
|
|
// Y rows (top, middle, bottom). If any sample hits a solid cell,
|
|
// undo X movement and zero vx so the cab stops against the wall.
|
|
{
|
|
int16_t leadX = (t->vx > 0)
|
|
? (int16_t)((nx >> ST_SUBPIXEL_SHIFT) + ST_TAXI_W_PX - 1)
|
|
: (int16_t)(nx >> ST_SUBPIXEL_SHIFT);
|
|
int16_t topY = (int16_t)(t->y >> ST_SUBPIXEL_SHIFT);
|
|
int16_t midY = (int16_t)(topY + ST_TAXI_H_PX / 2);
|
|
int16_t botY = (int16_t)(topY + ST_TAXI_H_PX - 1);
|
|
if (isSolidAt(L, leadX, topY) ||
|
|
isSolidAt(L, leadX, midY) ||
|
|
isSolidAt(L, leadX, botY)) {
|
|
nx = t->x;
|
|
t->vx = 0;
|
|
}
|
|
}
|
|
|
|
// Y-axis collision. The C64 game has no velocity threshold: any
|
|
// sprite-vs-background contact crashes the cab unless the cab is
|
|
// touching a pad surface (in which case it's a successful landing
|
|
// regardless of descent speed). We do the same via tile lookup.
|
|
feetY = (int16_t)((ny >> ST_SUBPIXEL_SHIFT) + ST_TAXI_H_PX - 1);
|
|
centerX = (int16_t)((nx >> ST_SUBPIXEL_SHIFT) + ST_TAXI_W_PX / 2);
|
|
|
|
if (t->vy > 0) {
|
|
if (isSolidAt(L, centerX, feetY)) {
|
|
if (onLandingPad(L, centerX, feetY, &landingPad)) {
|
|
ny = (int32_t)(feetY / ST_TILE_PIXELS) * ST_TILE_PIXELS * ST_SUBPIXEL
|
|
- (int32_t)ST_TAXI_H_PX * ST_SUBPIXEL;
|
|
t->vx = 0;
|
|
t->vy = 0;
|
|
t->landed = true;
|
|
t->onPad = landingPad;
|
|
} else {
|
|
stAudioSfxCrash();
|
|
// Enter the falling-death state. Zero vx so the cab
|
|
// doesn't drift sideways during the fall, and inject
|
|
// a high downward vy matching the C64's $6A4E-$6A53
|
|
// setup (which writes $0303 = 771 sub-units into
|
|
// $714E/$714F regardless of pre-impact state). The
|
|
// crashTicks gate at the top of stEngineTick handles
|
|
// gravity integration + respawn from here.
|
|
t->vx = 0;
|
|
t->vy = ST_CRASH_FALL_VY;
|
|
t->thrusting = false;
|
|
t->crashTicks = ST_CRASH_ANIM_FRAMES;
|
|
}
|
|
} else {
|
|
t->landed = false;
|
|
t->onPad = 0xFFu;
|
|
}
|
|
} else if (t->vy < 0) {
|
|
// Ascending: head hits ceiling? Stop vertical motion, leave
|
|
// horizontal alone so the cab can drift along the underside.
|
|
int16_t headY = (int16_t)(ny >> ST_SUBPIXEL_SHIFT);
|
|
if (isSolidAt(L, centerX, headY)) {
|
|
ny = t->y;
|
|
t->vy = 0;
|
|
}
|
|
t->landed = false;
|
|
}
|
|
|
|
t->x = nx;
|
|
t->y = ny;
|
|
reflectAtEdges(t);
|
|
}
|