romdevtools 0.28.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +51 -41
- package/CHANGELOG.md +46 -0
- package/README.md +3 -3
- package/examples/README.md +7 -7
- package/examples/atari2600/templates/platformer.asm +1225 -332
- package/examples/atari2600/templates/puzzle.asm +1056 -0
- package/examples/atari2600/templates/racing.asm +906 -275
- package/examples/atari2600/templates/shmup.asm +1031 -239
- package/examples/atari2600/templates/sports.asm +1135 -253
- package/examples/atari7800/templates/platformer.c +991 -156
- package/examples/atari7800/templates/puzzle.c +1091 -148
- package/examples/atari7800/templates/racing.c +952 -124
- package/examples/atari7800/templates/shmup.c +812 -134
- package/examples/atari7800/templates/sports.c +820 -184
- package/examples/c64/templates/platformer.c +879 -164
- package/examples/c64/templates/puzzle.c +855 -178
- package/examples/c64/templates/racing.c +873 -97
- package/examples/c64/templates/shmup.c +757 -161
- package/examples/c64/templates/sports.c +755 -100
- package/examples/gb/templates/platformer.c +841 -179
- package/examples/gb/templates/puzzle.c +986 -246
- package/examples/gb/templates/racing.c +754 -174
- package/examples/gb/templates/shmup.c +673 -175
- package/examples/gb/templates/sports.c +790 -159
- package/examples/gba/templates/platformer.c +626 -165
- package/examples/gba/templates/puzzle.c +519 -269
- package/examples/gba/templates/racing.c +511 -206
- package/examples/gba/templates/shmup.c +564 -179
- package/examples/gba/templates/sports.c +454 -174
- package/examples/gbc/templates/platformer.c +944 -180
- package/examples/gbc/templates/puzzle.c +363 -109
- package/examples/gbc/templates/racing.c +884 -180
- package/examples/gbc/templates/shmup.c +821 -185
- package/examples/gbc/templates/sports.c +870 -162
- package/examples/genesis/templates/platformer.c +747 -129
- package/examples/genesis/templates/puzzle.c +694 -261
- package/examples/genesis/templates/racing.c +726 -203
- package/examples/genesis/templates/shmup.c +535 -142
- package/examples/genesis/templates/sports.c +495 -158
- package/examples/gg/templates/platformer.c +880 -215
- package/examples/gg/templates/puzzle.c +875 -216
- package/examples/gg/templates/racing.c +915 -172
- package/examples/gg/templates/shmup.c +714 -191
- package/examples/gg/templates/sports.c +732 -129
- package/examples/lynx/templates/platformer.c +604 -69
- package/examples/lynx/templates/puzzle.c +498 -158
- package/examples/lynx/templates/racing.c +538 -102
- package/examples/lynx/templates/shmup.c +458 -131
- package/examples/lynx/templates/sports.c +496 -72
- package/examples/msx/platformer/main.c +649 -162
- package/examples/msx/puzzle/main.c +742 -240
- package/examples/msx/racing/main.c +669 -178
- package/examples/msx/shmup/main.c +460 -178
- package/examples/msx/sports/main.c +592 -126
- package/examples/nes/templates/platformer.c +589 -171
- package/examples/nes/templates/puzzle.c +563 -242
- package/examples/nes/templates/racing.c +502 -208
- package/examples/nes/templates/shmup.c +339 -145
- package/examples/nes/templates/sports.c +341 -183
- package/examples/pce/platformer/main.c +874 -205
- package/examples/pce/puzzle/main.c +802 -287
- package/examples/pce/racing/main.c +783 -208
- package/examples/pce/shmup/main.c +638 -212
- package/examples/pce/sports/main.c +586 -169
- package/examples/porting-across-platforms/README.md +1 -1
- package/examples/sms/templates/platformer.c +762 -177
- package/examples/sms/templates/puzzle.c +752 -212
- package/examples/sms/templates/racing.c +808 -145
- package/examples/sms/templates/shmup.c +599 -162
- package/examples/sms/templates/sports.c +630 -122
- package/examples/snes/templates/music_demo.c +7 -0
- package/examples/snes/templates/platformer-data.asm +123 -24
- package/examples/snes/templates/platformer-hdr.asm +57 -0
- package/examples/snes/templates/platformer.c +586 -165
- package/examples/snes/templates/puzzle-data.asm +116 -21
- package/examples/snes/templates/puzzle-hdr.asm +57 -0
- package/examples/snes/templates/puzzle.c +614 -235
- package/examples/snes/templates/racing-data.asm +390 -32
- package/examples/snes/templates/racing-hdr.asm +57 -0
- package/examples/snes/templates/racing.c +807 -196
- package/examples/snes/templates/shmup-data.asm +87 -29
- package/examples/snes/templates/shmup-hdr.asm +57 -0
- package/examples/snes/templates/shmup.c +459 -198
- package/examples/snes/templates/sports-data.asm +48 -2
- package/examples/snes/templates/sports-hdr.asm +57 -0
- package/examples/snes/templates/sports.c +414 -163
- package/package.json +1 -1
- package/src/host/LibretroHost.js +59 -1
- package/src/http/tool-registry.js +11 -11
- package/src/mcp/tools/cheats.js +2 -1
- package/src/mcp/tools/frame.js +3 -2
- package/src/mcp/tools/index.js +3 -3
- package/src/mcp/tools/input.js +5 -4
- package/src/mcp/tools/lifecycle.js +6 -4
- package/src/mcp/tools/platform-docs.js +1 -1
- package/src/mcp/tools/preview-tile.js +6 -2
- package/src/mcp/tools/project.js +1098 -130
- package/src/mcp/tools/rom-id.js +5 -1
- package/src/mcp/tools/run-until.js +4 -2
- package/src/mcp/tools/snippets.js +6 -6
- package/src/mcp/tools/sprite-pipeline.js +14 -2
- package/src/mcp/tools/state.js +2 -1
- package/src/mcp/tools/tile-inspect.js +8 -1
- package/src/mcp/tools/toolchain.js +12 -1
- package/src/mcp/tools/watch-memory.js +4 -3
- package/src/observer/bus.js +73 -0
- package/src/observer/livestream.html +4 -2
- package/src/observer/tool-wrap.js +17 -14
- package/src/platforms/atari7800/MENTAL_MODEL.md +5 -5
- package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
- package/src/platforms/c64/MENTAL_MODEL.md +11 -4
- package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
- package/src/platforms/gb/MENTAL_MODEL.md +3 -3
- package/src/platforms/gb/TROUBLESHOOTING.md +61 -8
- package/src/platforms/gb/lib/c/README.md +10 -11
- package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
- package/src/platforms/gb/lib/c/patch-header.js +13 -3
- package/src/platforms/gba/MENTAL_MODEL.md +4 -4
- package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
- package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
- package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
- package/src/platforms/gbc/MENTAL_MODEL.md +4 -4
- package/src/platforms/gbc/TROUBLESHOOTING.md +4 -4
- package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gbc/lib/c/README.md +10 -11
- package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
- package/src/platforms/gbc/lib/c/patch-header.js +13 -3
- package/src/platforms/genesis/MENTAL_MODEL.md +3 -3
- package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
- package/src/platforms/gg/MENTAL_MODEL.md +4 -4
- package/src/platforms/gg/TROUBLESHOOTING.md +3 -3
- package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gg/lib/c/joypad_read.c +29 -0
- package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
- package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
- package/src/platforms/msx/MENTAL_MODEL.md +5 -5
- package/src/platforms/msx/TROUBLESHOOTING.md +2 -2
- package/src/platforms/msx/lib/c/msx_hw.h +1 -0
- package/src/platforms/msx/lib/c/msx_vdp.c +25 -0
- package/src/platforms/nes/MENTAL_MODEL.md +2 -2
- package/src/platforms/nes/lib/c/nes_runtime.c +149 -34
- package/src/platforms/nes/lib/c/nes_runtime.h +34 -1
- package/src/platforms/pce/MENTAL_MODEL.md +5 -5
- package/src/platforms/pce/TROUBLESHOOTING.md +1 -1
- package/src/platforms/pce/lib/c/pce_hw.h +11 -0
- package/src/platforms/pce/lib/c/pce_video.c +32 -0
- package/src/platforms/sms/MENTAL_MODEL.md +6 -6
- package/src/platforms/snes/MENTAL_MODEL.md +2 -2
- package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
- package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
- package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
- package/src/toolchains/index.js +27 -11
|
@@ -1,103 +1,276 @@
|
|
|
1
|
-
/* ── shmup.c — Game Boy vertical
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
1
|
+
/* ── shmup.c — PHOTON DRIFT: Game Boy Color vertical shooter (complete example game) ──
|
|
2
|
+
*
|
|
3
|
+
* A COMPLETE, working game — title screen, lives, score + persistent
|
|
4
|
+
* battery hi-score (MBC1+RAM+BATTERY SRAM), GB APU music + SFX, the Game
|
|
5
|
+
* Boy's signature WINDOW-LAYER fixed HUD over a scrolling starfield — and
|
|
6
|
+
* the GBC's signature feature on top of all of it: TRUE per-tile color.
|
|
7
|
+
* The ship, its bullets, the enemies and the starfield are each REAL CGB
|
|
8
|
+
* palettes (15-bit BGR, loaded through BCPS/BCPD + OCPS/OCPD): the field is
|
|
9
|
+
* three DEPTH-BANDED blue palettes selected per BG cell through the VRAM
|
|
10
|
+
* bank-1 attribute map, and the ship (cyan), bullets (gold) and enemies
|
|
11
|
+
* (red) are their own OBJ palettes through OCPS — not a colorized
|
|
12
|
+
* monochrome game.
|
|
13
|
+
*
|
|
14
|
+
* THE GAME: a one-stick vertical shooter. The d-pad flies your ship around
|
|
15
|
+
* the lower playfield, A fires (a six-deep bullet pool), and waves of
|
|
16
|
+
* enemies drift down a parallax-banded starfield. Shoot them for points;
|
|
17
|
+
* one that reaches your ship costs a life. Three lives; the battery
|
|
18
|
+
* remembers your best run forever. SELECT toggles the music.
|
|
19
|
+
*
|
|
20
|
+
* THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
|
|
21
|
+
* very different one. The markers tell you what's what:
|
|
22
|
+
* HARDWARE IDIOM (load-bearing) — dodges a documented GB/GBC footgun;
|
|
23
|
+
* reshape your gameplay around it (see TROUBLESHOOTING before changing).
|
|
24
|
+
* GAME LOGIC (clay) — enemy patterns, scoring, tuning, art: reshape freely.
|
|
25
|
+
*
|
|
26
|
+
* SINGLE-PLAYER, honestly: the Game Boy's "player 2" is a LINK CABLE, which
|
|
27
|
+
* one emulator instance cannot provide — so handheld examples ship a
|
|
28
|
+
* press-start title and no 2P mode instead of faking one. (Consoles' shmup
|
|
29
|
+
* examples have real co-op 2P.)
|
|
30
|
+
*
|
|
31
|
+
* What depends on what:
|
|
32
|
+
* gb_hardware.h — register names (LCDC/WX/WY/VBK/BCPS/NRxx/...) + masks.
|
|
33
|
+
* gb_runtime.{h,c} — vblank wait (HALT-driven), joypad, shadow OAM + the
|
|
34
|
+
* OAM-DMA-from-HRAM routine, VRAM-safe memcpy, APU helpers (shared GB).
|
|
35
|
+
* gb_crt0.s — boot + interrupt vectors + the cartridge header window. It
|
|
36
|
+
* DECLARES the cart as MBC1+RAM+BATTERY ($0147=$03, $0149=$02): that
|
|
37
|
+
* header is what makes the SRAM hi-score persist (the GB equivalent of
|
|
38
|
+
* the NES BATTERY bit). Load-bearing; edit with TROUBLESHOOTING open.
|
|
39
|
+
* font.h — 0-9 A-Z 2bpp glyphs for all text.
|
|
40
|
+
*
|
|
41
|
+
* The starfield fills the FULL 32-row BG map (not just the visible 18)
|
|
42
|
+
* because the uint8 SCY scroll wraps through all 32 — a part-filled map
|
|
43
|
+
* scrolls garbage into view. The color travels with the tiles: each cell's
|
|
44
|
+
* bank-1 attribute byte scrolls along with its tile, so a "far" depth band
|
|
45
|
+
* stays its dim blue wherever it slides under the screen.
|
|
46
|
+
*
|
|
47
|
+
* WRAM NOTE: build with dataLoc:0xC200 so our statics sit ABOVE shadow_oam
|
|
48
|
+
* ($C100) — else oam_clear() would zero our state. The project recipe sets
|
|
49
|
+
* that automatically.
|
|
15
50
|
*/
|
|
16
51
|
|
|
17
52
|
#include "gb_hardware.h"
|
|
18
53
|
#include "gb_runtime.h"
|
|
54
|
+
#include "font.h"
|
|
19
55
|
|
|
20
|
-
|
|
21
|
-
|
|
56
|
+
/* The title screen renders this — examples({op:'fork'}) stamps your game's
|
|
57
|
+
* name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
|
|
58
|
+
#define GAME_TITLE "PHOTON DRIFT"
|
|
22
59
|
|
|
60
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
61
|
+
* Tile inventory. GB/GBC tiles are 16 bytes: 8 rows × [low-plane byte,
|
|
62
|
+
* high-plane byte]. Pixel colour index = (hi_bit << 1) | lo_bit (0..3); on
|
|
63
|
+
* CGB that index selects a colour WITHIN whichever CGB palette the cell's
|
|
64
|
+
* bank-1 attribute (BG) or the sprite's OAM attr (OBJ) chose. So one ship
|
|
65
|
+
* tile reads cyan or any other palette purely by its attribute byte. */
|
|
23
66
|
static const uint8_t tile_blank[16] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 };
|
|
24
|
-
static const uint8_t tile_ship[16] = {
|
|
25
|
-
0x18,0x18,
|
|
26
|
-
|
|
27
|
-
};
|
|
28
|
-
/* ── BG tiles (starfield) ─────────────────────────────────────────────
|
|
29
|
-
* The background is a real starfield so the screen is never one flat
|
|
30
|
-
* colour (LCDC_BG_ON below — drop it and the screen reads as blank, the
|
|
31
|
-
* #1 GB "why is it blank" footgun).
|
|
32
|
-
* tile_space — a 50/50 dither of palette colours 0 (deep space blue) +
|
|
33
|
-
* 1 (mid blue), so even an empty patch of space mixes two
|
|
34
|
-
* shades and never lets one colour dominate the frame.
|
|
35
|
-
* tile_star — a bright colour-3 (white) "+" star on the dithered field. */
|
|
36
|
-
static const uint8_t tile_space[16] = {
|
|
37
|
-
0x55,0x00, 0xAA,0x00, 0x55,0x00, 0xAA,0x00,
|
|
38
|
-
0x55,0x00, 0xAA,0x00, 0x55,0x00, 0xAA,0x00,
|
|
67
|
+
static const uint8_t tile_ship[16] = { /* arrowhead fighter */
|
|
68
|
+
0x18,0x18, 0x18,0x18, 0x3C,0x24, 0x3C,0x24,
|
|
69
|
+
0x7E,0x5A, 0xFF,0xDB, 0xFF,0xA5, 0x66,0x66,
|
|
39
70
|
};
|
|
40
|
-
static const uint8_t
|
|
41
|
-
0x10,0x10, 0x10,0x10, 0x54,0x54, 0x38,0x38,
|
|
42
|
-
0x54,0x54, 0x10,0x10, 0x10,0x10, 0x00,0x00,
|
|
43
|
-
};
|
|
44
|
-
#define T_SPACE 4
|
|
45
|
-
#define T_STAR 5
|
|
46
|
-
static const uint8_t tile_bullet[16] = {
|
|
71
|
+
static const uint8_t tile_bullet[16] = { /* bright bolt (value 3) */
|
|
47
72
|
0x00,0x00, 0x18,0x18, 0x3C,0x3C, 0x3C,0x3C,
|
|
48
73
|
0x3C,0x3C, 0x3C,0x3C, 0x18,0x18, 0x00,0x00,
|
|
49
74
|
};
|
|
50
|
-
static const uint8_t tile_enemy[16] = {
|
|
51
|
-
0x81,0x81, 0x42,
|
|
52
|
-
0xFF,0xFF, 0x24,
|
|
75
|
+
static const uint8_t tile_enemy[16] = { /* spiky drone (value 3) */
|
|
76
|
+
0x81,0x81, 0x42,0x5A, 0x24,0x3C, 0xFF,0xFF,
|
|
77
|
+
0xFF,0xFF, 0x24,0x3C, 0x42,0x5A, 0x81,0x81,
|
|
78
|
+
};
|
|
79
|
+
/* Starfield BG tiles. tile_space carries two value-1 specks so even "empty"
|
|
80
|
+
* space is never one flat colour (the render-health floor every example
|
|
81
|
+
* keeps), and the specks make vertical scroll motion visible everywhere. */
|
|
82
|
+
static const uint8_t tile_space[16] = { /* faint specks (value 1) */
|
|
83
|
+
0x00,0x00, 0x08,0x00, 0x00,0x00, 0x00,0x00,
|
|
84
|
+
0x40,0x00, 0x00,0x00, 0x02,0x00, 0x00,0x00,
|
|
85
|
+
};
|
|
86
|
+
static const uint8_t tile_star[16] = { /* value-2 dot */
|
|
87
|
+
0x00,0x00, 0x00,0x00, 0x18,0x00, 0x3C,0x00,
|
|
88
|
+
0x3C,0x00, 0x18,0x00, 0x00,0x00, 0x00,0x00,
|
|
89
|
+
};
|
|
90
|
+
static const uint8_t tile_brite[16] = { /* value-3 "+" twinkle */
|
|
91
|
+
0x00,0x00, 0x18,0x18, 0x18,0x18, 0x7E,0x7E,
|
|
92
|
+
0x7E,0x7E, 0x18,0x18, 0x18,0x18, 0x00,0x00,
|
|
53
93
|
};
|
|
94
|
+
static const uint8_t tile_hudbar[16] = { /* solid value-3 divider */
|
|
95
|
+
0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF,
|
|
96
|
+
0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/* Tile indices ($8000 unsigned addressing — LCDC bit 4 set below). Sprites
|
|
100
|
+
* and BG share the $8000 table in this layout, so one upload serves both.
|
|
101
|
+
* Font glyphs follow at FONT_BASE (digits 0-9, then A-Z). */
|
|
102
|
+
#define T_BLANK 0
|
|
103
|
+
#define T_SHIP 1
|
|
104
|
+
#define T_BULLET 2
|
|
105
|
+
#define T_ENEMY 3
|
|
106
|
+
#define T_SPACE 4
|
|
107
|
+
#define T_STAR 5
|
|
108
|
+
#define T_BRITE 6
|
|
109
|
+
#define T_HUDBAR 7
|
|
110
|
+
#define FONT_BASE 16 /* digit d = 16+d, letter L = 16+10+idx (see font.h) */
|
|
111
|
+
|
|
112
|
+
/* ── GAME LOGIC (clay — reshape freely) ── the CGB palette TABLE (the colours
|
|
113
|
+
* themselves are art; the LOADER below is the hardware idiom).
|
|
114
|
+
* 15-bit BGR: 5 bits each, blue in the high bits — RGB() packs it. Colour 0
|
|
115
|
+
* of a BG palette is the cell's "background" shade; for OBJ palettes colour 0
|
|
116
|
+
* is transparent (the scene shows through). */
|
|
117
|
+
#define RGB(r,g,b) ((uint16_t)(((uint16_t)(b)<<10)|((uint16_t)(g)<<5)|(r)))
|
|
54
118
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
119
|
+
/* BG palette slots (bank-1 attribute byte bits 0-2 select one of these).
|
|
120
|
+
* The three depth bands give the parallax starfield real distance — and they
|
|
121
|
+
* are genuinely DIFFERENT HUES (deep indigo far, teal-cyan mid, magenta-violet
|
|
122
|
+
* near), not three shades of one blue. So the field reads as a colourful
|
|
123
|
+
* nebula band, and a wide-area hue census sees several distinct colours — the
|
|
124
|
+
* proof the cart is doing per-tile CGB colour, not 4-shade-green DMG. */
|
|
125
|
+
#define PAL_FAR 0 /* deep blue distance */
|
|
126
|
+
#define PAL_MID 1 /* teal mid band */
|
|
127
|
+
#define PAL_GRN 2 /* green inner band */
|
|
128
|
+
#define PAL_NEAR 4 /* magenta-violet foreground band */
|
|
129
|
+
#define PAL_HUD 3 /* HUD bar + all text */
|
|
130
|
+
|
|
131
|
+
static const uint16_t bg_palettes[8][4] = {
|
|
132
|
+
/* 0 far */ { RGB(2,3,14), RGB(5,8,24), RGB(8,12,30), RGB(18,20,31) },
|
|
133
|
+
/* 1 mid */ { RGB(1,8,9), RGB(3,20,22), RGB(6,30,30), RGB(20,31,31) },
|
|
134
|
+
/* 2 grn */ { RGB(2,9,3), RGB(6,22,7), RGB(10,31,12), RGB(22,31,20) },
|
|
135
|
+
/* 3 hud */ { RGB(2,2,6), RGB(8,9,16), RGB(2,2,6), RGB(31,31,31) },
|
|
136
|
+
/* 4 near */ { RGB(12,1,12), RGB(26,3,24), RGB(31,8,26), RGB(31,22,31) },
|
|
137
|
+
/* 5 spare*/ { RGB(0,0,0), RGB(10,10,10), RGB(20,20,20), RGB(31,31,31) },
|
|
138
|
+
/* 6 spare*/ { RGB(0,0,0), RGB(10,10,10), RGB(20,20,20), RGB(31,31,31) },
|
|
139
|
+
/* 7 spare*/ { RGB(0,0,0), RGB(10,10,10), RGB(20,20,20), RGB(31,31,31) },
|
|
60
140
|
};
|
|
61
141
|
|
|
62
|
-
/*
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
142
|
+
/* OBJ palette slots (OAM attr bits 0-2 select one of these). Colour 0 is
|
|
143
|
+
* always transparent. */
|
|
144
|
+
#define OPAL_SHIP 0 /* cyan hero */
|
|
145
|
+
#define OPAL_BULLET 1 /* gold bolt */
|
|
146
|
+
#define OPAL_ENEMY 2 /* danger red drone */
|
|
147
|
+
|
|
148
|
+
static const uint16_t obj_palettes[8][4] = {
|
|
149
|
+
/* 0 ship */ { 0, RGB(8,28,31), RGB(2,16,28), RGB(28,31,31) },
|
|
150
|
+
/* 1 bullet */ { 0, RGB(31,28,6), RGB(31,20,2), RGB(31,31,20) },
|
|
151
|
+
/* 2 enemy */ { 0, RGB(31,8,8), RGB(20,2,2), RGB(31,24,16) },
|
|
152
|
+
/* 3 spare */ { 0, RGB(20,20,20), RGB(31,31,31), RGB(10,10,10) },
|
|
153
|
+
/* 4 spare */ { 0, RGB(20,20,20), RGB(31,31,31), RGB(10,10,10) },
|
|
154
|
+
/* 5 spare */ { 0, RGB(20,20,20), RGB(31,31,31), RGB(10,10,10) },
|
|
155
|
+
/* 6 spare */ { 0, RGB(20,20,20), RGB(31,31,31), RGB(10,10,10) },
|
|
156
|
+
/* 7 spare */ { 0, RGB(20,20,20), RGB(31,31,31), RGB(10,10,10) },
|
|
68
157
|
};
|
|
69
158
|
|
|
70
|
-
|
|
159
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
160
|
+
* THE WINDOW-LAYER HUD — the Game Boy's signature "fixed HUD over a
|
|
161
|
+
* scrolling world" technique. The window is a second BG plane with its own
|
|
162
|
+
* 32×32 tile map and NO scroll registers: it always draws its map from
|
|
163
|
+
* (0,0), pinned to the screen, on top of the BG. So the HUD lives in the
|
|
164
|
+
* window and the playfield lives in the BG — SCY scrolls the starfield all
|
|
165
|
+
* it likes and the HUD never moves. No raster splits, no IRQ timing (the NES
|
|
166
|
+
* needs a sprite-0 polling dance for this exact effect; on GB it's three
|
|
167
|
+
* register writes). On CGB the window cells take bank-1 palette attributes
|
|
168
|
+
* exactly like the BG (set_wcell writes both banks).
|
|
169
|
+
*
|
|
170
|
+
* The three registers, and their two famous footguns:
|
|
171
|
+
* WY ($FF4A) — first screen LINE the window covers. We use 128: lines
|
|
172
|
+
* 0-127 are playfield, 128-143 (two tile rows) are the HUD strip.
|
|
173
|
+
* WX ($FF4B) — screen column PLUS SEVEN. WX=7 means "left edge". The -7
|
|
174
|
+
* offset is hardware fact: WX=0..6 glitches, WX≥167 is off-screen.
|
|
175
|
+
* LCDC bit 5 — window enable; bit 6 — which map it reads ($9800/$9C00).
|
|
176
|
+
*
|
|
177
|
+
* FOOTGUN 1 — "the window ate the bottom of my screen": once the window
|
|
178
|
+
* starts on a line it covers EVERY line from there DOWN, full width. There
|
|
179
|
+
* is no window height register. That is why GB HUDs sit at the BOTTOM of the
|
|
180
|
+
* screen. A TOP HUD needs a STAT-interrupt LYC trick — a different, fragile
|
|
181
|
+
* idiom; don't drift into it by accident by setting WY=0.
|
|
182
|
+
*
|
|
183
|
+
* FOOTGUN 2 — sprites are NOT clipped by the window. OBJs draw over it, so a
|
|
184
|
+
* sprite below line 128 sits ON the HUD. Gameplay despawns every enemy and
|
|
185
|
+
* clamps the ship above PLAY_H, so nothing overlaps the HUD strip.
|
|
186
|
+
*
|
|
187
|
+
* Requires: window map at $9C00 (LCDC bit 6), tile data at $8000 (bit 4),
|
|
188
|
+
* WX=7, WY=PLAY_H, LCDC bit 5 set during play (title turns the window off). */
|
|
189
|
+
#define PLAY_H 128 /* first HUD line = window top */
|
|
190
|
+
#define LCDC_TITLE (LCDC_LCD_ON | LCDC_BG_ON | LCDC_OBJ_ON | LCDC_TILE_DATA_LO)
|
|
191
|
+
#define LCDC_PLAY (LCDC_TITLE | LCDC_WINDOW_ON | LCDC_WINDOW_MAP_HI)
|
|
192
|
+
|
|
193
|
+
#define VRAM ((volatile uint8_t *)0x9800) /* BG map $9800 base */
|
|
194
|
+
#define WIN_OFF 0x400 /* window map $9C00 = $9800 + $400 */
|
|
195
|
+
|
|
196
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
197
|
+
* BATTERY SRAM — persistent hi-score. MBC1 cart RAM is 8KB at $A000-$BFFF,
|
|
198
|
+
* but it boots DISABLED and writes to a disabled bank are silently
|
|
199
|
+
* discarded (reads float). The gate is the MBC's RAM-enable register: any
|
|
200
|
+
* WRITE to ROM space $0000-$1FFF with $0A in the low nibble enables the RAM;
|
|
201
|
+
* writing $00 disables it again. (Writing "into ROM" feels wrong the first
|
|
202
|
+
* time — ROM-area writes never touch ROM, they talk to the mapper chip.)
|
|
203
|
+
* Leaving RAM enabled all the time "works" in emulators but on real hardware
|
|
204
|
+
* risks corruption at power-off — battery carts since forever do
|
|
205
|
+
* enable → touch → disable, so we do too.
|
|
206
|
+
*
|
|
207
|
+
* First boot is GARBAGE, not zeros: battery RAM holds whatever the silicon
|
|
208
|
+
* woke up with. The magic 'H','S' + checksum is how the load path tells "my
|
|
209
|
+
* save" from "factory noise" — without it a fresh cart shows a junk hi-score.
|
|
210
|
+
*
|
|
211
|
+
* Save block at $A000: 'H' 'S' lo hi (lo^hi^$A5)
|
|
212
|
+
*
|
|
213
|
+
* Requires: gb_crt0.s declaring $0147=$03 (MBC1+RAM+BATTERY) + $0149=$02
|
|
214
|
+
* (8KB) — those header bytes are how the emulator knows to allocate and
|
|
215
|
+
* persist SAVE_RAM. Verify headlessly: play, game over, then
|
|
216
|
+
* memory({op:'read', region:'save_ram'}) shows the block, and the hi-score
|
|
217
|
+
* survives host.hardReset(). */
|
|
218
|
+
#define MBC_RAM_ENABLE (*(volatile uint8_t *)0x0000)
|
|
219
|
+
#define SRAM ((volatile uint8_t *)0xA000)
|
|
220
|
+
|
|
221
|
+
static uint16_t hiscore_load(void) {
|
|
222
|
+
uint16_t v = 0;
|
|
223
|
+
MBC_RAM_ENABLE = 0x0A; /* unlock cart RAM */
|
|
224
|
+
if (SRAM[0] == 'H' && SRAM[1] == 'S' &&
|
|
225
|
+
SRAM[4] == (uint8_t)(SRAM[2] ^ SRAM[3] ^ 0xA5)) {
|
|
226
|
+
v = (uint16_t)(SRAM[2] | ((uint16_t)SRAM[3] << 8));
|
|
227
|
+
}
|
|
228
|
+
MBC_RAM_ENABLE = 0x00; /* re-lock (battery hygiene) */
|
|
229
|
+
return v;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
static void hiscore_save(uint16_t v) {
|
|
233
|
+
uint8_t lo = (uint8_t)(v & 0xFF), hi = (uint8_t)(v >> 8);
|
|
234
|
+
MBC_RAM_ENABLE = 0x0A;
|
|
235
|
+
SRAM[0] = 'H'; SRAM[1] = 'S';
|
|
236
|
+
SRAM[2] = lo; SRAM[3] = hi;
|
|
237
|
+
SRAM[4] = (uint8_t)(lo ^ hi ^ 0xA5);
|
|
238
|
+
MBC_RAM_ENABLE = 0x00;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
242
|
+
* Object pools — fixed slots, no allocation. OAM slot plan (40 hardware
|
|
243
|
+
* slots, we use 13): 0 = ship, 1-6 bullets, 7-12 enemies. Sub-10 sprites on
|
|
244
|
+
* any one scanline keeps us clear of the 10-OBJ/line hardware drop. */
|
|
245
|
+
#define MAX_BULLETS 6
|
|
246
|
+
#define MAX_ENEMIES 6
|
|
247
|
+
#define START_LIVES 3
|
|
71
248
|
|
|
72
|
-
|
|
249
|
+
typedef struct { uint8_t x, y, alive; } Obj; /* screen coords (not OAM) */
|
|
250
|
+
|
|
251
|
+
static Obj ship;
|
|
73
252
|
static Obj bullets[MAX_BULLETS];
|
|
74
253
|
static Obj enemies[MAX_ENEMIES];
|
|
254
|
+
static uint8_t lives;
|
|
75
255
|
static uint16_t score;
|
|
256
|
+
static uint16_t hiscore; /* live HUD readout: max(score, record) */
|
|
257
|
+
static uint16_t record; /* what the battery SRAM actually holds */
|
|
258
|
+
static uint8_t fire_cd;
|
|
76
259
|
static uint8_t spawn_timer;
|
|
260
|
+
static uint8_t scroll_y; /* starfield drift, committed to SCY */
|
|
261
|
+
static uint8_t prev_pad;
|
|
262
|
+
static uint8_t hud_dirty; /* queue VRAM writes; vblank commits them */
|
|
263
|
+
static uint8_t msg_stage; /* game-over text: 2 = line 1 pending, 1 = line 2 */
|
|
264
|
+
static uint8_t msg_row; /* BG map row for GAME OVER (scroll-aware) */
|
|
77
265
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
static void fire(void) {
|
|
86
|
-
uint8_t i;
|
|
87
|
-
for (i = 0; i < MAX_BULLETS; i++) {
|
|
88
|
-
if (!bullets[i].alive) {
|
|
89
|
-
bullets[i].x = player.x;
|
|
90
|
-
bullets[i].y = player.y - 8;
|
|
91
|
-
bullets[i].alive = 1;
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
266
|
+
/* Game states — the shell every example shares: title → play → game over.
|
|
267
|
+
* (Handheld adaptation: title is press-start; consoles add a 1P/2P pick.) */
|
|
268
|
+
#define ST_TITLE 0
|
|
269
|
+
#define ST_PLAY 1
|
|
270
|
+
#define ST_OVER 2
|
|
271
|
+
static uint8_t state;
|
|
96
272
|
|
|
97
|
-
/* Galois LFSR (taps $B8), period 255
|
|
98
|
-
* The old code derived the spawn column from spawn_timer, but the caller
|
|
99
|
-
* resets spawn_timer just before calling here, so it was CONSTANT and
|
|
100
|
-
* every enemy spawned in the same left column/lane. */
|
|
273
|
+
/* ── GAME LOGIC (clay) — Galois LFSR (taps $B8), period 255 ── */
|
|
101
274
|
static uint8_t rng_state = 0xA5;
|
|
102
275
|
static uint8_t rand8(void) {
|
|
103
276
|
uint8_t lsb = (uint8_t)(rng_state & 1);
|
|
@@ -106,129 +279,592 @@ static uint8_t rand8(void) {
|
|
|
106
279
|
return rng_state;
|
|
107
280
|
}
|
|
108
281
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
282
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
283
|
+
* CGB palette RAM — the BCPS/BCPD (BG) and OCPS/OCPD (OBJ) port pairs.
|
|
284
|
+
* requires: a .gbc build (CGB flag $0143 set — the build pipeline does it);
|
|
285
|
+
* on a DMG build these registers are dead and you get 4-shade green.
|
|
286
|
+
*
|
|
287
|
+
* Palette RAM is NOT memory-mapped: it's 64 bytes (8 palettes × 4 colours ×
|
|
288
|
+
* 2 bytes, little-endian 15-bit BGR) behind an index/data port pair.
|
|
289
|
+
* BCPS = 0x80 | index — set write index; bit 7 = AUTO-INCREMENT, so a
|
|
290
|
+
* burst of BCPD writes walks the whole 64 bytes.
|
|
291
|
+
* BCPD = low byte; BCPD = high byte; ... 32 times = all 8 palettes.
|
|
292
|
+
*
|
|
293
|
+
* TIMING FOOTGUN: palette RAM belongs to the PPU. Writes during active
|
|
294
|
+
* display (mode 3) are IGNORED on real hardware — same constraint as VRAM.
|
|
295
|
+
* Load palettes with the LCD OFF (boot / transitions, as here) or inside
|
|
296
|
+
* vblank. A palette "fade" = a few BCPD writes per vblank, never a mid-frame
|
|
297
|
+
* burst. */
|
|
298
|
+
static void load_bg_palettes(void) {
|
|
299
|
+
uint8_t p, i;
|
|
300
|
+
BCPS = 0x80; /* index 0, auto-increment on */
|
|
301
|
+
for (p = 0; p < 8; p++)
|
|
302
|
+
for (i = 0; i < 4; i++) {
|
|
303
|
+
BCPD = (uint8_t)(bg_palettes[p][i] & 0xFF);
|
|
304
|
+
BCPD = (uint8_t)((bg_palettes[p][i] >> 8) & 0xFF);
|
|
118
305
|
}
|
|
119
306
|
}
|
|
120
307
|
|
|
308
|
+
static void load_obj_palettes(void) {
|
|
309
|
+
uint8_t p, i;
|
|
310
|
+
OCPS = 0x80;
|
|
311
|
+
for (p = 0; p < 8; p++)
|
|
312
|
+
for (i = 0; i < 4; i++) {
|
|
313
|
+
OCPD = (uint8_t)(obj_palettes[p][i] & 0xFF);
|
|
314
|
+
OCPD = (uint8_t)((obj_palettes[p][i] >> 8) & 0xFF);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/* ── GAME LOGIC (clay) — VRAM upload + text helpers ──────────────────────────
|
|
319
|
+
* All of these write VRAM, so they run with the LCD OFF (boot/repaints) or
|
|
320
|
+
* inside vblank (the HUD digit commit). memcpy_vram walks a pointer
|
|
321
|
+
* (*dst++ = v) — never index dst[i] through a VRAM pointer (SDCC's sm83 port
|
|
322
|
+
* miscompiles indexed stores through VRAM-pointing pointers). */
|
|
121
323
|
static void upload_tile(uint8_t slot, const uint8_t *src) {
|
|
122
|
-
|
|
123
|
-
/* memcpy_vram (pointer-walk) — NOT an indexed dst[i]=src[i] loop, which
|
|
124
|
-
* SDCC sm83 miscompiles when dst points into VRAM ($8000-$9FFF). */
|
|
125
|
-
memcpy_vram(dst, src, 16);
|
|
324
|
+
memcpy_vram((uint8_t *)(0x8000 + (uint16_t)slot * 16), src, 16);
|
|
126
325
|
}
|
|
127
326
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
uint8_t *
|
|
133
|
-
|
|
134
|
-
for (r = 0; r < 18; r++)
|
|
135
|
-
for (c = 0; c < 20; c++)
|
|
136
|
-
bg[r * 32 + c] = ((r * 7 + c * 5) % 11 == 0) ? T_STAR : T_SPACE;
|
|
327
|
+
static void upload_font(void) {
|
|
328
|
+
uint8_t g;
|
|
329
|
+
/* font.h glyphs are already 2bpp (16 bytes each) — straight copy. */
|
|
330
|
+
for (g = 0; g < FONT_GLYPHS; g++)
|
|
331
|
+
memcpy_vram((uint8_t *)(0x8000 + (uint16_t)(FONT_BASE + g) * 16),
|
|
332
|
+
&font_data[g * 16], 16);
|
|
137
333
|
}
|
|
138
334
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
335
|
+
static uint8_t char_tile(char ch) {
|
|
336
|
+
if (ch >= '0' && ch <= '9') return (uint8_t)(FONT_BASE + (ch - '0'));
|
|
337
|
+
if (ch >= 'A' && ch <= 'Z') return (uint8_t)(FONT_BASE + 10 + (ch - 'A'));
|
|
338
|
+
return T_BLANK; /* space / unknown → blank */
|
|
339
|
+
}
|
|
142
340
|
|
|
143
|
-
|
|
144
|
-
|
|
341
|
+
/* Pre-convert a string to tile indices at full-frame time, so the vblank
|
|
342
|
+
* commit (commit_bg_text) is a dumb byte copy — see game_over(). */
|
|
343
|
+
static uint8_t msg_q[20]; /* 9 "GAME OVER" + 11 "PRESS START" */
|
|
344
|
+
static void stage_text(const char *s, uint8_t *out) {
|
|
345
|
+
while (*s) *out++ = char_tile(*s++);
|
|
346
|
+
}
|
|
145
347
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
348
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
349
|
+
* Per-tile color — the VRAM bank-1 attribute map (VBK register).
|
|
350
|
+
* requires: CGB mode (see the palette idiom above); writes in a VRAM-safe
|
|
351
|
+
* window (LCD off, or a bounded vblank batch).
|
|
352
|
+
*
|
|
353
|
+
* VRAM is TWO 8KB banks behind the same $8000-$9FFF window; VBK ($FF4F)
|
|
354
|
+
* selects which one the CPU sees. Bank 0 holds what the DMG had: tile pixels
|
|
355
|
+
* + the tile-index maps. Bank 1 at the SAME map address holds one ATTRIBUTE
|
|
356
|
+
* byte per cell:
|
|
357
|
+
* bits 0-2 palette 0-7 ← this game's whole color system
|
|
358
|
+
* bit 3 tile VRAM bank
|
|
359
|
+
* bit 5/6 H/V flip
|
|
360
|
+
* bit 7 BG-over-OBJ priority
|
|
361
|
+
* So coloring a cell is a write PAIR: tile index with VBK=0, attribute with
|
|
362
|
+
* VBK=1, at the SAME offset.
|
|
363
|
+
*
|
|
364
|
+
* FOOTGUN: VBK is global state. Forget to restore VBK=0 and every later
|
|
365
|
+
* "tile" write lands in the attribute map — the screen turns into garbage
|
|
366
|
+
* colors while the tile data you wrote is simply gone. Always end VBK=0
|
|
367
|
+
* (every routine here does). */
|
|
368
|
+
static void set_cell(uint8_t mx, uint8_t my, uint8_t tile, uint8_t pal) {
|
|
369
|
+
uint16_t off = (uint16_t)my * 32 + mx;
|
|
370
|
+
VBK = 0;
|
|
371
|
+
VRAM[off] = tile;
|
|
372
|
+
VBK = 1;
|
|
373
|
+
VRAM[off] = pal;
|
|
374
|
+
VBK = 0;
|
|
375
|
+
}
|
|
152
376
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
377
|
+
/* same write-pair, into the WINDOW's map at $9C00 (window HUD idiom) */
|
|
378
|
+
static void set_wcell(uint8_t wx, uint8_t wy, uint8_t tile, uint8_t pal) {
|
|
379
|
+
uint16_t off = WIN_OFF + (uint16_t)wy * 32 + wx;
|
|
380
|
+
VBK = 0;
|
|
381
|
+
VRAM[off] = tile;
|
|
382
|
+
VBK = 1;
|
|
383
|
+
VRAM[off] = pal;
|
|
384
|
+
VBK = 0;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/* draw a NUL-terminated string into the BG map (palette PAL_HUD = readable). */
|
|
388
|
+
static void draw_text(uint8_t col, uint8_t row, const char *s) {
|
|
389
|
+
uint8_t i;
|
|
390
|
+
for (i = 0; s[i] != 0; i++)
|
|
391
|
+
set_cell((uint8_t)(col + i), row, char_tile(s[i]), PAL_HUD);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/* draw a NUL-terminated string into the WINDOW map. */
|
|
395
|
+
static void draw_wtext(uint8_t col, uint8_t row, const char *s) {
|
|
396
|
+
uint8_t i;
|
|
397
|
+
for (i = 0; s[i] != 0; i++)
|
|
398
|
+
set_wcell((uint8_t)(col + i), row, char_tile(s[i]), PAL_HUD);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/* Decimal digits WITHOUT divide/modulo (the sm83 has neither — SDCC's
|
|
402
|
+
* software % costs ~700 cycles a call). Repeated power-of-ten subtraction
|
|
403
|
+
* caps at 36 SUBs for any u16. Writes 5 tile slots into out5. */
|
|
404
|
+
static void u16_to_tiles(uint16_t v, uint8_t *out5) {
|
|
405
|
+
static const uint16_t pow10[4] = { 10000, 1000, 100, 10 };
|
|
406
|
+
uint8_t i, d;
|
|
407
|
+
for (i = 0; i < 4; i++) {
|
|
408
|
+
d = 0;
|
|
409
|
+
while (v >= pow10[i]) { v -= pow10[i]; ++d; }
|
|
410
|
+
*out5++ = (uint8_t)(FONT_BASE + d);
|
|
411
|
+
}
|
|
412
|
+
*out5 = (uint8_t)(FONT_BASE + (uint8_t)v);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
416
|
+
* The PARALLAX-BANDED starfield — the game's CGB colour proof in the BG.
|
|
417
|
+
* The field fills the FULL 32-row map; SCY scrolling wraps through all 32,
|
|
418
|
+
* so a part-filled map would scroll garbage into view. Each cell takes a
|
|
419
|
+
* tile (bank 0) AND a depth-band palette attribute (bank 1) — that pairing
|
|
420
|
+
* is the whole CGB colour story.
|
|
421
|
+
*
|
|
422
|
+
* PERF FOOTGUN (measured, not theoretical): the obvious pattern formula
|
|
423
|
+
* `(r*7 + c*5) % 11` calls SDCC's software modulo (~700 cycles) — 1024 times
|
|
424
|
+
* over a 32×32 map ≈ a multi-frame frozen boot. The fix is the classic 8-bit
|
|
425
|
+
* move: keep running counters and subtract on overflow (zero divisions). The
|
|
426
|
+
* sm83 has no divide instruction; treat every / and % in a loop as a red flag.
|
|
427
|
+
*
|
|
428
|
+
* The DEPTH BAND is chosen by the map ROW in a repeating 4-row cycle
|
|
429
|
+
* (blue, teal, green, magenta) so FOUR distinct nebula hues are ALWAYS on
|
|
430
|
+
* screen at once — the field reads as a banded nebula and a wide hue census
|
|
431
|
+
* sees several distinct colours regardless of where SCY has scrolled. As the
|
|
432
|
+
* field scrolls the bands scroll with it (the attribute byte rides the tile).
|
|
433
|
+
* 32 rows IS a multiple of 4, so the band cycle wraps seamlessly at the map
|
|
434
|
+
* seam too. */
|
|
435
|
+
static const uint8_t band_cycle[4] = { PAL_FAR, PAL_MID, PAL_GRN, PAL_NEAR };
|
|
436
|
+
static uint8_t band_for_row(uint8_t r) {
|
|
437
|
+
return band_cycle[r & 3]; /* r mod 4 — divide-free */
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
static void paint_starfield(void) {
|
|
441
|
+
uint8_t r, c, t, pal;
|
|
442
|
+
uint8_t ar = 0, br = 0; /* row seeds: (r*7) mod 11, (r*3) mod 29 */
|
|
443
|
+
uint8_t a, b;
|
|
444
|
+
for (r = 0; r < 32; r++) {
|
|
445
|
+
a = ar; b = br;
|
|
446
|
+
pal = band_for_row(r);
|
|
447
|
+
for (c = 0; c < 32; c++) {
|
|
448
|
+
t = T_SPACE;
|
|
449
|
+
if (a == 0) t = T_STAR;
|
|
450
|
+
if (b == 0) t = T_BRITE;
|
|
451
|
+
VBK = 0; VRAM[(uint16_t)r * 32 + c] = t;
|
|
452
|
+
VBK = 1; VRAM[(uint16_t)r * 32 + c] = pal;
|
|
453
|
+
a += 5; if (a >= 11) a -= 11; /* +5 ≡ c step, mod 11 */
|
|
454
|
+
b += 13; if (b >= 29) b -= 29; /* +13 ≡ c step, mod 29 */
|
|
158
455
|
}
|
|
456
|
+
ar += 7; if (ar >= 11) ar -= 11; /* +7 ≡ r step, mod 11 */
|
|
457
|
+
br += 3; if (br >= 29) br -= 29; /* +3 ≡ r step, mod 29 */
|
|
458
|
+
}
|
|
459
|
+
VBK = 0;
|
|
460
|
+
}
|
|
159
461
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
462
|
+
static void paint_title(void) {
|
|
463
|
+
paint_starfield(); /* banded field — text owns the top */
|
|
464
|
+
draw_text((uint8_t)((20 - (sizeof(GAME_TITLE) - 1)) / 2), 3, GAME_TITLE);
|
|
465
|
+
draw_text(4, 8, "PRESS START");
|
|
466
|
+
draw_text(6, 11, "HI");
|
|
467
|
+
{
|
|
468
|
+
uint8_t d[5], i;
|
|
469
|
+
u16_to_tiles(hiscore, d);
|
|
470
|
+
for (i = 0; i < 5; i++) set_cell((uint8_t)(9 + i), 11, d[i], PAL_HUD);
|
|
471
|
+
}
|
|
472
|
+
draw_text(6, 14, "1P ONLY"); /* see header: no link 2P */
|
|
473
|
+
SCY = 0; SCX = 0;
|
|
474
|
+
scroll_y = 0;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/* HUD strip = window rows 0-1: a solid divider bar, then the text row.
|
|
478
|
+
* Columns 0-19 are the visible 20 (WX=7 pins map col 0 to screen x 0). */
|
|
479
|
+
static void paint_hud(void) {
|
|
480
|
+
uint8_t c, d[5], i;
|
|
481
|
+
for (c = 0; c < 20; c++) set_wcell(c, 0, T_HUDBAR, PAL_HUD);
|
|
482
|
+
for (c = 0; c < 20; c++) set_wcell(c, 1, T_BLANK, PAL_HUD);
|
|
483
|
+
draw_wtext(0, 1, "SC");
|
|
484
|
+
u16_to_tiles(score, d);
|
|
485
|
+
for (i = 0; i < 5; i++) set_wcell((uint8_t)(3 + i), 1, d[i], PAL_HUD);
|
|
486
|
+
draw_wtext(9, 1, "HI");
|
|
487
|
+
u16_to_tiles(hiscore, d);
|
|
488
|
+
for (i = 0; i < 5; i++) set_wcell((uint8_t)(12 + i), 1, d[i], PAL_HUD);
|
|
489
|
+
draw_wtext(18, 1, "L");
|
|
490
|
+
set_wcell(19, 1, (uint8_t)(FONT_BASE + lives), PAL_HUD);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
494
|
+
* LCD-off repaints. Bulk VRAM rewrites (full title/field repaints) happen
|
|
495
|
+
* with the LCD OFF — free access, no per-byte timing worries. The rule:
|
|
496
|
+
* only flip LCDC bit 7 to 0 DURING VBLANK. Killing the LCD mid-scanline is
|
|
497
|
+
* the classic "damages real DMG hardware" move; emulators shrug, real units
|
|
498
|
+
* can be permanently marked. wait_vblank() first, always.
|
|
499
|
+
* Requires: enable_vblank_irq() already called (wait_vblank HALT path);
|
|
500
|
+
* lcd_off-safe runtime (gb_runtime's wait_vblank bails if LCD is off). */
|
|
501
|
+
static void repaint_with_lcd_off(uint8_t to_title) {
|
|
502
|
+
msg_stage = 0; /* a queued game-over line must not land on
|
|
503
|
+
* the freshly painted screen a frame later */
|
|
504
|
+
wait_vblank(); /* never cut the LCD outside vblank */
|
|
505
|
+
LCDC = 0;
|
|
506
|
+
if (to_title) {
|
|
507
|
+
paint_title();
|
|
508
|
+
oam_clear(); /* hide every sprite slot before re-enable */
|
|
509
|
+
LCDC = LCDC_TITLE; /* window OFF on the title */
|
|
510
|
+
} else {
|
|
511
|
+
paint_starfield();
|
|
512
|
+
paint_hud();
|
|
513
|
+
LCDC = LCDC_PLAY; /* window ON below WY — the HUD appears */
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/* ── GAME LOGIC (clay) — sound: frame-ticked tune + fire/boom SFX ─────────────
|
|
518
|
+
* Channel plan keeps SFX from cutting the music: ch2 = music (one
|
|
519
|
+
* sound_play_tone trigger per note, the APU sustains it), ch1 = fire blips,
|
|
520
|
+
* ch4 = noise for explosions. music_tick() runs once per frame from the main
|
|
521
|
+
* loop; the APU needs no other upkeep. Periods are the 11-bit GB frequency
|
|
522
|
+
* code: 2048 - (131072 / Hz). 0 = rest. SELECT toggles it. */
|
|
523
|
+
static const uint16_t tune[16] = {
|
|
524
|
+
1547, 0, 1650, 0, 1714, 0, 1798, 0, /* C4 E4 G4 C5 */
|
|
525
|
+
1714, 0, 1650, 0, 1602, 0, 1650, 0, /* G4 E4 D4 E4 */
|
|
526
|
+
};
|
|
527
|
+
static uint8_t music_on = 1, music_pos, music_timer;
|
|
528
|
+
static void music_tick(void) {
|
|
529
|
+
uint16_t n;
|
|
530
|
+
if (!music_on) return;
|
|
531
|
+
if (++music_timer < 14) return;
|
|
532
|
+
music_timer = 0;
|
|
533
|
+
n = tune[music_pos];
|
|
534
|
+
music_pos = (uint8_t)((music_pos + 1) & 15);
|
|
535
|
+
if (n) sound_play_tone(2, n, 12);
|
|
536
|
+
}
|
|
537
|
+
static void music_toggle(void) {
|
|
538
|
+
music_on = (uint8_t)(!music_on);
|
|
539
|
+
if (!music_on) { NR21 = 0x00; NR22 = 0x00; NR24 = 0x80; } /* silence ch2 */
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/* ── GAME LOGIC (clay) — spawning, firing, collision ── */
|
|
543
|
+
static void fire_bullet(void) {
|
|
544
|
+
uint8_t i;
|
|
545
|
+
for (i = 0; i < MAX_BULLETS; i++) {
|
|
546
|
+
if (!bullets[i].alive) {
|
|
547
|
+
bullets[i].x = ship.x;
|
|
548
|
+
bullets[i].y = (uint8_t)(ship.y - 8);
|
|
549
|
+
bullets[i].alive = 1;
|
|
550
|
+
sound_play_tone(1, 1900, 4); /* ch1 blip — music keeps ch2 */
|
|
551
|
+
return;
|
|
165
552
|
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
166
555
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
while (1) {
|
|
180
|
-
wait_vblank();
|
|
181
|
-
|
|
182
|
-
/* Stage OAM for this frame BEFORE we update game state — the
|
|
183
|
-
* shadow OAM gets DMA'd next vblank. */
|
|
184
|
-
for (i = 0; i < 40; i++) oam_set(i, 0, 0, 0, 0);
|
|
185
|
-
oam_set(0, player.y + 16, player.x + 8, 1, 0);
|
|
186
|
-
for (i = 0; i < MAX_BULLETS; i++) {
|
|
187
|
-
if (bullets[i].alive)
|
|
188
|
-
oam_set(1 + i, bullets[i].y + 16, bullets[i].x + 8, 2, 0);
|
|
189
|
-
}
|
|
190
|
-
for (i = 0; i < MAX_ENEMIES; i++) {
|
|
191
|
-
if (enemies[i].alive)
|
|
192
|
-
oam_set(5 + i, enemies[i].y + 16, enemies[i].x + 8, 3, 0);
|
|
193
|
-
}
|
|
194
|
-
oam_dma_flush();
|
|
195
|
-
|
|
196
|
-
pad = joypad_read();
|
|
197
|
-
|
|
198
|
-
if (pad & PAD_LEFT && player.x > 0) player.x -= 2;
|
|
199
|
-
if (pad & PAD_RIGHT && player.x < 160 - 8) player.x += 2;
|
|
200
|
-
if (pad & PAD_UP && player.y > 0) player.y -= 2;
|
|
201
|
-
if (pad & PAD_DOWN && player.y < 144 - 8) player.y += 2;
|
|
202
|
-
if ((pad & PAD_A) && !(prev & PAD_A)) {
|
|
203
|
-
fire();
|
|
204
|
-
sound_play_tone(2, 1900, 4);
|
|
205
|
-
}
|
|
206
|
-
prev = pad;
|
|
207
|
-
|
|
208
|
-
for (i = 0; i < MAX_BULLETS; i++) {
|
|
209
|
-
if (!bullets[i].alive) continue;
|
|
210
|
-
if (bullets[i].y < 4) { bullets[i].alive = 0; continue; }
|
|
211
|
-
bullets[i].y -= 4;
|
|
212
|
-
}
|
|
213
|
-
for (i = 0; i < MAX_ENEMIES; i++) {
|
|
214
|
-
if (!enemies[i].alive) continue;
|
|
215
|
-
enemies[i].y += 1;
|
|
216
|
-
if (enemies[i].y >= 144) enemies[i].alive = 0;
|
|
217
|
-
}
|
|
218
|
-
if (++spawn_timer >= 28) { spawn_timer = 0; spawn(); }
|
|
219
|
-
|
|
220
|
-
for (i = 0; i < MAX_BULLETS; i++) {
|
|
221
|
-
if (!bullets[i].alive) continue;
|
|
222
|
-
for (j = 0; j < MAX_ENEMIES; j++) {
|
|
223
|
-
if (!enemies[j].alive) continue;
|
|
224
|
-
if (aabb(&bullets[i], &enemies[j])) {
|
|
225
|
-
bullets[i].alive = 0;
|
|
226
|
-
enemies[j].alive = 0;
|
|
227
|
-
if (score < 65500u) score += 10;
|
|
228
|
-
sound_play_noise(6);
|
|
229
|
-
break;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
556
|
+
static void spawn_enemy(void) {
|
|
557
|
+
uint8_t i;
|
|
558
|
+
for (i = 0; i < MAX_ENEMIES; i++) {
|
|
559
|
+
if (!enemies[i].alive) {
|
|
560
|
+
/* One software-% per spawn (every ~32 frames) is fine — the
|
|
561
|
+
* divide-free rule (see paint_starfield) is about per-cell/per-frame
|
|
562
|
+
* loops, not superstition. */
|
|
563
|
+
enemies[i].x = (uint8_t)(rand8() % 145 + 4);
|
|
564
|
+
enemies[i].y = 0;
|
|
565
|
+
enemies[i].alive = 1;
|
|
566
|
+
return;
|
|
233
567
|
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
static uint8_t hits(Obj *a, Obj *b) { /* AABB, both 8×8 */
|
|
572
|
+
uint8_t dx = (uint8_t)((a->x > b->x) ? (a->x - b->x) : (b->x - a->x));
|
|
573
|
+
uint8_t dy = (uint8_t)((a->y > b->y) ? (a->y - b->y) : (b->y - a->y));
|
|
574
|
+
return (uint8_t)((dx < 8) && (dy < 8));
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/* ── GAME LOGIC (clay) — state transitions ── */
|
|
578
|
+
static void start_game(void) {
|
|
579
|
+
uint8_t i;
|
|
580
|
+
ship.x = 76; ship.y = 104; ship.alive = 1;
|
|
581
|
+
for (i = 0; i < MAX_BULLETS; i++) bullets[i].alive = 0;
|
|
582
|
+
for (i = 0; i < MAX_ENEMIES; i++) enemies[i].alive = 0;
|
|
583
|
+
lives = START_LIVES;
|
|
584
|
+
score = 0;
|
|
585
|
+
fire_cd = 0;
|
|
586
|
+
spawn_timer = 0;
|
|
587
|
+
hud_dirty = 1; /* restage hud_q — a stale game-over stage queued
|
|
588
|
+
* before the repaint would overwrite the fresh
|
|
589
|
+
* zeros next vblank otherwise */
|
|
590
|
+
state = ST_PLAY;
|
|
591
|
+
repaint_with_lcd_off(0);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
static void game_over(void) {
|
|
595
|
+
/* Compare against the SAVED record, not the live `hiscore` readout — the
|
|
596
|
+
* kill handler already raised `hiscore` to track the run, so testing
|
|
597
|
+
* `score > hiscore` here would never fire. */
|
|
598
|
+
if (score > record) {
|
|
599
|
+
record = score;
|
|
600
|
+
hiscore_save(record); /* battery write — survives power-off */
|
|
601
|
+
}
|
|
602
|
+
state = ST_OVER;
|
|
603
|
+
/* The BG has scrolled: map row 0 is no longer screen row 0. Anchor the
|
|
604
|
+
* text relative to the CURRENT scroll so it lands mid-playfield on-screen
|
|
605
|
+
* ((SCY/8 + screen_row) & 31 = the map row under that screen row). Convert
|
|
606
|
+
* the strings to tile indices HERE (full-frame time) into msg_q — the
|
|
607
|
+
* vblank commit is then a DUMB byte copy. char_tile's per-char compare
|
|
608
|
+
* chain is exactly the work that blows the ~1140-cycle vblank budget; doing
|
|
609
|
+
* it inside the commit dropped the middle of the 11-char PRESS START line
|
|
610
|
+
* (verified on the GB original). Stage out here, copy in there. */
|
|
611
|
+
msg_row = (uint8_t)(((scroll_y >> 3) + 6) & 31);
|
|
612
|
+
stage_text("GAME OVER", msg_q);
|
|
613
|
+
stage_text("PRESS START", msg_q + 9);
|
|
614
|
+
msg_stage = 2;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/* ── GAME LOGIC (clay) — per-state update (runs OUTSIDE vblank) ── */
|
|
618
|
+
static void update_play(uint8_t pad) {
|
|
619
|
+
uint8_t i, j;
|
|
620
|
+
|
|
621
|
+
if ((pad & PAD_LEFT) && ship.x > 0) ship.x -= 2;
|
|
622
|
+
if ((pad & PAD_RIGHT) && ship.x < 160 - 8) ship.x += 2;
|
|
623
|
+
if ((pad & PAD_UP) && ship.y > 8) ship.y -= 2;
|
|
624
|
+
if ((pad & PAD_DOWN) && ship.y < PLAY_H - 24) ship.y += 2;
|
|
625
|
+
if ((pad & PAD_A) && fire_cd == 0) { fire_bullet(); fire_cd = 8; }
|
|
626
|
+
if (fire_cd) --fire_cd;
|
|
627
|
+
|
|
628
|
+
/* Starfield drift — the window HUD makes this free (no split timing). */
|
|
629
|
+
if ((spawn_timer & 1) == 0) --scroll_y;
|
|
630
|
+
|
|
631
|
+
for (i = 0; i < MAX_BULLETS; i++) {
|
|
632
|
+
if (!bullets[i].alive) continue;
|
|
633
|
+
if (bullets[i].y < 4) { bullets[i].alive = 0; continue; }
|
|
634
|
+
bullets[i].y -= 4;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/* Enemies despawn BEFORE the HUD line — sprites draw OVER the window
|
|
638
|
+
* (footgun 2 above), so nothing may drift past PLAY_H. */
|
|
639
|
+
for (i = 0; i < MAX_ENEMIES; i++) {
|
|
640
|
+
if (!enemies[i].alive) continue;
|
|
641
|
+
enemies[i].y += 1;
|
|
642
|
+
if (enemies[i].y >= PLAY_H - 12) enemies[i].alive = 0;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (++spawn_timer >= 32) { spawn_timer = 0; spawn_enemy(); }
|
|
646
|
+
|
|
647
|
+
/* Bullets ↔ enemies. */
|
|
648
|
+
for (i = 0; i < MAX_BULLETS; i++) {
|
|
649
|
+
if (!bullets[i].alive) continue;
|
|
650
|
+
for (j = 0; j < MAX_ENEMIES; j++) {
|
|
651
|
+
if (!enemies[j].alive) continue;
|
|
652
|
+
if (hits(&bullets[i], &enemies[j])) {
|
|
653
|
+
bullets[i].alive = 0;
|
|
654
|
+
enemies[j].alive = 0;
|
|
655
|
+
if (score <= 65525u) score += 10;
|
|
656
|
+
if (score > hiscore) hiscore = score; /* live HI readout; SRAM
|
|
657
|
+
* write waits for game over */
|
|
658
|
+
sound_play_noise(8);
|
|
659
|
+
hud_dirty = 1;
|
|
660
|
+
break;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/* Enemies ↔ ship. */
|
|
666
|
+
for (j = 0; j < MAX_ENEMIES; j++) {
|
|
667
|
+
if (!enemies[j].alive) continue;
|
|
668
|
+
if (hits(&enemies[j], &ship)) {
|
|
669
|
+
enemies[j].alive = 0;
|
|
670
|
+
sound_play_noise(24);
|
|
671
|
+
if (lives) --lives;
|
|
672
|
+
hud_dirty = 1;
|
|
673
|
+
if (lives == 0) { game_over(); return; }
|
|
674
|
+
ship.x = 76; ship.y = 104; /* respawn knockback */
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/* ── GAME LOGIC (clay) — stage the shadow OAM for THIS frame ──────────────────
|
|
680
|
+
* Pure WRAM writes (shadow_oam at $C100) — safe any time; only the DMA flush
|
|
681
|
+
* is vblank-sensitive. OAM coords are hardware coords: +16 on Y, +8 on X.
|
|
682
|
+
* A sprite's CGB palette = OAM attr bits 0-2 — that's the whole "color this
|
|
683
|
+
* sprite" story. Slot plan (40 hardware slots, we use 13): 0 = ship,
|
|
684
|
+
* 1-6 bullets, 7-12 enemies — well under the 10-OBJ/line hardware drop. */
|
|
685
|
+
static void stage_sprites(void) {
|
|
686
|
+
uint8_t i;
|
|
687
|
+
oam_clear();
|
|
688
|
+
if (state == ST_TITLE) {
|
|
689
|
+
/* Guaranteed-visible sprite from the first title frame — proof the OAM
|
|
690
|
+
* pipeline (shadow → HRAM DMA stub → OAM) is alive before any gameplay
|
|
691
|
+
* complicates the picture. */
|
|
692
|
+
oam_set(0, 96 + 16, 76 + 8, T_SHIP, OPAL_SHIP);
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
if (ship.alive)
|
|
696
|
+
oam_set(0, (uint8_t)(ship.y + 16), (uint8_t)(ship.x + 8), T_SHIP, OPAL_SHIP);
|
|
697
|
+
for (i = 0; i < MAX_BULLETS; i++)
|
|
698
|
+
if (bullets[i].alive)
|
|
699
|
+
oam_set((uint8_t)(1 + i), (uint8_t)(bullets[i].y + 16),
|
|
700
|
+
(uint8_t)(bullets[i].x + 8), T_BULLET, OPAL_BULLET);
|
|
701
|
+
for (i = 0; i < MAX_ENEMIES; i++)
|
|
702
|
+
if (enemies[i].alive)
|
|
703
|
+
oam_set((uint8_t)(7 + i), (uint8_t)(enemies[i].y + 16),
|
|
704
|
+
(uint8_t)(enemies[i].x + 8), T_ENEMY, OPAL_ENEMY);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
708
|
+
* Queued VRAM commits — and the bank-0-only HUD write. Two-phase update,
|
|
709
|
+
* mirroring the shadow-OAM discipline: game logic only sets hud_dirty /
|
|
710
|
+
* msg_stage. stage_hud() (full-frame time) does the digit math into hud_q;
|
|
711
|
+
* commit_vram() (vblank time) writes bytes — AT MOST ONE queued item/vblank.
|
|
712
|
+
*
|
|
713
|
+
* THE CGB TWIST (load-bearing): a naive set_wcell() per HUD cell toggles VBK
|
|
714
|
+
* twice + writes two banks PER cell — for 11 HUD cells that's ~33 VBK writes
|
|
715
|
+
* in one vblank, which OVERRUNS the ~1140-cycle window and silently drops the
|
|
716
|
+
* tail writes (the lives digit at col 19 vanished — verified on the GBC
|
|
717
|
+
* platformer). The fix: the window HUD cells' bank-1 ATTRIBUTE bytes are
|
|
718
|
+
* constant PAL_HUD (painted once by paint_hud at LCD-off and never changed),
|
|
719
|
+
* so the per-frame commit only needs to rewrite bank-0 TILE bytes. We set
|
|
720
|
+
* VBK=0 ONCE and pointer-walk the digit cells — a tight write that fits
|
|
721
|
+
* vblank with room to spare. (Pointer walk, not map[i] indexing — the SDCC
|
|
722
|
+
* VRAM footgun.)
|
|
723
|
+
*
|
|
724
|
+
* The game-over text on the BG goes the same way: pre-staged tiles, written
|
|
725
|
+
* one line per vblank, and we DELIBERATELY leave the cells' bank-1 attribute
|
|
726
|
+
* alone — the field painted them a depth-band palette whose colour-3 (the
|
|
727
|
+
* font ink value) is bright, so the text reads on top with ZERO attribute
|
|
728
|
+
* writes. That halves the vblank cost. */
|
|
729
|
+
static uint8_t hud_q[11]; /* 5 score digits, 5 hi digits, lives tile */
|
|
730
|
+
static uint8_t hud_ready;
|
|
731
|
+
#define WIN_TILE ((volatile uint8_t *)0x9C00) /* window map, bank 0 */
|
|
732
|
+
|
|
733
|
+
static void stage_hud(void) {
|
|
734
|
+
if (!hud_dirty) return;
|
|
735
|
+
hud_dirty = 0;
|
|
736
|
+
u16_to_tiles(score, hud_q);
|
|
737
|
+
u16_to_tiles(hiscore, hud_q + 5);
|
|
738
|
+
hud_q[10] = (uint8_t)(FONT_BASE + lives);
|
|
739
|
+
hud_ready = 1;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/* Write a scroll-anchored, pre-staged BG-map line (msg_q tiles) as a single
|
|
743
|
+
* BANK-0 tile copy — a dumb byte walk, no char_tile work and no per-cell VBK
|
|
744
|
+
* toggling. col wraps at the 32-col map seam (the text is scroll-anchored, so
|
|
745
|
+
* it can straddle the wrap). */
|
|
746
|
+
static void commit_bg_text(uint8_t row, uint8_t col, const uint8_t *q, uint8_t len) {
|
|
747
|
+
volatile uint8_t *base = VRAM + (uint16_t)row * 32;
|
|
748
|
+
volatile uint8_t *p = base + col;
|
|
749
|
+
uint8_t n = (uint8_t)(32 - col);
|
|
750
|
+
VBK = 0;
|
|
751
|
+
if (n > len) n = len; /* run 1: up to the map seam */
|
|
752
|
+
len -= n;
|
|
753
|
+
while (n--) *p++ = *q++;
|
|
754
|
+
p = base; /* run 2: wrapped remainder */
|
|
755
|
+
while (len--) *p++ = *q++;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
static void commit_vram(void) {
|
|
759
|
+
uint8_t i;
|
|
760
|
+
if (hud_ready) { /* item 1: HUD digits (bank 0) */
|
|
761
|
+
hud_ready = 0;
|
|
762
|
+
VBK = 0; /* attributes already PAL_HUD */
|
|
763
|
+
for (i = 0; i < 5; i++) WIN_TILE[32 + 3 + i] = hud_q[i]; /* score */
|
|
764
|
+
for (i = 0; i < 5; i++) WIN_TILE[32 + 12 + i] = hud_q[5 + i]; /* hi */
|
|
765
|
+
WIN_TILE[32 + 19] = hud_q[10]; /* lives */
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
if (msg_stage == 2) { /* item 2: GAME OVER line */
|
|
769
|
+
msg_stage = 1;
|
|
770
|
+
commit_bg_text(msg_row, 5, msg_q, 9);
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
if (msg_stage == 1) { /* item 3: PRESS START line */
|
|
774
|
+
msg_stage = 0;
|
|
775
|
+
commit_bg_text((uint8_t)((msg_row + 2) & 31), 4, msg_q + 9, 11);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
void main(void) {
|
|
780
|
+
uint8_t pad;
|
|
781
|
+
|
|
782
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
783
|
+
* Boot order. Three load-bearing calls, in this order:
|
|
784
|
+
* 1. lcd_init_default() — sane LCD state AND it installs the OAM-DMA stub
|
|
785
|
+
* into HRAM ($FF80). During OAM DMA the CPU can only fetch from HRAM;
|
|
786
|
+
* the broken alternative (spinning in ROM) fetches $FF = rst $38 and
|
|
787
|
+
* corrupts the stack — the classic "sprites never show / game dies
|
|
788
|
+
* after a while" GB death. Every oam_dma_flush() depends on this stub.
|
|
789
|
+
* 2. enable_vblank_irq() — flips wait_vblank() from LY-polling to
|
|
790
|
+
* HALT-until-vblank-IRQ. The polling fallback runs at ~1/30 speed on
|
|
791
|
+
* the WASM emulator; the HALT path is full speed everywhere.
|
|
792
|
+
* 3. LCD off (inside vblank) for the bulk VRAM uploads — tiles, font,
|
|
793
|
+
* palettes, first screen — then back on. Tile/palette/map uploads
|
|
794
|
+
* REQUIRE a VRAM-safe window; boot does them all at once, so LCD-off
|
|
795
|
+
* is the only sane choice here. */
|
|
796
|
+
lcd_init_default();
|
|
797
|
+
enable_vblank_irq();
|
|
798
|
+
sound_init();
|
|
799
|
+
|
|
800
|
+
wait_vblank();
|
|
801
|
+
LCDC = 0; /* LCD off — free VRAM access from here */
|
|
802
|
+
|
|
803
|
+
upload_tile(T_BLANK, tile_blank);
|
|
804
|
+
upload_tile(T_SHIP, tile_ship);
|
|
805
|
+
upload_tile(T_BULLET, tile_bullet);
|
|
806
|
+
upload_tile(T_ENEMY, tile_enemy);
|
|
807
|
+
upload_tile(T_SPACE, tile_space);
|
|
808
|
+
upload_tile(T_STAR, tile_star);
|
|
809
|
+
upload_tile(T_BRITE, tile_brite);
|
|
810
|
+
upload_tile(T_HUDBAR, tile_hudbar);
|
|
811
|
+
upload_font();
|
|
812
|
+
|
|
813
|
+
load_bg_palettes(); /* the CGB BG palettes — depth bands + HUD */
|
|
814
|
+
load_obj_palettes(); /* ship / bullet / enemy OBJ palettes */
|
|
815
|
+
|
|
816
|
+
/* Window position — set once; LCDC bit 5 decides if it shows. */
|
|
817
|
+
WX = 7; /* the +7 quirk: 7 = screen left edge */
|
|
818
|
+
WY = PLAY_H; /* HUD owns lines 128-143 */
|
|
819
|
+
|
|
820
|
+
record = hiscore_load(); /* battery SRAM — 0 on first boot */
|
|
821
|
+
hiscore = record;
|
|
822
|
+
state = ST_TITLE;
|
|
823
|
+
paint_title();
|
|
824
|
+
oam_clear();
|
|
825
|
+
LCDC = LCDC_TITLE;
|
|
826
|
+
|
|
827
|
+
for (;;) {
|
|
828
|
+
/* ── full-frame work: input, game state, shadow-OAM staging ── */
|
|
829
|
+
pad = joypad_read();
|
|
830
|
+
|
|
831
|
+
/* SELECT toggles the background music, in any state. */
|
|
832
|
+
if ((pad & PAD_SELECT) && !(prev_pad & PAD_SELECT)) music_toggle();
|
|
833
|
+
|
|
834
|
+
if (state == ST_TITLE) {
|
|
835
|
+
if ((pad & (PAD_START | PAD_A)) && !(prev_pad & (PAD_START | PAD_A))) start_game();
|
|
836
|
+
prev_pad = pad;
|
|
837
|
+
} else if (state == ST_PLAY) {
|
|
838
|
+
update_play(pad);
|
|
839
|
+
prev_pad = pad;
|
|
840
|
+
} else { /* ST_OVER — freeze the field; START/A returns to title */
|
|
841
|
+
if ((pad & (PAD_START | PAD_A)) && !(prev_pad & (PAD_START | PAD_A))) {
|
|
842
|
+
state = ST_TITLE;
|
|
843
|
+
repaint_with_lcd_off(1);
|
|
844
|
+
}
|
|
845
|
+
prev_pad = pad;
|
|
846
|
+
}
|
|
847
|
+
stage_sprites();
|
|
848
|
+
stage_hud(); /* digit math out here, not in vblank */
|
|
849
|
+
|
|
850
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
851
|
+
* The vblank slice. wait_vblank() wakes at the START of vblank
|
|
852
|
+
* (~1140 cycles of safe OAM/VRAM access). Order is everything:
|
|
853
|
+
* oam_dma_flush() FIRST — the DMA takes ~165 cycles and MUST finish
|
|
854
|
+
* inside vblank; pushing it later (after VRAM writes that grow over
|
|
855
|
+
* time) slides it into active display, where the PPU is reading OAM
|
|
856
|
+
* = one frame of torn/invisible sprites, intermittent and miserable
|
|
857
|
+
* to debug.
|
|
858
|
+
* commit_vram() second — the few queued HUD/map bytes (one item/frame).
|
|
859
|
+
* SCY last — scroll latches per-scanline, so writing it during vblank
|
|
860
|
+
* (before line 0 renders) moves the WHOLE next frame consistently;
|
|
861
|
+
* the window ignores it by design (the HUD idiom).
|
|
862
|
+
* Game logic above NEVER touches VRAM directly — it sets the dirty flags
|
|
863
|
+
* and shadow OAM, and this slice commits them. Keep that split. */
|
|
864
|
+
wait_vblank();
|
|
865
|
+
oam_dma_flush();
|
|
866
|
+
commit_vram();
|
|
867
|
+
SCY = scroll_y; /* title resets scroll_y to 0; over freezes it */
|
|
868
|
+
music_tick();
|
|
869
|
+
}
|
|
234
870
|
}
|