diff -ruN a/assets/genC64Data.py b/assets/genC64Data.py --- a/assets/genC64Data.py 2026-10-05 14:05:44.479069311 -0500 +++ b/assets/genC64Data.py 2026-10-05 14:12:10.086072322 -0500 @@ -67,6 +67,8 @@ ("uint8_t stC64RngByte(uint8_t idx);", "kStRngTable[idx]"), ("const uint8_t *stC64SfxProgram(uint8_t idx);", "kStSfxPrograms[idx]"), ("uint8_t stC64FlameCel(uint8_t dirMask);", "kStFlameCelByDirMask[dirMask]"), + ("int8_t stC64IntroStarDx(uint8_t idx);", "kStIntroStarDx[idx]"), + ("int8_t stC64IntroStarDy(uint8_t idx);", "kStIntroStarDy[idx]"), ] @@ -115,6 +117,10 @@ h.append("// 8=RIGHT); 0 = no cel for that combination.") h.append("extern const uint8_t kStFlameCelByDirMask[16];") h.append("") + h.append("// Level-intro star velocities per sprite 0..6 ($450C / $4513).") + h.append("extern const int8_t kStIntroStarDx[7];") + h.append("extern const int8_t kStIntroStarDy[7];") + h.append("") h.append("// Cross-segment-safe accessors (see stC64Data.c): reach the tables above") h.append("// by an inter-segment call on the IIgs, where a direct 2-byte data") h.append("// reference from another bank is not relocatable.") @@ -160,6 +166,11 @@ c.append(c_bytes(flame)) c.append("};") c.append("") + dx = [b - 256 if b >= 128 else b for b in raw[0x450C:0x450C + 7]] + dy = [b - 256 if b >= 128 else b for b in raw[0x4513:0x4513 + 7]] + c.append("const int8_t kStIntroStarDx[7] = { " + ", ".join(str(v) for v in dx) + " };") + c.append("const int8_t kStIntroStarDy[7] = { " + ", ".join(str(v) for v in dy) + " };") + c.append("") c.append("") c.append("// ---------------------------------------------------------------------------") c.append("// Accessors. On the IIgs the program is split across banks and a 2-byte") diff -ruN a/spacetaxi.c b/spacetaxi.c --- a/spacetaxi.c 2026-10-05 14:05:44.477069267 -0500 +++ b/spacetaxi.c 2026-10-05 14:12:10.087072342 -0500 @@ -925,8 +925,6 @@ static void startIntro(void) { stIntroEnter(&gSim, &gLevel); stRenderSceneChanged(&gSim); - // After the scene change, which turns it back off. - stRenderIntroStars(); // $459C: the intro's tune plays under the fly-in, not blocking; the // C64 leaves it to the IRQ player and stops it at $4709. stAudioMusic(ST_SONG_INTRO, true); diff -ruN a/spacetaxi.h b/spacetaxi.h --- a/spacetaxi.h 2026-10-05 14:05:44.478069289 -0500 +++ b/spacetaxi.h 2026-10-05 14:12:10.088072362 -0500 @@ -52,10 +52,6 @@ // cycle are palette writes instead of repainting 103 (and 407) cells. // Call after stRenderSceneChanged, which turns it back off. void stRenderTitleLogo(StSimT *sim); -// Level intro only: cycle the starfield's ring palette slots so the warp -// animates without a single pixel moving. Call after -// stRenderSceneChanged, which turns it back off. -void stRenderIntroStars(void); // Fill the stage with a two-line message and present it. Used when an // asset the game ships with will not load, so a player sees why the // game stopped instead of a silently degraded ride. diff -ruN a/stC64Data.c b/stC64Data.c --- a/stC64Data.c 2026-10-05 14:05:44.475069223 -0500 +++ b/stC64Data.c 2026-10-05 14:12:10.085072302 -0500 @@ -1322,6 +1322,9 @@ 0x00, 0xD8, 0xD4, 0x00, 0xD5, 0xD2, 0xD1, 0x00, 0xD7, 0xD3, 0xD6, 0x00, 0x00, 0x00, 0x00, 0x00, }; +const int8_t kStIntroStarDx[7] = { 0, 6, 6, 6, -6, -6, -6 }; +const int8_t kStIntroStarDy[7] = { -4, -4, 0, 4, 4, 0, -4 }; + // --------------------------------------------------------------------------- // Accessors. On the IIgs the program is split across banks and a 2-byte @@ -1354,3 +1357,13 @@ uint8_t stC64FlameCel(uint8_t dirMask) { return kStFlameCelByDirMask[dirMask]; } + + +int8_t stC64IntroStarDx(uint8_t idx) { + return kStIntroStarDx[idx]; +} + + +int8_t stC64IntroStarDy(uint8_t idx) { + return kStIntroStarDy[idx]; +} diff -ruN a/stC64Data.h b/stC64Data.h --- a/stC64Data.h 2026-10-05 14:05:44.476069245 -0500 +++ b/stC64Data.h 2026-10-05 14:12:10.086072322 -0500 @@ -44,6 +44,10 @@ // 8=RIGHT); 0 = no cel for that combination. extern const uint8_t kStFlameCelByDirMask[16]; +// Level-intro star velocities per sprite 0..6 ($450C / $4513). +extern const int8_t kStIntroStarDx[7]; +extern const int8_t kStIntroStarDy[7]; + // Cross-segment-safe accessors (see stC64Data.c): reach the tables above // by an inter-segment call on the IIgs, where a direct 2-byte data // reference from another bank is not relocatable. @@ -52,5 +56,7 @@ uint8_t stC64RngByte(uint8_t idx); const uint8_t *stC64SfxProgram(uint8_t idx); uint8_t stC64FlameCel(uint8_t dirMask); +int8_t stC64IntroStarDx(uint8_t idx); +int8_t stC64IntroStarDy(uint8_t idx); #endif diff -ruN a/stCels.c b/stCels.c --- a/stCels.c 2026-10-05 14:06:02.361459928 -0500 +++ b/stCels.c 2026-10-05 14:12:10.090072402 -0500 @@ -2,9 +2,9 @@ // See stCels.h. Ordered exactly as stRenderPrewarm built the cels: the // cab set (flying, wreck and the intro decorations) and passenger set in // multicolour at the constant level colours (cab/passenger 6, shared -// $D025/$D026 = 2/7), then the flame set in multicolour colour 7. No -// intro star: the level intro paints its starfield into the character -// screen (stTitle.c), so the star sprite is never drawn. +// $D025/$D026 = 2/7), then the flame set in multicolour colour 7. The +// intro star is not here: a bank's cels share one size, and the star is +// built cropped to its one tile at boot (prewarmStar in stRender.c). #include "stCels.h" diff -ruN a/stRender.c b/stRender.c --- a/stRender.c 2026-10-05 14:07:17.861084192 -0500 +++ b/stRender.c 2026-10-05 14:12:10.098072560 -0500 @@ -37,7 +37,7 @@ // The IIgs reaches its globals DBR-relative, so all of BSS must fit the // entry bank below the I/O window; the caches are sized down there. The // sprite cache must still hold every cel stRenderPrewarm builds (the -// cab, exhaust and passenger sets) or a cel change mid-play rebuilds +// cab, exhaust and passenger sets and the intro star) or a cel change mid-play rebuilds // and recompiles a sprite. Glyphs are direct-mapped by character, so // the count is a power of two. // Room for the baked cab/passenger/exhaust set (36) plus the largest @@ -96,9 +96,6 @@ // Title logo (stRenderTitleLogo): all four flip-book frames in one // tile, animated by rewriting logoSlotMask[]'s palette slots. bool logoMode; - bool starMode; // level intro: cycling the starfield - uint8_t starPhase; // which ring the bright band is on - uint8_t starTick; // frames until the band steps outward bool logoTileOk; // the live glyph is one of the frames uint8_t logoSlotCount; uint8_t logoSlotMask[ST_LOGO_PAL_SLOTS]; // frames each slot is lit in @@ -141,16 +138,6 @@ // four of these are used; the rest are headroom if the frames change. static const uint8_t kLogoPalSlot[ST_LOGO_PAL_SLOTS] = { 4u, 8u, 9u, 10u, 13u, 14u, 15u }; -// Level-intro warp: how a star ring looks at each distance BEHIND the -// bright band, as C64 colour indices (white, light grey, grey, dark -// grey, then black). A ring this far behind the band or further is -// unlit, so a lit head with a short fading tail sweeps outward and the -// rest of the field stays dark. -static const uint8_t kStarRamp[] = { 1u, 15u, 12u, 11u }; -#define ST_STAR_RAMP_LEN (sizeof(kStarRamp) / sizeof(kStarRamp[0])) -// Frames per ring step. The intro runs at the game's 30 Hz tick, so a -// step every other frame sweeps the eight rings in about half a second. -#define ST_STAR_STEP_FRAMES 2u static StSpriteCacheT *cachedSprite(StSimT *sim, uint8_t idx); @@ -175,8 +162,8 @@ static void paintCells(jlSurfaceT *stage, StSimT *sim); static void pasteCell(jlSurfaceT *stage, const StSimT *sim, uint16_t cell, uint8_t bx, uint8_t by); static bool prewarmReport(uint16_t expect); +static void prewarmStar(StSimT *sim); static bool spriteOnScreen(int16_t px, int16_t py); -static void starPaletteApply(void); static void tileFromChunky(jlTileT *out, const uint8_t *chunky); static bool tileHasPixels(const uint8_t *chunky); @@ -677,29 +664,6 @@ } -// Light the starfield's ring colours for the current phase: the ring -// the band is on takes kStarRamp[0], the ones just inside it the rest -// of the ramp, and everything else the background. One palette write -// per step and not a single pixel moves -- that is the whole point of -// ST_STAR_RING_FIRST (stSim.h). -static void starPaletteApply(void) { - uint16_t pal[SURFACE_COLORS_PER_PALETTE]; - uint16_t color; - uint8_t behind; - uint8_t ring; - - jlPaletteGet(gRender.stage, 0u, pal); - for (ring = 0u; ring < ST_STAR_RING_COUNT; ring++) { - // How far this ring sits behind the band, wrapping so the band - // re-enters at the centre as it leaves the edge. - behind = (uint8_t)((gRender.starPhase - ring) & (ST_STAR_RING_COUNT - 1u)); - color = (behind < ST_STAR_RAMP_LEN) ? kC64Palette[kStarRamp[behind]] : kC64Palette[0]; - pal[ST_STAR_RING_FIRST + ring] = color; - } - jlPaletteSet(gRender.stage, 0u, pal); -} - - static bool spriteOnScreen(int16_t px, int16_t py) { return px < SURFACE_WIDTH && py < SURFACE_HEIGHT && px > -ST_SPRITE_W && py > -(int16_t)ST_SPRITE_PX; } @@ -769,17 +733,6 @@ } } } - if (gRender.starMode) { - // The warp is the only thing animating here, and it animates in - // the palette: no cells change, no sprite moves for it, so the - // present that ends this frame copies no pixels at all. - gRender.starTick++; - if (gRender.starTick >= ST_STAR_STEP_FRAMES) { - gRender.starTick = 0u; - gRender.starPhase = (uint8_t)((gRender.starPhase + 1u) & (ST_STAR_RING_COUNT - 1u)); - starPaletteApply(); - } - } collectGlyphs(sim); dirtyBands(sim); for (k = 0u; k < ST_HW_SPRITES; k++) { @@ -1148,7 +1101,7 @@ static const uint8_t kCabPtrs[] = { 0xC0, 0xC1, 0xDC, 0xDD, 0xCC, 0xCD, 0xCE, 0xCF, 0xD0, 0xD1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8 }; static const uint8_t kPassPtrs[] = { 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xD9 }; static const uint8_t kFlamePtrs[] = { 0xD8, 0xD4, 0xD5, 0xD2, 0xD1, 0xD7, 0xD3, 0xD6 }; - uint8_t total = (uint8_t)(sizeof(kCabPtrs) + sizeof(kPassPtrs) + sizeof(kFlamePtrs)); + uint8_t total = (uint8_t)(sizeof(kCabPtrs) + sizeof(kPassPtrs) + sizeof(kFlamePtrs) + 1u); uint8_t done = 0u; bool ok; uint8_t k; @@ -1158,7 +1111,8 @@ // runtime build below on any port without a matching .spc. if (loadPrecompiledCels()) { ST_LOG("spacetaxi: cels from sprites/staxicels.spc"); - return prewarmReport(kStCelCount); + prewarmStar(sim); + return prewarmReport((uint16_t)(kStCelCount + 1u)); } ST_LOG("spacetaxi: ! no usable sprites/staxicels.spc -- JIT prewarm"); saved = sim->frame; @@ -1189,6 +1143,7 @@ } sim->frame = saved; gPrewarming = false; + prewarmStar(sim); ok = prewarmReport(total); jlFillRect(stage, 60, 110, 200u, 6u, 0u); return ok; @@ -1231,15 +1186,32 @@ } +// The intro star is built and compiled here rather than baked: a bank's +// cels all share one size (3x3 tiles), and the star's nine pixels fit +// one tile, so a baked star would save, draw, restore and present nine +// times its area -- seven times a frame. A moving (idx <= 2) cel, so it +// compiles; the interpreter cannot keep up with seven of them. +static void prewarmStar(StSimT *sim) { + StFrameT saved = sim->frame; + + gPrewarming = true; + sim->frame.multiMask = 0u; + sim->frame.ptr[2] = ST_INTRO_STAR_PTR; + sim->frame.color[2] = ST_INTRO_STAR_COLOR; + (void)cachedSprite(sim, 2u); + sim->frame = saved; + gPrewarming = false; +} + + // A new scene: nothing drawn is valid any more. void stRenderSceneChanged(StSimT *sim) { uint8_t k; - if (gRender.logoMode || gRender.starMode) { + if (gRender.logoMode) { // The borrowed slots go back to being C64 colours. gRender.logoMode = false; gRender.logoTileOk = false; - gRender.starMode = false; jlPaletteSet(gRender.stage, 0u, kC64Palette); } for (k = 0u; k < ST_HW_SPRITES; k++) { @@ -1269,18 +1241,6 @@ } -// Turn on the level intro's palette-cycled starfield. Call it after -// stRenderSceneChanged (which turns it back off) on entry to the level -// intro. The cells were painted by stIntroEnter; from here the warp is -// one palette write every ST_STAR_STEP_FRAMES frames. -void stRenderIntroStars(void) { - gRender.starMode = true; - gRender.starPhase = 0u; - gRender.starTick = 0u; - starPaletteApply(); -} - - void stRenderShutdown(void) { dropCache(); if (gRender.scratch != 0) { diff -ruN a/stSim.h b/stSim.h --- a/stSim.h 2026-10-05 14:05:53.055256946 -0500 +++ b/stSim.h 2026-10-05 14:12:10.089072382 -0500 @@ -82,42 +82,16 @@ #define ST_CHAR_ELECTROID 0x6Fu // level O's scrolling barrier glyph #define ST_FARE_DEST_UP 0x0Bu // $7D8E: the fare aboard wants UP -// ---- level-intro starfield -------------------------------------------- +// ---- level-intro star warp -------------------------------------------- // -// The C64's level intro streams seven star SPRITES out of the centre of -// the screen. That is the most expensive thing this port asks a present -// for: eight sprites are save-under drawn and restored every frame, and -// because a row's dirty band is one min..max interval, two stars far -// apart on the same row widen that row to nearly full width. Radially -// spreading stars maximise exactly that, so the warp ends up presenting -// close to the whole framebuffer every frame -- which is what makes it -// the slowest thing the IIgs does, and wasted work everywhere else. -// -// So the warp is not animated in pixels at all. The starfield is painted -// ONCE into screen RAM and animated by cycling the colours its cells are -// drawn in -- the same trick the title logo already uses -// (stRenderTitleLogo). A frame costs one palette upload and NO dirty -// pixel rows. -// -// The contract between the two halves is just this colour range: -// stTitle.c colours a star's cell ST_STAR_RING_FIRST + ring (ring 0 is -// the centre), and the renderer cycles that range. These are C64 colour -// indices the intro screen never uses -- it draws in 0, 3 and, in demo -// mode, 4 and 5; the cab sprite adds 2, 6 and 7. -#define ST_STAR_RING_FIRST 8u -#define ST_STAR_RING_COUNT 8u - -// The renderer wraps the sweep's phase with a mask rather than a -// modulo, so the ring count has to stay a power of two. Changing it to -// something else must fail to compile here instead of quietly making -// the warp skip rings. -typedef char stStarRingCountIsPow2[((ST_STAR_RING_COUNT & (ST_STAR_RING_COUNT - 1u)) == 0u) ? 1 : -1]; -// Ring 0 is the centre, which in a warp is the VANISHING POINT: those -// stars are the far ones and get the small glyph. The big glyph belongs -// to the outer rings, the stars about to rush past the viewer. -#define ST_STAR_GLYPH_NEAR 0x2Au // '*', the outer rings -#define ST_STAR_GLYPH_FAR 0x2Eu // '.', the inner rings -#define ST_STAR_FAR_RINGS 5u // rings 0..4 are still distant +// The C64's level intro ($4666) streams seven hires star sprites -- +// hardware sprites 0..6, all showing block $DA -- out of the centre of +// the screen: each waits out a random spawn delay ($44F8), then flies on +// its own fixed velocity ($450C / $4513) for $20 frames ($4500) and +// respawns near the centre. +#define ST_INTRO_STARS 7u +#define ST_INTRO_STAR_PTR 0xDAu // a hires 5x3-pixel star +#define ST_INTRO_STAR_COLOR 0x07u // VIC sprite block pointers the game selects by name. The cab's bit 0 is // the landing gear, so each facing is a pair. @@ -423,6 +397,10 @@ // 407 cells of colour RAM); part of the picture, so it survives the // same way colour RAM does. uint8_t logoColor; + // $4500: frames until each intro star respawns. Neither $5EC4 nor the + // intro ($4523 seeds only the spawn delays) clears it, so a star whose + // delay draws 0 flies on whatever the previous intro left here. + uint8_t starReload[ST_INTRO_STARS]; // ---- cleared by stSimNewGame from here down ---- @@ -550,6 +528,11 @@ uint8_t introLoopCount; // $4508 uint8_t introCabDx; // $450A uint8_t introCabDxTimer; // $450B + uint8_t starTimer[ST_INTRO_STARS]; // $44F8: frames until the star first shows + // The 6502 carry flag as the $4666 star loop finds it. The respawn + // column is an ADC that never clears it first ($4684), so the flag + // left by whatever ran before is part of where a star appears. + uint8_t introCarry; uint8_t spriteMc0; // $D025 uint8_t spriteMc1; // $D026 diff -ruN a/stTitle.c b/stTitle.c --- a/stTitle.c 2026-10-05 14:05:44.474069201 -0500 +++ b/stTitle.c 2026-10-05 14:12:10.084072282 -0500 @@ -11,11 +11,6 @@ // 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 cab still flies, and it is still what ends the intro, so the -// sequence keeps its length and its cue; only the warp changes. #include @@ -39,48 +34,36 @@ #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_START_ROW 0x8Cu +#define ST_INTRO_STAR_START_COL 0xAAu +#define ST_INTRO_STAR_DELAY 0x20u // $4080 range of the spawn delay +#define ST_INTRO_STAR_RELOAD 0x20u // frames a star flies before respawning +// $467F..$4694: a respawning star's position comes from star 0's row -- +// its low three bits pick the column, bits 4..5 the row. +#define ST_INTRO_SPAWN_COL 0xA6u +#define ST_INTRO_SPAWN_COL_BITS 0x07u +#define ST_INTRO_SPAWN_ROW 0x89u +#define ST_INTRO_SPAWN_ROW_BITS 0x30u +#define ST_INTRO_SPAWN_SHIFT 4u +#define ST_INTRO_CAB_START_ROW 0xE4u #define ST_INTRO_CAB_END_ROW 0x94u #define ST_INTRO_CAB_DX_RELOAD 0x28u -// The palette-cycled starfield. Seven stars per ring so the bright band -// stays the same weight all the way out, placed at fixed pseudo-random -// angles and clear of the three lines the intro screen writes (the level -// name on row 12, and the demo hints on rows 3 and 24). -typedef struct { - uint8_t col; - uint8_t row; - uint8_t ring; -} StStarT; - -static const StStarT kIntroStar[] = { - { 18u, 10u, 0u }, { 19u, 10u, 0u }, { 17u, 11u, 0u }, { 18u, 13u, 0u }, - { 21u, 13u, 0u }, { 22u, 14u, 0u }, { 18u, 15u, 0u }, { 18u, 8u, 1u }, - { 19u, 8u, 1u }, { 14u, 10u, 1u }, { 25u, 14u, 1u }, { 21u, 15u, 1u }, - { 25u, 15u, 1u }, { 20u, 17u, 1u }, { 26u, 7u, 2u }, { 12u, 10u, 2u }, - { 29u, 11u, 2u }, { 11u, 13u, 2u }, { 29u, 14u, 2u }, { 21u, 17u, 2u }, - { 23u, 18u, 2u }, { 21u, 4u, 3u }, { 24u, 4u, 3u }, { 11u, 8u, 3u }, - { 12u, 8u, 3u }, { 10u, 10u, 3u }, { 30u, 16u, 3u }, { 14u, 17u, 3u }, - { 15u, 2u, 4u }, { 8u, 8u, 4u }, { 32u, 9u, 4u }, { 9u, 17u, 4u }, - { 32u, 17u, 4u }, { 32u, 18u, 4u }, { 24u, 22u, 4u }, { 16u, 1u, 5u }, - { 28u, 1u, 5u }, { 31u, 2u, 5u }, { 36u, 13u, 5u }, { 6u, 15u, 5u }, - { 4u, 16u, 5u }, { 5u, 19u, 5u }, { 8u, 0u, 6u }, { 30u, 0u, 6u }, - { 1u, 8u, 6u }, { 2u, 12u, 6u }, { 37u, 16u, 6u }, { 7u, 21u, 6u }, - { 32u, 22u, 6u }, { 0u, 4u, 7u }, { 39u, 7u, 7u }, { 0u, 10u, 7u }, - { 38u, 20u, 7u }, { 38u, 21u, 7u }, { 4u, 22u, 7u }, { 35u, 22u, 7u }, -}; -#define ST_INTRO_STAR_COUNT (sizeof(kIntroStar) / sizeof(kIntroStar[0])) - - -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); - - -// $4113 -static void addToRow(StSimT *sim, uint8_t idx, uint8_t delta) { - sim->spr[idx].row = (uint8_t)(sim->spr[idx].row + delta); + +static uint8_t addToRow(StSimT *sim, uint8_t idx, uint8_t delta); +static void logoColorCycle(StSimT *sim); +static void logoFlip(StSimT *sim); +static void starRespawn(StSimT *sim, uint8_t idx, uint8_t carry); +static void starsStep(StSimT *sim); + + +// $4113 -- CLC; ADC row. The carry it leaves is the 6502 flag the star +// loop goes on with (see introCarry in stSim.h). +static uint8_t addToRow(StSimT *sim, uint8_t idx, uint8_t delta) { + uint16_t row = (uint16_t)((uint16_t)sim->spr[idx].row + delta); + + sim->spr[idx].row = (uint8_t)row; + return (uint8_t)(row >> 8); } @@ -124,20 +107,47 @@ } -// Paint the level-intro starfield into screen RAM, once. A star's cell -// is coloured ST_STAR_RING_FIRST + its ring and never touched again -- -// the renderer animates the warp by cycling those colours, which costs -// no dirty rows at all. See ST_STAR_RING_FIRST in stSim.h. -static void paintStarfield(StSimT *sim) { - const StStarT *star; - uint8_t one[2]; - uint8_t k; - - one[1] = 0u; // stSimDrawText stops below screen code 6 - for (k = 0u; k < (uint8_t)ST_INTRO_STAR_COUNT; k++) { - star = &kIntroStar[k]; - one[0] = (star->ring < ST_STAR_FAR_RINGS) ? ST_STAR_GLYPH_FAR : ST_STAR_GLYPH_NEAR; - stSimDrawText(sim, star->col, star->row, one, (uint8_t)(ST_STAR_RING_FIRST + star->ring)); +// $467A -- a star (re)appears near the centre, placed from star 0's +// row. The column's ADC adds the carry the loop arrived with; the row's +// adds the bit the last LSR shifted out, which the mask keeps clear, and +// its sum stays below $100, so the loop goes on with the carry clear. +static void starRespawn(StSimT *sim, uint8_t idx, uint8_t carry) { + uint8_t seed = sim->spr[0].row; + + sim->spr[idx].msb = 0u; + sim->spr[idx].col = (uint8_t)((seed & ST_INTRO_SPAWN_COL_BITS) + ST_INTRO_SPAWN_COL + carry); + sim->spr[idx].row = (uint8_t)(((seed & ST_INTRO_SPAWN_ROW_BITS) >> ST_INTRO_SPAWN_SHIFT) + ST_INTRO_SPAWN_ROW); + sim->starReload[idx] = ST_INTRO_STAR_RELOAD; +} + + +// $4662..$46A2 -- the seven stars, sprite 6 down to sprite 0. A star +// counts down its spawn delay and appears ($4675); from then on it flies +// on its own velocity ($4711) and respawns when its reload runs out. +// The carry threads through the loop exactly as the 6502 leaves it: a +// flight's row add sets it, a respawn clears it, a waiting star keeps it. +static void starsStep(StSimT *sim) { + uint8_t carry = sim->introCarry; + uint8_t k; + + for (k = 0u; k < ST_INTRO_STARS; k++) { + uint8_t idx = (uint8_t)(ST_INTRO_STARS - 1u - k); + if (sim->starTimer[idx] == 0u) { + stSimSpriteAddX(sim, idx, (uint8_t)stC64IntroStarDx(idx)); + carry = addToRow(sim, idx, (uint8_t)stC64IntroStarDy(idx)); + sim->starReload[idx]--; + if (sim->starReload[idx] == 0u) { + starRespawn(sim, idx, carry); + carry = 0u; + } + continue; + } + sim->starTimer[idx]--; + if (sim->starTimer[idx] == 0u) { + sim->spr[idx].enable = 1u; + starRespawn(sim, idx, carry); + carry = 0u; + } } } @@ -158,23 +168,20 @@ sim->spr[7].col = 0xAAu; sim->spr[7].msb = 0u; sim->spr[7].color = 0x06u; - sim->spr[7].row = 0xE4u; + sim->spr[7].row = ST_INTRO_CAB_START_ROW; sim->introCabDxTimer = 0x14u; sim->introCabDx = 0x01u; - for (k = 0u; k < 7u; k++) { - uint8_t idx = (uint8_t)(6u - k); - sim->spr[idx].row = 0x8Cu; + 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_START_ROW; sim->spr[idx].msb = 0u; - sim->spr[idx].col = 0xAAu; - sim->spr[idx].color = 0x07u; + sim->spr[idx].col = ST_INTRO_STAR_START_COL; + sim->spr[idx].color = ST_INTRO_STAR_COLOR; sim->spr[idx].ptr = ST_INTRO_STAR_PTR; sim->spr[idx].enable = 0u; - // $44F8/$4500 held each star's spawn timer and reload. Nothing - // reads them now that the warp is painted rather than flown, but - // the seven RNG draws MUST stay: they advance the shared RNG and - // the level that follows draws from it, so dropping them would - // change the game that comes after the intro. - (void)stSimRng(sim, 0x20u); + // These draws also advance the shared RNG the level that follows + // draws from, so the next ride depends on them too. + sim->starTimer[idx] = stSimRng(sim, ST_INTRO_STAR_DELAY); } sim->borderColor = 0u; sim->bgColor = 0u; @@ -188,23 +195,26 @@ sim->charDirtyAll = true; sim->charDirtyCount = 0u; stSimDrawText(sim, 10u, 12u, level->name, 3u); - paintStarfield(sim); if (sim->demoMode != 0u && sim->postMortem == 0u) { stSimDrawText(sim, 7u, 3u, kTextDemoExit, 5u); stSimDrawText(sim, 3u, 24u, kTextScreens, 4u); } sim->introLoopCount = 0u; + // $4662 is reached from $41C2's RTS, whose CMP #$06 on the text's + // zero terminator leaves the carry clear. + sim->introCarry = 0u; } -// $4666 -- one frame of the star field; true when the cab has risen +// $4662 -- 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; + starsStep(sim); stSimMarshal(sim); sim->introLoopCount = (uint8_t)((sim->introLoopCount + 1u) & 3u); - addToRow(sim, 7u, (uint8_t)kIntroCabDy[sim->introLoopCount]); + (void)addToRow(sim, 7u, (uint8_t)kIntroCabDy[sim->introLoopCount]); if ((sim->introLoopCount & 1u) == 0u) { stSimSpriteAddX(sim, 7u, sim->introCabDx); sim->introCabDxTimer--; @@ -214,6 +224,9 @@ sim->introCabDx ^= 0xFEu; } } + // $46FC: CMP #$94. Its carry is what the next frame's star loop + // starts with; the cab is still above the end row, so it is set. + sim->introCarry = (sim->spr[7].row >= ST_INTRO_CAB_END_ROW) ? 1u : 0u; if (sim->spr[7].row != ST_INTRO_CAB_END_ROW) { return false; } @@ -300,7 +313,7 @@ break; case ST_STAGE_RIDING: // $4A48: lift off under UP thrust until above row $14. - addToRow(sim, 0u, 0xFEu); + (void)addToRow(sim, 0u, 0xFEu); sim->dirMask = 0x01u; sim->spr[0].ptr = 0xC0u; stSimFlameUpdate(sim);