More bug fixing and optimizations.
This commit is contained in:
parent
efa9fecad9
commit
a05958bd86
7 changed files with 338 additions and 178 deletions
|
|
@ -4,8 +4,9 @@
|
|||
# exact sprite-to-background collision bits), the standard sprite
|
||||
# bitmaps (cab, passenger, flame, warp, death, intro star, leaving
|
||||
# cels), the demo-mode RNG table ($446A), the SID SFX programs (the
|
||||
# 9-byte blocks fed to $42E9), the engine-flame pointer table ($6DB0)
|
||||
# and the level-intro star velocity tables ($450C/$4513).
|
||||
# 9-byte blocks fed to $42E9) and the engine-flame pointer table ($6DB0).
|
||||
# (The level-intro star velocity tables, $450C/$4513, went with the moving
|
||||
# warp; claudeDocs/spacetaxiSpriteWarp.patch puts them back.)
|
||||
#
|
||||
# Everything comes straight out of stuff/spacetaxi/raw.bin (a VICE
|
||||
# "bank ram" dump, 2-byte load-address header) so the port has one
|
||||
|
|
|
|||
90
spacetaxi.c
90
spacetaxi.c
|
|
@ -27,6 +27,15 @@
|
|||
|
||||
#define ST_TICK_HZ 30u
|
||||
#define ST_MAX_CATCHUP_TICKS 4u
|
||||
// The C64 runs the game every other frame but the level intro ($4666)
|
||||
// every frame.
|
||||
#define ST_C64_FRAMES_PER_TICK 2u
|
||||
// The intro's loop count ($4508) runs 0..3 and the cab's judder ($470D)
|
||||
// steps -2 into count 0; batches that cannot show every frame end on the
|
||||
// counts either side of that jump (runIntro).
|
||||
#define ST_INTRO_LOOP_MASK 3u
|
||||
#define ST_INTRO_SHOW_BEFORE 3u
|
||||
#define ST_INTRO_SHOW_AFTER 0u
|
||||
// Screens A..X, then screen Y -- the 25th, " MYSTERY SCREEN !!! " -- which
|
||||
// $5167 loads once the 24-hour shifts have cleared all of A..X. Files are
|
||||
// levels/level01.dat .. level25.dat, so Y is index 24.
|
||||
|
|
@ -85,6 +94,8 @@ typedef struct {
|
|||
bool fireArmed; // $445D: FIRE must be released first
|
||||
uint8_t lastFrame;
|
||||
uint16_t paceAcc;
|
||||
uint16_t frameAcc; // C64 frames owed, in paceAcc's units
|
||||
int8_t introOwed; // intro frames owed to (+) or run ahead of (-) the next batch
|
||||
StStateE afterJingle; // where ST_STATE_JINGLE returns to
|
||||
bool gameOverTune; // $5C24: song 5 started after the wait
|
||||
uint8_t hsSlot; // $4D51 table slot being named
|
||||
|
|
@ -189,11 +200,11 @@ static bool joyFire(void);
|
|||
static uint8_t joyMask(void);
|
||||
static bool loadLevel(uint8_t index);
|
||||
static uint8_t nextLevelForShift(void);
|
||||
static uint8_t paceTicks(void);
|
||||
static uint8_t paceTicks(uint8_t *c64Frames);
|
||||
static bool serviceJingle(void);
|
||||
static void runCabbiesMenu(uint8_t ticks);
|
||||
static void runDemoEnd(void);
|
||||
static void runIntro(uint8_t ticks);
|
||||
static void runIntro(uint8_t c64Frames);
|
||||
static void runNameEntry(uint8_t ticks);
|
||||
static bool nameEntryStart(void);
|
||||
static void runPlaying(uint8_t ticks);
|
||||
|
|
@ -604,22 +615,35 @@ static uint8_t nextLevelForShift(void) {
|
|||
// Game ticks due this rendered frame, from the vertical blank count:
|
||||
// one tick per two frames on 50/60 Hz displays, three per seven on
|
||||
// the 70 Hz VGA mode. jlFrameCount is 8-bit-wrapped so a stall of any
|
||||
// length just clamps to the catch-up cap.
|
||||
static uint8_t paceTicks(void) {
|
||||
uint16_t hz = jlFrameHz();
|
||||
uint8_t now = (uint8_t)jlFrameCount();
|
||||
uint8_t frames = (uint8_t)(now - gGame.lastFrame);
|
||||
uint8_t ticks = 0u;
|
||||
// length just clamps to the catch-up cap. *c64Frames gets the C64 frames
|
||||
// due as well, ST_C64_FRAMES_PER_TICK a tick: one per display frame at
|
||||
// 50/60 Hz, so the level intro shows every frame the C64 does -- the
|
||||
// cab's up-and-down judder ($470D) is a one-frame pattern that stepping
|
||||
// two frames at a time cancels out.
|
||||
static uint8_t paceTicks(uint8_t *c64Frames) {
|
||||
uint16_t hz = jlFrameHz();
|
||||
uint8_t now = (uint8_t)jlFrameCount();
|
||||
uint8_t frames = (uint8_t)(now - gGame.lastFrame);
|
||||
uint8_t ticks = 0u;
|
||||
uint16_t perTick = ST_TICK_HZ;
|
||||
|
||||
gGame.lastFrame = now;
|
||||
if (frames > 16u) {
|
||||
frames = 16u;
|
||||
}
|
||||
if (hz == 50u || hz == 60u) {
|
||||
gGame.paceAcc += (uint16_t)(frames * 35u);
|
||||
hz = 70u;
|
||||
} else {
|
||||
gGame.paceAcc += (uint16_t)(frames * ST_TICK_HZ);
|
||||
perTick = 35u;
|
||||
hz = 70u;
|
||||
}
|
||||
gGame.paceAcc += (uint16_t)(frames * perTick);
|
||||
gGame.frameAcc += (uint16_t)(frames * perTick * ST_C64_FRAMES_PER_TICK);
|
||||
*c64Frames = 0u;
|
||||
while (gGame.frameAcc >= hz && *c64Frames < ST_MAX_CATCHUP_TICKS * ST_C64_FRAMES_PER_TICK) {
|
||||
gGame.frameAcc = (uint16_t)(gGame.frameAcc - hz);
|
||||
(*c64Frames)++;
|
||||
}
|
||||
if (gGame.frameAcc > (uint16_t)(hz * ST_MAX_CATCHUP_TICKS * ST_C64_FRAMES_PER_TICK)) {
|
||||
gGame.frameAcc = (uint16_t)(hz * ST_MAX_CATCHUP_TICKS * ST_C64_FRAMES_PER_TICK);
|
||||
}
|
||||
// Ticks not run this frame stay owed (a slow frame catches up over
|
||||
// the next ones); a stall longer than the cap's worth is forgiven.
|
||||
|
|
@ -674,15 +698,42 @@ static void runDemoEnd(void) {
|
|||
}
|
||||
|
||||
|
||||
// $4666 loop: two frames per game tick; joystick ends a demo.
|
||||
static void runIntro(uint8_t ticks) {
|
||||
// $4666 loop: once per C64 frame; joystick ends a demo. A port that shows
|
||||
// every frame shows the cab's one-frame judder ($470D) as the C64 does. One
|
||||
// that shows only some would sample it at a fixed phase -- every second frame
|
||||
// shows -1, 0, -1, 0, a smooth climb -- so when two or more frames are due
|
||||
// the batch is cut or stretched by one (evened out on the next batch) to end
|
||||
// on the loop counts either side of the -2 jump: the frames shown then cross
|
||||
// it and the cab judders, at the same average speed.
|
||||
static void runIntro(uint8_t c64Frames) {
|
||||
static const int8_t kTry[] = { 0, -1, 1 };
|
||||
int16_t due;
|
||||
uint8_t steps;
|
||||
uint8_t end;
|
||||
uint8_t t;
|
||||
uint8_t k;
|
||||
|
||||
if (gGame.demo && (joyMask() & 0x1Fu) != 0u) {
|
||||
runDemoEnd();
|
||||
return;
|
||||
}
|
||||
for (k = 0u; k < (uint8_t)(ticks * 2u); k++) {
|
||||
due = (int16_t)(c64Frames + gGame.introOwed);
|
||||
gGame.introOwed = 0;
|
||||
steps = (uint8_t)((due > 0) ? due : 0);
|
||||
if (c64Frames >= ST_C64_FRAMES_PER_TICK) {
|
||||
for (t = 0u; t < (uint8_t)sizeof(kTry); t++) {
|
||||
if (due + kTry[t] < 1) {
|
||||
continue;
|
||||
}
|
||||
end = (uint8_t)((gSim.introLoopCount + due + kTry[t]) & ST_INTRO_LOOP_MASK);
|
||||
if (end == ST_INTRO_SHOW_BEFORE || end == ST_INTRO_SHOW_AFTER) {
|
||||
steps = (uint8_t)(due + kTry[t]);
|
||||
gGame.introOwed = (int8_t)(-kTry[t]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (k = 0u; k < steps; k++) {
|
||||
if (stIntroStep(&gSim)) {
|
||||
jlMusicStop(); // $4709
|
||||
stSimEnterLevel(&gSim, &gLevel);
|
||||
|
|
@ -930,7 +981,8 @@ static void startIntro(void) {
|
|||
// $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);
|
||||
gGame.state = ST_STATE_INTRO;
|
||||
gGame.introOwed = 0;
|
||||
gGame.state = ST_STATE_INTRO;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1018,6 +1070,7 @@ int main(void) {
|
|||
|
||||
for (;;) {
|
||||
uint8_t ticks;
|
||||
uint8_t c64Frames;
|
||||
|
||||
if (gGame.fatal) {
|
||||
fatalHold();
|
||||
|
|
@ -1045,6 +1098,7 @@ int main(void) {
|
|||
gGame.paused = false;
|
||||
gGame.lastFrame = (uint8_t)jlFrameCount(); // the held time is not owed
|
||||
gGame.paceAcc = 0u;
|
||||
gGame.frameAcc = 0u;
|
||||
if (gGame.state == ST_STATE_INTRO) {
|
||||
stAudioMusic(ST_SONG_INTRO, true); // $46B5
|
||||
}
|
||||
|
|
@ -1058,7 +1112,7 @@ int main(void) {
|
|||
enterTitle();
|
||||
}
|
||||
}
|
||||
ticks = paceTicks();
|
||||
ticks = paceTicks(&c64Frames);
|
||||
switch (gGame.state) {
|
||||
case ST_STATE_TITLE:
|
||||
runTitle(ticks);
|
||||
|
|
@ -1090,7 +1144,7 @@ int main(void) {
|
|||
}
|
||||
break;
|
||||
case ST_STATE_INTRO:
|
||||
runIntro(ticks);
|
||||
runIntro(c64Frames);
|
||||
break;
|
||||
case ST_STATE_PLAYING:
|
||||
runPlaying(ticks);
|
||||
|
|
|
|||
|
|
@ -323,8 +323,8 @@ static uint32_t speechFill(void *ctx, int8_t *dst, uint32_t count) {
|
|||
}
|
||||
n = (gSpeechStageCount < want) ? gSpeechStageCount : want;
|
||||
if (n != 0u) {
|
||||
// jlMemCopy (MVN on the IIgs; libc memcpy is a byte loop there),
|
||||
// in two runs when the unread bytes wrap the ring's end.
|
||||
// jlMemCopy (MVN on the IIgs), in two runs when the unread bytes
|
||||
// wrap the ring's end.
|
||||
uint8_t *to = (uint8_t *)dst;
|
||||
uint16_t left = n;
|
||||
|
||||
|
|
|
|||
9
stCels.c
9
stCels.c
|
|
@ -2,8 +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), the flame set in multicolour colour 7, then the
|
||||
// hires intro star in colour 7.
|
||||
// $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.
|
||||
|
||||
#include "stCels.h"
|
||||
|
||||
|
|
@ -12,7 +13,7 @@
|
|||
#define MC0 0x02u
|
||||
#define MC1 0x07u
|
||||
#define CAB 0x06u // cab and passenger per-sprite colour
|
||||
#define FLM 0x07u // flame / star colour
|
||||
#define FLM 0x07u // flame colour
|
||||
|
||||
const StCelDefT kStCels[] = {
|
||||
// Cab: flying (gear up/down x left/right), wreck cels, intro cabs.
|
||||
|
|
@ -37,8 +38,6 @@ const StCelDefT kStCels[] = {
|
|||
{ 0xD5, FLM, 1u, MC0, MC1 }, { 0xD2, FLM, 1u, MC0, MC1 },
|
||||
{ 0xD1, FLM, 1u, MC0, MC1 }, { 0xD7, FLM, 1u, MC0, MC1 },
|
||||
{ 0xD3, FLM, 1u, MC0, MC1 }, { 0xD6, FLM, 1u, MC0, MC1 },
|
||||
// Intro star, hires colour 7.
|
||||
{ 0xDA, FLM, 0u, 0u, 0u },
|
||||
};
|
||||
|
||||
const uint16_t kStCelCount = (uint16_t)(sizeof(kStCels) / sizeof(kStCels[0]));
|
||||
|
|
|
|||
238
stRender.c
238
stRender.c
|
|
@ -37,10 +37,10 @@
|
|||
// 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 plus 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 (37) plus the largest
|
||||
// cab, exhaust and passenger sets) 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
|
||||
// level's hook cels (I: 30) at once.
|
||||
#define ST_SPRITE_CACHE 80u
|
||||
#define ST_SPRITE_BACKUP_BYTES JOEY_SPRITE_BACKUP_BYTES(ST_SPRITE_TILES, ST_SPRITE_TILES)
|
||||
|
|
@ -66,6 +66,10 @@ typedef struct {
|
|||
uint8_t mc0;
|
||||
uint8_t mc1;
|
||||
uint8_t level; // level index the bitmap came from (level sprites)
|
||||
uint8_t offX; // where the (cropped) cel sits in the 24x24
|
||||
uint8_t offY; // VIC frame, in pixels -- see celCreate
|
||||
uint8_t w; // the cel's size in pixels
|
||||
uint8_t h;
|
||||
bool used;
|
||||
bool pinned; // prewarmed moving cel: never evicted (its
|
||||
// compiled code is costly to re-emit on the 65816)
|
||||
|
|
@ -78,6 +82,7 @@ typedef struct {
|
|||
// What is drawn in a draw slot (slot 0 = sprite 7 ... slot 7 = sprite 0).
|
||||
typedef struct {
|
||||
bool drawn;
|
||||
bool trusted; // backed up by the compiled, unclipped save
|
||||
uint16_t x;
|
||||
uint8_t y;
|
||||
uint8_t ptr;
|
||||
|
|
@ -92,8 +97,7 @@ typedef struct {
|
|||
// 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
|
||||
uint8_t starPhase; // which step of every flight is lit
|
||||
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
|
||||
|
|
@ -136,22 +140,18 @@ static const uint16_t kC64Palette[16] = {
|
|||
// 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 };
|
||||
// Level-intro warp: how a star step looks at each distance BEHIND the
|
||||
// lit step, as C64 colour indices; further behind is unlit. One yellow
|
||||
// star per ray, as the C64's star sprites (colour 7, $4523) fly with no
|
||||
// trail.
|
||||
static const uint8_t kStarRamp[] = { 7u };
|
||||
#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 void buildSpriteCel(const uint8_t *bm, uint8_t multi, uint8_t color, uint8_t mc0, uint8_t mc1);
|
||||
static jlSpriteT *cachedSprite(StSimT *sim, uint8_t idx);
|
||||
static StSpriteCacheT *cachedSprite(StSimT *sim, uint8_t idx);
|
||||
static void cacheCompile(StSpriteCacheT *e);
|
||||
static StSpriteCacheT *cacheFreeSlot(void);
|
||||
static bool celCreate(StSpriteCacheT *e, const uint8_t *bm, uint8_t multi, uint8_t color, uint8_t mc0, uint8_t mc1);
|
||||
static void levelCelsEvict(void);
|
||||
static const uint8_t *levelSpriteBitmap(const StLevelT *level, uint8_t ptr);
|
||||
static void loadingBar(jlSurfaceT *stage, uint8_t done, uint8_t total);
|
||||
|
|
@ -173,26 +173,7 @@ static bool prewarmReport(uint16_t expect);
|
|||
static bool spriteOnScreen(int16_t px, int16_t py);
|
||||
static void starPaletteApply(void);
|
||||
static void tileFromChunky(jlTileT *out, const uint8_t *chunky);
|
||||
|
||||
|
||||
// Paint a VIC sprite bitmap onto the scratch surface's top-left 3x3
|
||||
// tiles from the shared cel expansion (stCels.c, the same one the
|
||||
// offline baker uses), so a cel built here for the interpreter fallback
|
||||
// matches the precompiled .spc byte for byte.
|
||||
static void buildSpriteCel(const uint8_t *bm, uint8_t multi, uint8_t color, uint8_t mc0, uint8_t mc1) {
|
||||
uint8_t blob[ST_CEL_BYTES];
|
||||
jlTileT tile;
|
||||
uint8_t tx;
|
||||
uint8_t ty;
|
||||
|
||||
stCelBlob(bm, multi, color, mc0, mc1, blob);
|
||||
for (ty = 0u; ty < ST_SPRITE_TILES; ty++) {
|
||||
for (tx = 0u; tx < ST_SPRITE_TILES; tx++) {
|
||||
tileFromChunky(&tile, &blob[(ty * ST_SPRITE_TILES + tx) * TILE_BYTES]);
|
||||
jlTilePaste(gRender.scratch, tx, ty, &tile);
|
||||
}
|
||||
}
|
||||
}
|
||||
static bool tileHasPixels(const uint8_t *chunky);
|
||||
|
||||
|
||||
// Compile a cached cel if it is not already, backing off after a failure
|
||||
|
|
@ -210,9 +191,10 @@ static void cacheCompile(StSpriteCacheT *e) {
|
|||
}
|
||||
|
||||
|
||||
// The JoeyLib sprite for hardware sprite `idx` of the current frame,
|
||||
// built on first use from its VIC bitmap and colours.
|
||||
static jlSpriteT *cachedSprite(StSimT *sim, uint8_t idx) {
|
||||
// The cached cel for hardware sprite `idx` of the current frame, built
|
||||
// on first use from its VIC bitmap and colours; NULL when there is
|
||||
// nothing to draw.
|
||||
static StSpriteCacheT *cachedSprite(StSimT *sim, uint8_t idx) {
|
||||
uint8_t ptr = sim->frame.ptr[idx];
|
||||
uint8_t color = drawColor(sim, idx);
|
||||
uint8_t multi = (uint8_t)((sim->frame.multiMask & kStBit[idx]) != 0u ? 1u : 0u);
|
||||
|
|
@ -227,14 +209,14 @@ static jlSpriteT *cachedSprite(StSimT *sim, uint8_t idx) {
|
|||
// Most frames a hardware sprite shows the cel it showed last time.
|
||||
if (e != 0 && e->used && e->ptr == ptr && e->color == color && e->multi == multi && e->mc0 == mc0 && e->mc1 == mc1 && e->level == level) {
|
||||
e->lastUse = gRender.cacheStamp;
|
||||
return e->sprite;
|
||||
return e;
|
||||
}
|
||||
for (k = 0u; k < ST_SPRITE_CACHE; k++) {
|
||||
e = &gRender.cache[k];
|
||||
if (e->used && e->ptr == ptr && e->color == color && e->multi == multi && e->mc0 == mc0 && e->mc1 == mc1 && e->level == level) {
|
||||
e->lastUse = gRender.cacheStamp;
|
||||
gRender.lastHit[idx] = e;
|
||||
return e->sprite;
|
||||
return e;
|
||||
}
|
||||
// Pinned entries (prewarmed cab/exhaust/passenger cels) are the
|
||||
// costly-to-recompile ones and are never evicted; the least-
|
||||
|
|
@ -264,9 +246,7 @@ static jlSpriteT *cachedSprite(StSimT *sim, uint8_t idx) {
|
|||
slot->used = false;
|
||||
slot->compiled = false;
|
||||
}
|
||||
buildSpriteCel(stSimSpriteBitmap(sim, ptr), multi, color, mc0, mc1);
|
||||
slot->sprite = jlSpriteCreateFromSurface(gRender.scratch, 0, 0, ST_SPRITE_TILES, ST_SPRITE_TILES);
|
||||
if (slot->sprite == 0) {
|
||||
if (!celCreate(slot, stSimSpriteBitmap(sim, ptr), multi, color, mc0, mc1)) {
|
||||
return 0;
|
||||
}
|
||||
// Nothing compiles mid-ride: a compile is ~0.5 s on the 65816, a
|
||||
|
|
@ -293,7 +273,7 @@ static jlSpriteT *cachedSprite(StSimT *sim, uint8_t idx) {
|
|||
slot->used = true;
|
||||
slot->pinned = gPrewarming;
|
||||
gRender.lastHit[idx] = slot;
|
||||
return slot->sprite;
|
||||
return slot;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -312,9 +292,61 @@ static const uint8_t kCellRow[125] = {
|
|||
};
|
||||
|
||||
|
||||
// Column of a cell whose row is known: cell - row * 40 through a table,
|
||||
// because the two-shift form of the multiply came back as a __mulhi3
|
||||
// library call on the 65816.
|
||||
// Build a VIC bitmap's cel into a cache entry from the shared cel
|
||||
// expansion (stCels.c, the same one the offline baker uses), cropped to
|
||||
// the tiles that hold pixels. Most VIC sprites leave whole tiles of their
|
||||
// 24x21 frame empty, and every one of those would otherwise be saved,
|
||||
// drawn, restored and presented each time the sprite moves. A bitmap
|
||||
// with no pixels keeps the whole frame.
|
||||
//
|
||||
// The tiles go onto the scratch surface last to first: on the planar
|
||||
// ports tileFromChunky converts through the scratch's top-left tile, so
|
||||
// that tile has to be the one written last.
|
||||
static bool celCreate(StSpriteCacheT *e, const uint8_t *bm, uint8_t multi, uint8_t color, uint8_t mc0, uint8_t mc1) {
|
||||
uint8_t blob[ST_CEL_BYTES];
|
||||
jlTileT tile;
|
||||
uint8_t txMin = ST_SPRITE_TILES;
|
||||
uint8_t txMax = 0u;
|
||||
uint8_t tyMin = ST_SPRITE_TILES;
|
||||
uint8_t tyMax = 0u;
|
||||
uint8_t tx;
|
||||
uint8_t ty;
|
||||
|
||||
stCelBlob(bm, multi, color, mc0, mc1, blob);
|
||||
ty = ST_SPRITE_TILES;
|
||||
while (ty > 0u) {
|
||||
ty--;
|
||||
tx = ST_SPRITE_TILES;
|
||||
while (tx > 0u) {
|
||||
const uint8_t *chunk;
|
||||
|
||||
tx--;
|
||||
chunk = &blob[(ty * ST_SPRITE_TILES + tx) * TILE_BYTES];
|
||||
tileFromChunky(&tile, chunk);
|
||||
jlTilePaste(gRender.scratch, tx, ty, &tile);
|
||||
if (tileHasPixels(chunk)) {
|
||||
txMin = (tx < txMin) ? tx : txMin;
|
||||
txMax = (tx > txMax) ? tx : txMax;
|
||||
tyMin = (ty < tyMin) ? ty : tyMin;
|
||||
tyMax = (ty > tyMax) ? ty : tyMax;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (txMin > txMax) {
|
||||
txMin = 0u;
|
||||
tyMin = 0u;
|
||||
txMax = ST_SPRITE_TILES - 1u;
|
||||
tyMax = ST_SPRITE_TILES - 1u;
|
||||
}
|
||||
e->offX = (uint8_t)(txMin * TILE_PIXELS_PER_SIDE);
|
||||
e->offY = (uint8_t)(tyMin * TILE_PIXELS_PER_SIDE);
|
||||
e->w = (uint8_t)((txMax - txMin + 1u) * TILE_PIXELS_PER_SIDE);
|
||||
e->h = (uint8_t)((tyMax - tyMin + 1u) * TILE_PIXELS_PER_SIDE);
|
||||
e->sprite = jlSpriteCreateFromSurface(gRender.scratch, (int16_t)e->offX, (int16_t)e->offY, (uint8_t)(e->w / TILE_PIXELS_PER_SIDE), (uint8_t)(e->h / TILE_PIXELS_PER_SIDE));
|
||||
return e->sprite != 0;
|
||||
}
|
||||
|
||||
|
||||
// True for a cel that is MEANT to be built here rather than pre-listed,
|
||||
// so the warning below stays about real misses. Only the title screen's
|
||||
// decorations qualify: logoColorCycle recolours them every cycle step,
|
||||
|
|
@ -335,6 +367,9 @@ static bool celDrawsInterpreted(uint8_t ptr, uint8_t color) {
|
|||
}
|
||||
|
||||
|
||||
// Column of a cell whose row is known: cell - row * 40 through a table,
|
||||
// because the two-shift form of the multiply came back as a __mulhi3
|
||||
// library call on the 65816.
|
||||
static uint8_t cellCol(uint16_t cell, uint8_t row) {
|
||||
static const uint16_t kRowBase[ST_SCREEN_ROWS] = {
|
||||
0, 40, 80, 120, 160, 200, 240, 280, 320, 360, 400, 440, 480,
|
||||
|
|
@ -425,8 +460,7 @@ static void dirtyBands(const StSimT *sim) {
|
|||
}
|
||||
return;
|
||||
}
|
||||
// A plain loop, not memset: 25 entries do not pay for the 65816
|
||||
// libc's far-called byte loop.
|
||||
// One loop over both 25-entry arrays.
|
||||
for (row = 0u; row < ST_SCREEN_ROWS; row++) {
|
||||
gRender.dirtyColMin[row] = 0xFFu;
|
||||
gRender.dirtyColMax[row] = 0u;
|
||||
|
|
@ -688,6 +722,19 @@ static void tileFromChunky(jlTileT *out, const uint8_t *chunky) {
|
|||
}
|
||||
|
||||
|
||||
// True when a chunky 8x8 tile has any non-transparent (non-zero) pixel.
|
||||
static bool tileHasPixels(const uint8_t *chunky) {
|
||||
uint8_t k;
|
||||
|
||||
for (k = 0u; k < TILE_BYTES; k++) {
|
||||
if (chunky[k] != 0u) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -718,13 +765,13 @@ void stRenderFrame(jlSurfaceT *stage, StSimT *sim) {
|
|||
}
|
||||
}
|
||||
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));
|
||||
// The warp animates in the palette: no cells change and no sprite
|
||||
// moves for it. The lit step follows the intro's own frame count,
|
||||
// so the stars keep the C64's speed whatever the frame rate.
|
||||
uint8_t phase = (uint8_t)((sim->introStepCount / ST_STAR_FRAMES_PER_STEP) & (ST_STAR_RING_COUNT - 1u));
|
||||
|
||||
if (phase != gRender.starPhase) {
|
||||
gRender.starPhase = phase;
|
||||
starPaletteApply();
|
||||
}
|
||||
}
|
||||
|
|
@ -752,17 +799,21 @@ void stRenderFrame(jlSurfaceT *stage, StSimT *sim) {
|
|||
for (k = ST_HW_SPRITES; k > first; k--) {
|
||||
StDrawnT *d = &gRender.slot[k - 1u];
|
||||
if (d->drawn) {
|
||||
jlSpriteRestoreUnder(stage, &gRender.backup[k - 1u]);
|
||||
if (d->trusted) {
|
||||
jlSpriteRestoreUnderTrusted(stage, &gRender.backup[k - 1u]);
|
||||
} else {
|
||||
jlSpriteRestoreUnder(stage, &gRender.backup[k - 1u]);
|
||||
}
|
||||
d->drawn = false;
|
||||
}
|
||||
}
|
||||
paintCells(stage, sim);
|
||||
for (k = first; k < ST_HW_SPRITES; k++) {
|
||||
uint8_t idx = (uint8_t)(ST_HW_SPRITES - 1u - k);
|
||||
StDrawnT *d = &gRender.slot[k];
|
||||
jlSpriteT *sp;
|
||||
int16_t px;
|
||||
int16_t py;
|
||||
uint8_t idx = (uint8_t)(ST_HW_SPRITES - 1u - k);
|
||||
StDrawnT *d = &gRender.slot[k];
|
||||
StSpriteCacheT *e;
|
||||
int16_t px;
|
||||
int16_t py;
|
||||
if ((sim->frame.enableMask & kStBit[idx]) == 0u) {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -771,11 +822,21 @@ void stRenderFrame(jlSurfaceT *stage, StSimT *sim) {
|
|||
if (!spriteOnScreen(px, py)) {
|
||||
continue;
|
||||
}
|
||||
sp = cachedSprite(sim, idx);
|
||||
if (sp == 0) {
|
||||
e = cachedSprite(sim, idx);
|
||||
if (e == 0) {
|
||||
continue;
|
||||
}
|
||||
jlSpriteSaveAndDraw(stage, sp, px, py, &gRender.backup[k]);
|
||||
px = (int16_t)(px + e->offX);
|
||||
py = (int16_t)(py + e->offY);
|
||||
// A compiled cel wholly on the stage takes the trusted calls,
|
||||
// which skip the per-call geometry checks; one that is clipped
|
||||
// or interpreted needs the checked ones, and so does its restore.
|
||||
d->trusted = e->compiled && px >= 0 && py >= 0 && px <= (int16_t)(SURFACE_WIDTH - e->w) && py <= (int16_t)(SURFACE_HEIGHT - e->h);
|
||||
if (d->trusted) {
|
||||
jlSpriteSaveAndDrawTrusted(stage, e->sprite, px, py, &gRender.backup[k]);
|
||||
} else {
|
||||
jlSpriteSaveAndDraw(stage, e->sprite, px, py, &gRender.backup[k]);
|
||||
}
|
||||
d->drawn = true;
|
||||
d->x = sim->frame.x[idx];
|
||||
d->y = sim->frame.y[idx];
|
||||
|
|
@ -824,14 +885,6 @@ void stRenderInit(jlSurfaceT *stage) {
|
|||
}
|
||||
|
||||
|
||||
// Build (and compile) the cels the game draws every screen -- cab,
|
||||
// exhaust, passenger, wreck, warp, intro cab and star -- up front, so
|
||||
// no frame pays for a sprite build mid-play. A bar on the stage shows
|
||||
// progress on the slower ports.
|
||||
// Load the offline-baked, already-compiled cel bank into the pinned
|
||||
// cache so no cel is JIT-compiled at boot (each 65816 compile costs
|
||||
// ~0.4 s). Every kStCels entry becomes a pinned cache slot keyed exactly
|
||||
// as the play lookups: system ptr (level 0xFF), the level-wide colours.
|
||||
// The first unused cache slot, or NULL when the cache is full.
|
||||
static StSpriteCacheT *cacheFreeSlot(void) {
|
||||
uint8_t k;
|
||||
|
|
@ -936,15 +989,17 @@ bool stRenderLevelCels(jlSurfaceT *stage, const StLevelT *level) {
|
|||
}
|
||||
if (got == n) {
|
||||
slot->sprite = cels[k];
|
||||
slot->compiled = true;
|
||||
slot->offX = 0u;
|
||||
slot->offY = 0u;
|
||||
slot->w = ST_SPRITE_PX;
|
||||
slot->h = ST_SPRITE_PX;
|
||||
slot->compiled = (jlSpriteCompiledSize(cels[k]) != 0u);
|
||||
} else {
|
||||
const uint8_t *bm = levelSpriteBitmap(level, list[k].ptr);
|
||||
if (bm == 0) {
|
||||
continue;
|
||||
}
|
||||
buildSpriteCel(bm, list[k].multi, list[k].color, list[k].mc0, list[k].mc1);
|
||||
slot->sprite = jlSpriteCreateFromSurface(gRender.scratch, 0, 0, ST_SPRITE_TILES, ST_SPRITE_TILES);
|
||||
if (slot->sprite == 0) {
|
||||
if (!celCreate(slot, bm, list[k].multi, list[k].color, list[k].mc0, list[k].mc1)) {
|
||||
continue;
|
||||
}
|
||||
slot->compiled = false;
|
||||
|
|
@ -975,6 +1030,10 @@ bool stRenderLevelCels(jlSurfaceT *stage, const StLevelT *level) {
|
|||
}
|
||||
|
||||
|
||||
// Load the offline-baked, already-compiled cel bank into the pinned
|
||||
// cache so no cel is JIT-compiled at boot (each 65816 compile costs
|
||||
// ~0.4 s). Every kStCels entry becomes a pinned cache slot keyed exactly
|
||||
// as the play lookups: system ptr (level 0xFF), the level-wide colours.
|
||||
// Returns false if the .spc is missing or built for another target/shift
|
||||
// count (a stale bake), so the caller falls back to the runtime JIT
|
||||
// prewarm below. Every port stages its own bake (make/<port>.mk).
|
||||
|
|
@ -1002,6 +1061,10 @@ static bool loadPrecompiledCels(void) {
|
|||
e->mc0 = kStCels[i].mc0;
|
||||
e->mc1 = kStCels[i].mc1;
|
||||
e->level = 0xFFu;
|
||||
e->offX = 0u;
|
||||
e->offY = 0u;
|
||||
e->w = ST_SPRITE_PX;
|
||||
e->h = ST_SPRITE_PX;
|
||||
e->used = true;
|
||||
e->pinned = true;
|
||||
e->lastUse = 0u;
|
||||
|
|
@ -1071,11 +1134,16 @@ void stRenderLoading(jlSurfaceT *stage) {
|
|||
jlStagePresent();
|
||||
}
|
||||
|
||||
|
||||
// Build (and compile) the cels the game draws every screen -- cab,
|
||||
// exhaust, passenger, wreck and intro cab -- up front, so no frame pays
|
||||
// for a sprite build mid-play. A bar on the stage shows progress on the
|
||||
// slower ports.
|
||||
bool stRenderPrewarm(jlSurfaceT *stage, StSimT *sim) {
|
||||
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) + 1u);
|
||||
uint8_t total = (uint8_t)(sizeof(kCabPtrs) + sizeof(kPassPtrs) + sizeof(kFlamePtrs));
|
||||
uint8_t done = 0u;
|
||||
bool ok;
|
||||
uint8_t k;
|
||||
|
|
@ -1114,13 +1182,6 @@ bool stRenderPrewarm(jlSurfaceT *stage, StSimT *sim) {
|
|||
done++;
|
||||
loadingBar(stage, done, total);
|
||||
}
|
||||
// The intro star is a hires sprite in colour 7. Build it as a moving
|
||||
// (idx <= 2) cel so it compiles: the level-name intro draws seven of
|
||||
// them every frame, and the interpreter cannot keep up.
|
||||
sim->frame.multiMask = 0u;
|
||||
sim->frame.ptr[2] = 0xDAu;
|
||||
sim->frame.color[2] = 0x07u;
|
||||
(void)cachedSprite(sim, 2u);
|
||||
sim->frame = saved;
|
||||
gPrewarming = false;
|
||||
ok = prewarmReport(total);
|
||||
|
|
@ -1206,11 +1267,10 @@ void stRenderTitleLogo(StSimT *sim) {
|
|||
// 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.
|
||||
// one palette write every ST_STAR_FRAMES_PER_STEP intro frames.
|
||||
void stRenderIntroStars(void) {
|
||||
gRender.starMode = true;
|
||||
gRender.starPhase = 0u;
|
||||
gRender.starTick = 0u;
|
||||
starPaletteApply();
|
||||
}
|
||||
|
||||
|
|
|
|||
28
stSim.h
28
stSim.h
|
|
@ -100,8 +100,9 @@
|
|||
// 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
|
||||
// stTitle.c colours a star's cell ST_STAR_RING_FIRST + the step of its
|
||||
// ray's flight (offset by the ray's launch delay), 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
|
||||
|
|
@ -112,12 +113,14 @@
|
|||
// 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
|
||||
// A C64 star flies $20 frames ($4500) and the painted field marks every
|
||||
// ST_STAR_FRAMES_PER_STEP of them, so the renderer steps the lit colour on
|
||||
// that many intro frames (introStepCount): the warp keeps the C64's speed at
|
||||
// any frame rate.
|
||||
#define ST_INTRO_STAR_FLIGHT 0x20u
|
||||
#define ST_STAR_FRAMES_PER_STEP (ST_INTRO_STAR_FLIGHT / ST_STAR_RING_COUNT)
|
||||
// Every star is the small glyph: the C64's star is a 5x3 speck.
|
||||
#define ST_STAR_GLYPH 0x2Eu // '.'
|
||||
|
||||
// VIC sprite block pointers the game selects by name. The cab's bit 0 is
|
||||
// the landing gear, so each facing is a pair.
|
||||
|
|
@ -309,7 +312,11 @@ typedef struct {
|
|||
uint8_t hookData[ST_HOOK_BYTES]; // $7D98..$7FFF as shipped
|
||||
} StLevelT;
|
||||
|
||||
// One VIC sprite as the game sees it through its shadow tables.
|
||||
// One VIC sprite as the game sees it through its shadow tables. Padded
|
||||
// to eight bytes: the 65816 compiler indexes a 6-byte element through
|
||||
// a call to its 32-bit multiply routine (sim is a far pointer), and
|
||||
// every sim->spr[idx] in the game paid for one; a power-of-two stride
|
||||
// is three shifts.
|
||||
typedef struct {
|
||||
uint8_t col; // $7175,X X low byte
|
||||
uint8_t msb; // $7185,X X high bit (any non-zero value = set)
|
||||
|
|
@ -317,7 +324,9 @@ typedef struct {
|
|||
uint8_t enable; // $718E,X
|
||||
uint8_t ptr; // $7197,X block pointer
|
||||
uint8_t color; // $719F,X
|
||||
uint8_t strideToPow2[2];
|
||||
} StSpriteT;
|
||||
typedef char stSpriteStrideIsPow2[((sizeof(StSpriteT) & (sizeof(StSpriteT) - 1u)) == 0u) ? 1 : -1];
|
||||
|
||||
// The frame the VIC last displayed ($4253 marshal + $4293 flush): the
|
||||
// renderer draws this and the collision test evaluates it.
|
||||
|
|
@ -542,6 +551,7 @@ typedef struct {
|
|||
uint8_t logoCycleAux; // $48A5
|
||||
uint8_t logoDiv; // $48A6
|
||||
uint8_t introLoopCount; // $4508
|
||||
uint8_t introStepCount; // port: intro frames run, for the painted warp
|
||||
uint8_t introCabDx; // $450A
|
||||
uint8_t introCabDxTimer; // $450B
|
||||
uint8_t spriteMc0; // $D025
|
||||
|
|
|
|||
142
stTitle.c
142
stTitle.c
|
|
@ -14,8 +14,10 @@
|
|||
//
|
||||
// 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.
|
||||
// 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>
|
||||
|
||||
|
|
@ -40,42 +42,35 @@ static const uint8_t kTextScreens[] = "THIS IS 1 OF 25 DIFFERENT SCREENS!";
|
|||
#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. 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;
|
||||
// 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 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 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);
|
||||
static void paintStarfield(StSimT *sim, const uint8_t *delay);
|
||||
static bool rowHasText(const StSimT *sim, uint8_t row);
|
||||
|
||||
|
||||
// $4113
|
||||
|
|
@ -124,21 +119,60 @@ static void logoFlip(StSimT *sim) {
|
|||
}
|
||||
|
||||
|
||||
// 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;
|
||||
// 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;
|
||||
|
||||
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));
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -148,6 +182,7 @@ static void paintStarfield(StSimT *sim) {
|
|||
|
||||
// $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;
|
||||
|
|
@ -161,20 +196,19 @@ void stIntroEnter(StSimT *sim, const StLevelT *level) {
|
|||
sim->spr[7].row = 0xE4u;
|
||||
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_ROW;
|
||||
sim->spr[idx].msb = 0u;
|
||||
sim->spr[idx].col = 0xAAu;
|
||||
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/$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);
|
||||
// $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;
|
||||
|
|
@ -188,12 +222,13 @@ void stIntroEnter(StSimT *sim, const StLevelT *level) {
|
|||
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);
|
||||
}
|
||||
paintStarfield(sim, delay);
|
||||
sim->introLoopCount = 0u;
|
||||
sim->introStepCount = 0u;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -203,6 +238,7 @@ 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) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue