spacetaxi/patches/spriteWarp.patch
2026-10-05 18:50:09 -05:00

617 lines
27 KiB
Diff

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 <string.h>
@@ -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);