From cdc7bc545d7c48f70629f33ff24bcca07ef366aa Mon Sep 17 00:00:00 2001 From: Jurjen Ladenius Date: Mon, 29 Jun 2026 23:40:09 +0200 Subject: [PATCH] Fix MSX2+ horizontal scroll and rework FH logo animation - Enable single-page horizontal scrolling (R#25 SP2=1) so the scroll offset wraps within the 256px page. Without it the V9958 default (two-page) scroll revealed the wrong VRAM page, making new tiles appear on the opposite edge during horizontal moves. - Replace the rectangle-approximated FH logo with strokes matching the original firehawk.sc5 artwork. The F+H is drawn as partitioned fills (crossbar arms, stem, right leg) with dedicated palette entries for the two stem/crossbar crossings, so each horizontal then each vertical shimmers in and out gap-free before the whole logo holds. Co-Authored-By: Claude Opus 4.8 --- projects/mazegame/emul/rom/mazegame.rom | Bin 262144 -> 262144 bytes projects/mazegame/mazegame.c | 85 ++++++++++++++++-------- 2 files changed, 56 insertions(+), 29 deletions(-) diff --git a/projects/mazegame/emul/rom/mazegame.rom b/projects/mazegame/emul/rom/mazegame.rom index ad5887315b499c0bf1e49d2dbb40ddbe91bf8fbc..c94b34b4b337d50e613325fe1485ab2fc4b9ef12 100644 GIT binary patch delta 3223 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 and back down to black +// together, so a stroke made of several palette entries shimmers as one shape. +static void LogoFadeSet(const u8* idx, u8 n, u8 delay_halts) { static const u8 seq[15] = {1,2,3,4,5,6,7,7,6,5,4,3,2,1,0}; for (u8 s = 0; s < 15; s++) { - u8 lvl = seq[s]; - u16 c = ((u16)lvl << 8) | ((u16)lvl << 4) | lvl; - VDP_SetPaletteEntry(idx, c); + u16 c = ((u16)seq[s] << 8) | ((u16)seq[s] << 4) | seq[s]; + for (u8 k = 0; k < n; k++) VDP_SetPaletteEntry(idx[k], c); for (u8 h = 0; h < delay_halts; h++) Halt(); } } -// GOSUB 1000 inner body: set all 4 palette entries to the same grey level, 5 Halts each. -static void LogoFadeAllColors(u8 lvl) -{ - u16 c = ((u16)lvl << 8) | ((u16)lvl << 4) | lvl; - for (u8 idx = 2; idx <= 5; idx++) { VDP_SetPaletteEntry(idx, c); Halt(); Halt(); Halt(); Halt(); Halt(); } -} +// The logo is an "F" and "H" combined, sharing the left vertical stroke and the +// middle crossbar. Each stroke shimmers in and out on its own before the whole +// logo lights up. Because the stem and crossbars overlap, the two crossing +// points get their OWN palette entries (6, 7) that are lit during BOTH the +// crossbar's turn and the stem's turn — so neither the horizontals nor the +// verticals ever show a gap. Strokes are partitioned (no shared pixels): +// pal2 = top crossbar arms pal3 = stem (excl. crossings) pal6 = top crossing +// pal4 = middle crossbar arms pal5 = right leg pal7 = middle crossing +// Origin X is even so the byte-aligned HMMV fills land on pixel boundaries. +#define LOGO_OX 110 // logo origin X (even: keeps HMMV fills pixel-aligned) +#define LOGO_OY 73 // logo origin Y static void ShowLogoAnimation(void) { @@ -756,30 +760,45 @@ static void ShowLogoAnimation(void) VDP_ClearVRAM(); VDP_DisableSprite(); - // Blank all 16 palette entries before enabling display + // Blank all 16 palette entries so the strokes stay invisible until faded in. for (u8 i = 0; i < 16; i++) VDP_SetPaletteEntry(i, 0); - // Draw FH logo using HMMV fills. - // pal2=f-crossbar, pal3=spine, pal4=h-crossbar, pal5=h-right-leg. - // Draw crossbars first, then spine overwrites their centre, then right leg. - VDP_CommandHMMV(118, 75, 28, 5, 0x22); VDP_CommandWait(); // f-crossbar - VDP_CommandHMMV(118, 90, 28, 4, 0x44); VDP_CommandWait(); // h-crossbar - VDP_CommandHMMV(122, 73, 10, 34, 0x33); VDP_CommandWait(); // spine - VDP_CommandHMMV(140, 88, 6, 19, 0x55); VDP_CommandWait(); // h right leg + // Draw the strokes. They are partitioned so no two share a pixel; the two + // stem/crossbar crossings are separate fills (pal6, pal7). Local x12..21 is + // the stem column; crossbars are split into left/right arms around it. + VDP_CommandHMMV(LOGO_OX + 12, LOGO_OY + 0, 10, 2, 0x33); VDP_CommandWait(); // stem: above top bar + VDP_CommandHMMV(LOGO_OX + 12, LOGO_OY + 7, 10, 10, 0x33); VDP_CommandWait(); // stem: between bars + VDP_CommandHMMV(LOGO_OX + 12, LOGO_OY + 21, 10, 13, 0x33); VDP_CommandWait(); // stem: below mid bar + VDP_CommandHMMV(LOGO_OX + 8, LOGO_OY + 2, 4, 5, 0x22); VDP_CommandWait(); // top bar: left arm + VDP_CommandHMMV(LOGO_OX + 22, LOGO_OY + 2, 12, 5, 0x22); VDP_CommandWait(); // top bar: right arm + VDP_CommandHMMV(LOGO_OX + 12, LOGO_OY + 2, 10, 5, 0x66); VDP_CommandWait(); // top crossing + VDP_CommandHMMV(LOGO_OX + 8, LOGO_OY + 17, 4, 4, 0x44); VDP_CommandWait(); // mid bar: left arm + VDP_CommandHMMV(LOGO_OX + 22, LOGO_OY + 17, 8, 4, 0x44); VDP_CommandWait(); // mid bar: right arm + VDP_CommandHMMV(LOGO_OX + 12, LOGO_OY + 17, 10, 4, 0x77); VDP_CommandWait(); // middle crossing + VDP_CommandHMMV(LOGO_OX + 30, LOGO_OY + 15, 4, 19, 0x55); VDP_CommandWait(); // right leg VDP_EnableDisplay(TRUE); - // Phase 1: verticals – spine then right leg, each fades in and out - LogoFade(3, 5); - LogoFade(5, 5); + // Reveal order: each horizontal shimmers, then each vertical, then the total. + // Each crossbar lights its arms + its crossing; the stem lights its body + + // both crossings — so every stroke appears as a complete shape. + static const u8 fadeTop[2] = {2, 6}; // top crossbar + static const u8 fadeMid[2] = {4, 7}; // middle crossbar + static const u8 fadeStem[3] = {3, 6, 7}; // left stem + static const u8 fadeLeg[1] = {5}; // right leg + static const u8 fadeAll[6] = {2, 3, 4, 5, 6, 7}; + LogoFadeSet(fadeTop, 2, 5); + LogoFadeSet(fadeMid, 2, 5); + LogoFadeSet(fadeStem, 3, 5); + LogoFadeSet(fadeLeg, 1, 5); - // Phase 2: horizontals – f-crossbar then h-crossbar, each fades in and out - LogoFade(2, 5); - LogoFade(4, 5); - - // Phase 3: complete FH – all four colors fade in together and hold - for (u8 lvl = 1; lvl <= 7; lvl++) LogoFadeAllColors(lvl); + // The total: all strokes fade up together and hold. + for (u8 lvl = 1; lvl <= 7; lvl++) { + u16 c = ((u16)lvl << 8) | ((u16)lvl << 4) | lvl; + for (u8 k = 0; k < 6; k++) VDP_SetPaletteEntry(fadeAll[k], c); + for (u8 h = 0; h < 5; h++) Halt(); + } for (u8 h = 0; h < 150; h++) Halt(); // Restore MSX2 default palette so SCREEN 0 text mode remains visible. @@ -961,6 +980,14 @@ void main(void) VDP_ClearVRAM(); VDP_DisableSprite(); // Software-only sprites: disable VDP hardware sprite layer + // Single-page horizontal scrolling (R#25 SP2=1). The default SP2=0 scrolls + // across TWO pages (512px), so a non-zero R#26/R#27 offset wraps the *next* + // VRAM page into the entering edge instead of column 0..63 of this page — + // which made the entering strip appear on the wrong side during horizontal + // scroll. SP2=1 makes the offset wrap within one 256px page, which is the + // behaviour the horizontal path in ScrollDraw() relies on. + VDP_SetHorizontalMode(R25_SP2); + // Software sprite: initialize direction and frame for this game. g_SprDir = SPR_SOUTH; // face south (toward exit) at game start g_SprFrame = 0;