spacetaxi/stTitle.c
2026-10-05 16:47:00 -05:00

354 lines
14 KiB
C

// Space Taxi -- the title screen intro and the level-name intro
// screen, run on the same simulation state as the game so everything
// that carries over (flame parity, wave index, walk parity, the
// sprite shadows) behaves as on the C64.
//
// Title ($477A + $47B0..$4A6C): the logo flip-book and colour cycle,
// a passenger who beams in, walks to the parked cab and boards, then
// the cab lifts off under thrust. When it has left, the attract demo
// starts.
//
// Level intro ($4523..$4727): a black screen with the level name, the
// cab rising from the bottom and seven star sprites streaming out of
// the centre, until the cab reaches mid-screen.
//
// The seven star sprites are replaced by a static starfield whose
// colours are cycled -- see ST_STAR_RING_FIRST in stSim.h for why and
// how. The stars sit on the seven rays the C64's stars fly, where those
// stars are every few frames of a flight, so the field plays the
// original's paths. The cab still flies, and it is still what ends the
// intro, so the sequence keeps its length and its cue.
#include <string.h>
#include "stSim.h"
// Title sprite init tables ($4994 / $499C / $49A4 / $49AC / $49B4).
static const uint8_t kTitleCol[8] = { 0x28, 0x32, 0x00, 0x86, 0x9C, 0xA2, 0xBA, 0xD4 };
static const uint8_t kTitleMsb[8] = { 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
static const uint8_t kTitleColor[8] = { 0x06, 0x06, 0x06, 0x07, 0x07, 0x07, 0x07, 0x07 };
static const uint8_t kTitleRow[8] = { 0x84, 0x84, 0x00, 0xE2, 0xE2, 0xE2, 0xE2, 0xE2 };
static const uint8_t kTitlePtr[8] = { 0xC1, 0xC7, 0x00, 0xDB, 0xDE, 0xDF, 0xE0, 0xE1 };
// Logo flip-book frame order ($48A7) and colour cycle ($489C).
static const uint8_t kLogoFlip[6] = { 1u, 2u, 3u, 4u, 3u, 2u };
static const uint8_t kLogoColor[8] = { 0x02, 0x08, 0x07, 0x05, 0x06, 0x0E, 0x03, 0x04 };
// Intro cab drift per loop count ($470D).
static const int8_t kIntroCabDy[4] = { -2, 1, -1, 1 };
// Intro texts ($4624 / $463F).
static const uint8_t kTextDemoExit[] = "DEMO, USE JOYSTICK TO EXIT";
static const uint8_t kTextScreens[] = "THIS IS 1 OF 25 DIFFERENT SCREENS!";
#define ST_TITLE_CAB_LIFT_ROW 0x14u
#define ST_TITLE_SPARKLE_TICKS 0x5Au
#define ST_TITLE_HOVER_COL 0x28u
#define ST_INTRO_STAR_PTR 0xDAu
#define ST_INTRO_STAR_COL 0xAAu // where a star launches ($4523)
#define ST_INTRO_STAR_ROW 0x8Cu
#define ST_INTRO_STAR_DELAYS 0x20u // launch delays drawn from 0..$1F ($44F8)
#define ST_INTRO_CAB_END_ROW 0x94u
#define ST_INTRO_CAB_DX_RELOAD 0x28u
// The palette-cycled starfield. The C64 flies one star sprite along each
// of seven rays at these velocities ($450C / $4513, pixels a frame) for
// $20 frames ($4500), then launches it again from the start. The painted
// field puts a star where each flight is every ST_STAR_FRAMES_PER_STEP
// frames; the renderer lights one step of every ray at a time, so a star
// hops out along its ray. A star is drawn where the sprite's image ($DA:
// a 5x3 speck centred on pixel 11, 9 of the sprite) would be.
#define ST_INTRO_STARS 7u
#define ST_INTRO_STAR_IMAGE_X 11 // the speck's centre in its sprite
#define ST_INTRO_STAR_IMAGE_Y 9
#define ST_SCREEN_WIDTH_PX ((int16_t)(ST_SCREEN_COLS * 8u))
#define ST_SCREEN_HEIGHT_PX ((int16_t)(ST_SCREEN_ROWS * 8u))
#define ST_CELL_SHIFT 3 // 8 pixels a cell
static const int8_t kIntroStarDx[ST_INTRO_STARS] = { 0, 6, 6, 6, -6, -6, -6 };
static const int8_t kIntroStarDy[ST_INTRO_STARS] = { -4, -4, 0, 4, 4, 0, -4 };
static void addToRow(StSimT *sim, uint8_t idx, uint8_t delta);
static void logoColorCycle(StSimT *sim);
static void logoFlip(StSimT *sim);
static void paintStarfield(StSimT *sim, const uint8_t *delay);
static bool rowHasText(const StSimT *sim, uint8_t row);
// $4113
static void addToRow(StSimT *sim, uint8_t idx, uint8_t delta) {
sim->spr[idx].row = (uint8_t)(sim->spr[idx].row + delta);
}
// $4861 -- the next logo colour, onto sprites 3..7 and onto the logo
// itself. The C64 writes the colour into all 407 cells of rows 1..11,
// columns 1..37; only the cells showing the logo character take any
// colour from it (the rest are blank), so the colour is published in
// logoColor instead and the renderer applies it to the logo's palette
// slot. Nothing reads the band's colour RAM.
static void logoColorCycle(StSimT *sim) {
uint8_t color;
uint8_t k;
sim->logoColorIdx = (uint8_t)((sim->logoColorIdx + 1u) & 7u);
color = kLogoColor[sim->logoColorIdx];
sim->logoColor = color;
for (k = 3u; k < ST_HW_SPRITES; k++) {
sim->spr[k].color = color;
}
}
// $4827 -- every fourth tick copy the next flip frame ($85..$88) over
// the logo glyph; the colour advances when the ping-pong hits 4.
static void logoFlip(StSimT *sim) {
sim->logoDiv = (uint8_t)((sim->logoDiv + 1u) & 3u);
if (sim->logoDiv != 0u) {
return;
}
memcpy(stSimGlyphMut(sim, ST_LOGO_CHAR),
stSimGlyph(sim, (uint8_t)(ST_LOGO_CHAR + kLogoFlip[sim->logoCycleAux])), 8u);
stSimMarkChar(sim, ST_LOGO_CHAR);
sim->logoCycleAux++;
if (sim->logoCycleAux == 6u) {
sim->logoCycleAux = 0u;
return;
}
if (sim->logoCycleAux == 4u) {
logoColorCycle(sim);
}
}
// Paint the level-intro starfield into screen RAM, once. A star's cell is
// coloured ST_STAR_RING_FIRST + the step of its flight it marks, offset by
// its ray's launch delay so the rays fire out of step as on the C64, and is
// never touched again -- the renderer animates the warp by cycling those
// colours, which costs no dirty rows at all (ST_STAR_RING_FIRST, stSim.h).
// Step 0 is the launch point, which every ray shares, and steps off the
// screen are left out, as are rows that carry the intro's text: on the C64
// the stars fly over the text, but a cell star there reads as part of it --
// a gap in the level name, or a full stop after the demo hint.
static void paintStarfield(StSimT *sim, const uint8_t *delay) {
static const uint8_t one[2] = { ST_STAR_GLYPH, 0u }; // stSimDrawText stops below screen code 6
bool textRow[ST_SCREEN_ROWS];
int16_t originX;
int16_t originY;
int16_t x;
int16_t y;
uint8_t row;
uint8_t ray;
uint8_t step;
uint8_t slot;
// Before any star goes down, or the first star on a row would hide the
// rest of that row's.
for (row = 0u; row < ST_SCREEN_ROWS; row++) {
textRow[row] = rowHasText(sim, row);
}
originX = (int16_t)(ST_INTRO_STAR_COL - ST_SPRITE_X_ORIGIN + ST_INTRO_STAR_IMAGE_X);
originY = (int16_t)(ST_INTRO_STAR_ROW - ST_SPRITE_Y_ORIGIN + ST_INTRO_STAR_IMAGE_Y);
for (ray = 0u; ray < ST_INTRO_STARS; ray++) {
for (step = 1u; step < ST_STAR_RING_COUNT; step++) {
x = (int16_t)(originX + kIntroStarDx[ray] * (int16_t)(step * ST_STAR_FRAMES_PER_STEP));
y = (int16_t)(originY + kIntroStarDy[ray] * (int16_t)(step * ST_STAR_FRAMES_PER_STEP));
if (x < 0 || x >= ST_SCREEN_WIDTH_PX || y < 0 || y >= ST_SCREEN_HEIGHT_PX || textRow[y >> ST_CELL_SHIFT]) {
continue;
}
slot = (uint8_t)((step + delay[ray] / ST_STAR_FRAMES_PER_STEP) & (ST_STAR_RING_COUNT - 1u));
stSimDrawText(sim, (uint8_t)(x >> ST_CELL_SHIFT), (uint8_t)(y >> ST_CELL_SHIFT), one, (uint8_t)(ST_STAR_RING_FIRST + slot));
}
}
}
// True when screen row `row` holds anything but blanks.
static bool rowHasText(const StSimT *sim, uint8_t row) {
const uint8_t *cell;
uint8_t col;
cell = &sim->screen[(uint16_t)row * ST_SCREEN_COLS];
for (col = 0u; col < ST_SCREEN_COLS; col++) {
if (cell[col] != ST_CHAR_SPACE) {
return true;
}
}
return false;
}
// ---------------------------------------------------------------------------
// Public
// ---------------------------------------------------------------------------
// $4523..$4665 -- set up the level-name screen.
void stIntroEnter(StSimT *sim, const StLevelT *level) {
uint8_t delay[ST_INTRO_STARS];
uint8_t k;
sim->level = level;
sim->spr[7].ptr = 0xC0u;
sim->spriteMc1 = 0x07u;
sim->spriteMc0 = 0x02u;
sim->spr[7].enable = 1u;
sim->spr[7].col = 0xAAu;
sim->spr[7].msb = 0u;
sim->spr[7].color = 0x06u;
sim->spr[7].row = 0xE4u;
sim->introCabDxTimer = 0x14u;
sim->introCabDx = 0x01u;
for (k = 0u; k < ST_INTRO_STARS; k++) {
uint8_t idx = (uint8_t)(ST_INTRO_STARS - 1u - k);
sim->spr[idx].row = ST_INTRO_STAR_ROW;
sim->spr[idx].msb = 0u;
sim->spr[idx].col = ST_INTRO_STAR_COL;
sim->spr[idx].color = 0x07u;
sim->spr[idx].ptr = ST_INTRO_STAR_PTR;
sim->spr[idx].enable = 0u;
// $44F8: each star's launch delay. The painted field uses it to
// put the rays out of step, and the seven RNG draws MUST stay in
// any case: they advance the shared RNG and the level that
// follows draws from it.
delay[idx] = stSimRng(sim, ST_INTRO_STAR_DELAYS);
}
sim->borderColor = 0u;
sim->bgColor = 0u;
sim->multiColorMask = 0x80u;
stSimMarshal(sim);
// $40CA: blank screen.
memset(sim->screen, ST_CHAR_SPACE, ST_SCREEN_CELLS);
memset(sim->color, 0, ST_SCREEN_CELLS);
stSimDirtyAll(sim);
stSimGlyphReset(sim);
sim->charDirtyAll = true;
sim->charDirtyCount = 0u;
stSimDrawText(sim, 10u, 12u, level->name, 3u);
if (sim->demoMode != 0u && sim->postMortem == 0u) {
stSimDrawText(sim, 7u, 3u, kTextDemoExit, 5u);
stSimDrawText(sim, 3u, 24u, kTextScreens, 4u);
}
paintStarfield(sim, delay);
sim->introLoopCount = 0u;
sim->introStepCount = 0u;
}
// $4666 -- one frame of the star field; true when the cab has risen
// to mid-screen and everything is hidden again.
bool stIntroStep(StSimT *sim) {
uint8_t k;
stSimMarshal(sim);
sim->introStepCount++;
sim->introLoopCount = (uint8_t)((sim->introLoopCount + 1u) & 3u);
addToRow(sim, 7u, (uint8_t)kIntroCabDy[sim->introLoopCount]);
if ((sim->introLoopCount & 1u) == 0u) {
stSimSpriteAddX(sim, 7u, sim->introCabDx);
sim->introCabDxTimer--;
if (sim->introCabDxTimer == 0u) {
sim->introCabDxTimer = ST_INTRO_CAB_DX_RELOAD;
sim->spr[7].ptr ^= 0x1Cu;
sim->introCabDx ^= 0xFEu;
}
}
if (sim->spr[7].row != ST_INTRO_CAB_END_ROW) {
return false;
}
// $4728: everything off; $44E1 then resets the SID and the RNG.
for (k = 0u; k < ST_HW_SPRITES; k++) {
sim->spr[k].enable = 0u;
}
stSimMarshal(sim);
sim->rngT1 = 0x06u;
sim->rngT2 = 0x17u;
return true;
}
// $477A + $47B0..$47E1 -- show the title and start the intro.
void stTitleEnter(StSimT *sim, const StLevelT *title) {
uint8_t k;
sim->level = title;
memcpy(sim->screen, title->screen, ST_SCREEN_CELLS);
memcpy(sim->color, title->color, ST_SCREEN_CELLS);
stSimDirtyAll(sim);
stSimGlyphReset(sim);
sim->charDirtyAll = true;
sim->charDirtyCount = 0u;
logoColorCycle(sim);
for (k = 0u; k < ST_HW_SPRITES; k++) {
sim->spr[k].enable = 0u;
}
sim->logoCycleAux = 0u;
sim->borderColor = 0u;
sim->bgColor = 0u;
// $49BC: the cab parked on the pad, the passenger far right, the
// decoration sprites along the bottom.
for (k = 0u; k < ST_HW_SPRITES; k++) {
sim->spr[k].enable = 1u;
sim->spr[k].col = kTitleCol[k];
sim->spr[k].msb = kTitleMsb[k];
sim->spr[k].row = kTitleRow[k];
sim->spr[k].ptr = kTitlePtr[k];
sim->spr[k].color = kTitleColor[k];
}
sim->multiColorMask = 0x07u;
sim->spriteMc1 = 0x07u;
sim->spriteMc0 = 0x02u;
stSimMarshal(sim);
sim->stage = ST_STAGE_WAIT;
sim->introFrameCount = ST_TITLE_SPARKLE_TICKS;
sim->hoverXFrac = 0u;
sim->hoverXCol = ST_TITLE_HOVER_COL;
sim->decayTimer = sim->decayReload;
sim->collisionPhase = 0u;
sim->activePad = 0u;
sim->dirMask = 0u;
}
// $47E4 loop body: the intro stage machine ($4A03), the sprite flush
// and the logo animation. True once the cab has lifted off.
bool stTitleTick(StSimT *sim) {
switch (sim->stage) {
case ST_STAGE_WAIT:
// $4A17: sparkle while the countdown runs.
stFareWaveStep(sim);
sim->introFrameCount--;
if (sim->introFrameCount == 0u) {
sim->stage++;
}
break;
case ST_STAGE_WALK_TO_CAB:
// $4A24: walk in until parked at column $28.
sim->decayTimer--;
if (sim->decayTimer == 0u) {
sim->decayTimer = sim->decayReload;
stFareWalkStep(sim);
if (sim->spr[1].col == ST_TITLE_HOVER_COL && sim->spr[1].msb == 0u) {
sim->stage++;
}
}
break;
case ST_STAGE_BOARD:
// $4A45 -> $67A6: shrink into the cab.
stFareLeaveStep(sim);
break;
case ST_STAGE_RIDING:
// $4A48: lift off under UP thrust until above row $14.
addToRow(sim, 0u, 0xFEu);
sim->dirMask = 0x01u;
sim->spr[0].ptr = 0xC0u;
stSimFlameUpdate(sim);
if (sim->spr[0].row < ST_TITLE_CAB_LIFT_ROW) {
sim->stage++;
stAudioNoise(false);
}
break;
default:
break;
}
stSimMarshal(sim);
logoFlip(sim);
return sim->stage == ST_STAGE_DROP_IN;
}