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,45 +1,76 @@
|
|
|
1
|
-
/*
|
|
1
|
+
/* ── puzzle.c — CHROMA WELL: Game Boy Color falling-jewel matcher (complete example game) ──
|
|
2
2
|
*
|
|
3
|
-
* A
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
3
|
+
* A COMPLETE, working game — title screen, persistent battery hi-score
|
|
4
|
+
* (MBC1+RAM+BATTERY SRAM), music + SFX, and the GBC's signature feature:
|
|
5
|
+
* TRUE per-tile color. Six jewel types are six REAL CGB palettes (15-bit
|
|
6
|
+
* BGR, loaded through BCPS/BCPD + OCPS/OCPD), selected per BG cell through
|
|
7
|
+
* the VRAM bank-1 attribute map and per sprite through OAM attribute bits —
|
|
8
|
+
* not a colorized monochrome game.
|
|
9
|
+
*
|
|
10
|
+
* THE GAME: a vertical column of 3 jewels falls into an 8-wide x 15-tall
|
|
11
|
+
* well. Move it left/right, soft-drop (Down), hard-drop (Start), and CYCLE
|
|
12
|
+
* the three colors (A/B). Line up 3+ of one color horizontally, vertically,
|
|
13
|
+
* or diagonally to clear; gravity pulls survivors down, which can chain.
|
|
14
|
+
* Every 18th piece is a MAGIC jewel that clears every gem of the color it
|
|
15
|
+
* lands on. SELECT toggles the music.
|
|
16
|
+
*
|
|
17
|
+
* THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
|
|
18
|
+
* very different one. The markers tell you what's what:
|
|
19
|
+
* HARDWARE IDIOM (load-bearing) — dodges a documented GB/GBC footgun;
|
|
20
|
+
* reshape your gameplay around it (see TROUBLESHOOTING before changing).
|
|
21
|
+
* GAME LOGIC (clay) — board rules, scoring, tuning, art: reshape freely.
|
|
22
|
+
*
|
|
23
|
+
* SINGLE-PLAYER, honestly: the Game Boy's "player 2" is a LINK CABLE, which
|
|
24
|
+
* one emulator instance cannot provide — so handheld examples ship a
|
|
25
|
+
* press-start title and no 2P mode instead of faking one.
|
|
26
|
+
*
|
|
27
|
+
* What depends on what:
|
|
28
|
+
* gb_runtime.{h,c} — vblank/joypad/OAM-DMA/sound library (shared with GB).
|
|
29
|
+
* gb_crt0.s — boot + header window. It DECLARES the cart as
|
|
30
|
+
* MBC1+RAM+BATTERY ($0147=$03, $0149=$02): that header is what makes
|
|
31
|
+
* the SRAM hi-score persist (the GB equivalent of the NES BATTERY bit).
|
|
32
|
+
* font.h — 0-9 A-Z glyphs for all text.
|
|
9
33
|
*
|
|
10
34
|
* RENDERING — the hard-won architecture (details at each routine below):
|
|
11
35
|
* - The FALLING column and the NEXT preview are OBJ sprites (OAM), not BG
|
|
12
36
|
* tiles, so moving them is just an OAM rewrite — no per-frame BG writes.
|
|
13
|
-
* - The LOCKED well
|
|
14
|
-
* redraw_collect() decides what to write (RAM only); redraw_flush()
|
|
15
|
-
* few cells to VRAM as the very first thing in vblank.
|
|
16
|
-
* job (OAM DMA + flush) MUST finish inside the ~10-line vblank
|
|
17
|
-
* overrunning into active display silently DROPS writes on this
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* -
|
|
21
|
-
*
|
|
22
|
-
*
|
|
37
|
+
* - The LOCKED well is BG tiles, updated through a COLLECT/FLUSH queue:
|
|
38
|
+
* redraw_collect() decides what to write (RAM only); redraw_flush()
|
|
39
|
+
* writes a few cells to VRAM as the very first thing in vblank. The whole
|
|
40
|
+
* per-frame job (OAM DMA + flush) MUST finish inside the ~10-line vblank
|
|
41
|
+
* window — overrunning into active display silently DROPS writes on this
|
|
42
|
+
* core. An idle "scrub" continuously repaints the well from the grid so
|
|
43
|
+
* nothing can drift.
|
|
44
|
+
* - The HUD (score / hi-score / level) lives on the WINDOW layer — a fixed
|
|
45
|
+
* strip at the bottom of the screen, immune to BG scrolling.
|
|
46
|
+
* - We NEVER toggle the LCD in-game (this core blanks the whole frame on
|
|
47
|
+
* any LCDC bit-7 toggle — a strobe). LCD-off is used only for the
|
|
48
|
+
* full-screen title <-> game transitions.
|
|
23
49
|
*
|
|
24
50
|
* WRAM NOTE: build with dataLoc:0xC200 so our statics sit ABOVE shadow_oam
|
|
25
|
-
* ($C100) — else oam_clear() would zero our state (RNG seed / grid).
|
|
51
|
+
* ($C100) — else oam_clear() would zero our state (RNG seed / grid). The
|
|
52
|
+
* project build recipe sets that automatically.
|
|
26
53
|
*/
|
|
27
54
|
#include "gb_hardware.h"
|
|
28
55
|
#include "gb_runtime.h"
|
|
29
56
|
#include "font.h"
|
|
30
57
|
|
|
58
|
+
/* The title screen renders this — examples({op:'fork'}) stamps your game's
|
|
59
|
+
* name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
|
|
60
|
+
#define GAME_TITLE "CHROMA WELL"
|
|
61
|
+
|
|
62
|
+
/* ── GAME LOGIC (clay — reshape freely) ── board geometry */
|
|
31
63
|
#define COLS 8
|
|
32
|
-
#define ROWS 17
|
|
64
|
+
#define ROWS 15 /* rows 0-14; floor at map row 15; window HUD rows 16-17 */
|
|
33
65
|
#define NCELL (ROWS * COLS)
|
|
34
|
-
#define NCOLORS 6 /* jewel colors 1..6 */
|
|
66
|
+
#define NCOLORS 6 /* jewel colors 1..6 — one CGB palette each */
|
|
35
67
|
|
|
36
68
|
/* BG map cell of interior grid cell (0,0) — the well's top-left corner.
|
|
37
|
-
* Open at the top (row 0); walls
|
|
38
|
-
* below the bottom (floor at WELL_MY+ROWS = row 17, the last screen row). */
|
|
69
|
+
* Open at the top (row 0); walls one cell outside left/right, floor below. */
|
|
39
70
|
#define WELL_MX 1
|
|
40
71
|
#define WELL_MY 0
|
|
41
72
|
|
|
42
|
-
/* BG map column where the
|
|
73
|
+
/* BG map column where the right-hand panel (NEXT preview) starts. */
|
|
43
74
|
#define HUD_X 12
|
|
44
75
|
|
|
45
76
|
#define G(r,c) grid[((r) * COLS) + (c)]
|
|
@@ -60,13 +91,20 @@
|
|
|
60
91
|
#define PAL_WELL 6
|
|
61
92
|
#define PAL_OUT 7
|
|
62
93
|
|
|
63
|
-
#define
|
|
64
|
-
#define
|
|
65
|
-
#define
|
|
94
|
+
#define ST_TITLE 0
|
|
95
|
+
#define ST_PLAY 1
|
|
96
|
+
#define ST_OVER 2
|
|
66
97
|
|
|
67
98
|
#define VRAM ((volatile uint8_t *)0x9800)
|
|
68
|
-
|
|
69
|
-
|
|
99
|
+
/* The window layer fetches from the $9C00 map — offset $400 past $9800 in
|
|
100
|
+
* the same VRAM pointer (see the WINDOW HUD idiom below). */
|
|
101
|
+
#define WIN_OFF 0x400
|
|
102
|
+
|
|
103
|
+
/* ── GAME LOGIC (clay — reshape freely) ── tile pixel data (2bpp).
|
|
104
|
+
* Each 8x8 tile = 16 bytes, 2 bytes per row (plane 0 then plane 1); a pixel's
|
|
105
|
+
* 2-bit color value indexes into whichever CGB palette the cell's attribute
|
|
106
|
+
* byte (BG) or the sprite's OAM attr (OBJ) selects. ONE gem tile becomes six
|
|
107
|
+
* different-colored gems purely through palette selection. */
|
|
70
108
|
static const uint8_t tile_empty[16] = {
|
|
71
109
|
0x00,0x00, 0x22,0x00, 0x00,0x00, 0x88,0x00,
|
|
72
110
|
0x00,0x00, 0x22,0x00, 0x00,0x00, 0x88,0x00,
|
|
@@ -100,7 +138,9 @@ static const uint8_t tile_exp2[16] = {
|
|
|
100
138
|
0x00,0x00, 0x00,0x00, 0x00,0x00, 0x00,0x81,
|
|
101
139
|
};
|
|
102
140
|
|
|
103
|
-
/* ──
|
|
141
|
+
/* ── GAME LOGIC (clay — reshape freely) ── the palette TABLE (the colors
|
|
142
|
+
* themselves are art; the LOADER below is the hardware idiom).
|
|
143
|
+
* 15-bit BGR: 5 bits each, blue in the high bits — RGB() packs it. */
|
|
104
144
|
#define RGB(r,g,b) ((uint16_t)(((uint16_t)(b)<<10)|((uint16_t)(g)<<5)|(r)))
|
|
105
145
|
#define C_WELL RGB(4,6,12)
|
|
106
146
|
#define C_OUT RGB(1,2,4)
|
|
@@ -118,7 +158,7 @@ static const uint16_t palettes[8][4] = {
|
|
|
118
158
|
/* 7 out/txt*/ { C_OUT, RGB(2,3,7), C_OUT, RGB(31,31,31) },
|
|
119
159
|
};
|
|
120
160
|
|
|
121
|
-
/* ── game state
|
|
161
|
+
/* ── GAME LOGIC (clay — reshape freely) ── game state */
|
|
122
162
|
static uint8_t grid[NCELL]; /* the well: 0 = empty, 1..NCOLORS = a gem */
|
|
123
163
|
static uint8_t matched[NCELL]; /* scratch: cells flagged for clearing */
|
|
124
164
|
static uint8_t shadow[NCELL]; /* color currently on the BG, for diff redraw */
|
|
@@ -134,7 +174,8 @@ static uint8_t cur_fall_rate; /* frames per downward step (lower = faster)
|
|
|
134
174
|
static uint16_t total_cleared; /* gems cleared this game (drives level) */
|
|
135
175
|
static uint8_t level;
|
|
136
176
|
static uint8_t score_d[6]; /* 6-digit BCD score, most significant first */
|
|
137
|
-
static uint8_t
|
|
177
|
+
static uint8_t hi_d[6]; /* 6-digit BCD hi-score (battery SRAM) */
|
|
178
|
+
static uint8_t state; /* ST_TITLE / ST_PLAY / ST_OVER */
|
|
138
179
|
static uint8_t chain; /* cascade depth of the current resolve */
|
|
139
180
|
static uint16_t rng = 0xACE1; /* xorshift PRNG state */
|
|
140
181
|
|
|
@@ -171,12 +212,79 @@ static void add_score(uint16_t amt) {
|
|
|
171
212
|
}
|
|
172
213
|
}
|
|
173
214
|
|
|
174
|
-
/*
|
|
215
|
+
/* most-significant-digit-first BCD compare: did this run beat the record? */
|
|
216
|
+
static uint8_t score_beats_hi(void) {
|
|
217
|
+
uint8_t i;
|
|
218
|
+
for (i = 0; i < 6; i++) {
|
|
219
|
+
if (score_d[i] > hi_d[i]) return 1;
|
|
220
|
+
if (score_d[i] < hi_d[i]) return 0;
|
|
221
|
+
}
|
|
222
|
+
return 0;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
226
|
+
* BATTERY SRAM hi-score — persistent saves on a Game Boy cart.
|
|
227
|
+
* requires: gb_crt0.s declaring MBC1+RAM+BATTERY in the cartridge header
|
|
228
|
+
* ($0147=$03, $0149=$02 → 8KB at $A000-$BFFF). With a ROM-only header the
|
|
229
|
+
* $A000 region is OPEN BUS: writes vanish, reads return garbage, and
|
|
230
|
+
* nothing tells you why. The header is the save system.
|
|
231
|
+
*
|
|
232
|
+
* The MBC powers up with cart RAM DISABLED (protection against corrupting
|
|
233
|
+
* the battery RAM with stray bus traffic while power rails settle). The
|
|
234
|
+
* $0A-enable dance:
|
|
235
|
+
* 1. write $0A to anywhere in $0000-$1FFF → RAM enabled
|
|
236
|
+
* 2. read/write $A000-$BFFF → real battery RAM
|
|
237
|
+
* 3. write $00 to $0000-$1FFF → RAM disabled again
|
|
238
|
+
* ALWAYS re-disable after access — that's what makes a yanked cartridge /
|
|
239
|
+
* dying battery corrupt at most the bytes mid-write, not the whole save.
|
|
240
|
+
*
|
|
241
|
+
* First boot is GARBAGE, not zeros: battery RAM holds whatever the silicon
|
|
242
|
+
* woke up with. The magic bytes + XOR checksum below are how the load path
|
|
243
|
+
* tells "my save" from "factory noise" — without them a fresh cart shows a
|
|
244
|
+
* junk hi-score like 974382.
|
|
245
|
+
*
|
|
246
|
+
* Save block at $A000: 'H' 'S' d0 d1 d2 d3 d4 d5 ck
|
|
247
|
+
* (6 BCD digits, most significant first; ck = d0^..^d5^$A5)
|
|
248
|
+
* No timing constraints — SRAM is not VRAM; access it any time. */
|
|
249
|
+
#define SRAM_BASE ((volatile uint8_t *)0xA000)
|
|
250
|
+
#define MBC_RAMG (*(volatile uint8_t *)0x0000) /* MBC1 RAM-gate register */
|
|
251
|
+
|
|
252
|
+
static void hiscore_load(void) {
|
|
253
|
+
uint8_t i, ck;
|
|
254
|
+
MBC_RAMG = 0x0A; /* enable cart RAM */
|
|
255
|
+
ck = 0xA5;
|
|
256
|
+
for (i = 0; i < 6; i++) ck ^= SRAM_BASE[2 + i];
|
|
257
|
+
if (SRAM_BASE[0] == 'H' && SRAM_BASE[1] == 'S' && SRAM_BASE[8] == ck) {
|
|
258
|
+
for (i = 0; i < 6; i++) {
|
|
259
|
+
hi_d[i] = SRAM_BASE[2 + i];
|
|
260
|
+
if (hi_d[i] > 9) hi_d[i] = 9; /* belt + braces on a bad digit */
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
for (i = 0; i < 6; i++) hi_d[i] = 0; /* first boot / corrupt → 0 */
|
|
264
|
+
}
|
|
265
|
+
MBC_RAMG = 0x00; /* ALWAYS re-disable */
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
static void hiscore_save(void) {
|
|
269
|
+
uint8_t i, ck;
|
|
270
|
+
MBC_RAMG = 0x0A;
|
|
271
|
+
SRAM_BASE[0] = 'H';
|
|
272
|
+
SRAM_BASE[1] = 'S';
|
|
273
|
+
ck = 0xA5;
|
|
274
|
+
for (i = 0; i < 6; i++) {
|
|
275
|
+
SRAM_BASE[2 + i] = hi_d[i];
|
|
276
|
+
ck ^= hi_d[i];
|
|
277
|
+
}
|
|
278
|
+
SRAM_BASE[8] = ck;
|
|
279
|
+
MBC_RAMG = 0x00;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/* ── GAME LOGIC (clay — reshape freely) ── sound effects.
|
|
175
283
|
* A tiny note sequencer driving square channel 2 directly. Each note has
|
|
176
284
|
* a real volume-decay envelope (NR22) so it fades instead of clicking off
|
|
177
|
-
* (
|
|
178
|
-
*
|
|
179
|
-
*
|
|
285
|
+
* (a hard NRx2=0 cut every note sounds like static). sfx_tick() advances
|
|
286
|
+
* one step per frame; multi-note effects become little arpeggios.
|
|
287
|
+
* GB period p ⇒ freq = 131072/(2048-p); higher p = higher note. */
|
|
180
288
|
#define P_C4 1548
|
|
181
289
|
#define P_G4 1714
|
|
182
290
|
#define P_A4 1750
|
|
@@ -242,7 +350,7 @@ static void sfx_over(void) { /* slow descending */
|
|
|
242
350
|
sfx_go(3);
|
|
243
351
|
}
|
|
244
352
|
|
|
245
|
-
/* ── background music
|
|
353
|
+
/* ── GAME LOGIC (clay — reshape freely) ── background music.
|
|
246
354
|
* A looping square-wave lead on channel 1 (SFX live on channel 2, so they
|
|
247
355
|
* mix and the effects cut through the music). music_tick() plays one melody
|
|
248
356
|
* step every 12 frames, re-triggering ch1 at a steady volume. Toggle on/off
|
|
@@ -294,6 +402,8 @@ static void music_toggle(void) {
|
|
|
294
402
|
if (!music_on) { NR12 = 0x00; NR14 = 0x80; } /* kill the lead immediately */
|
|
295
403
|
}
|
|
296
404
|
|
|
405
|
+
/* ── GAME LOGIC (clay — reshape freely) ── board mechanics */
|
|
406
|
+
|
|
297
407
|
/* is grid cell (r,col) off the bottom or already filled? */
|
|
298
408
|
static uint8_t cell_blocked(uint8_t r, uint8_t col) {
|
|
299
409
|
if (r >= ROWS) return 1;
|
|
@@ -311,6 +421,8 @@ static uint8_t collides(uint8_t col, uint8_t topy) {
|
|
|
311
421
|
return 0;
|
|
312
422
|
}
|
|
313
423
|
|
|
424
|
+
static void game_over(void);
|
|
425
|
+
|
|
314
426
|
/* start a new falling column at the top-center. Every 18th piece is a MAGIC
|
|
315
427
|
* column; otherwise take the previewed colors and roll the next preview. If
|
|
316
428
|
* it can't even appear, the well is full → game over. */
|
|
@@ -330,10 +442,7 @@ static void spawn(void) {
|
|
|
330
442
|
piece_active = 1;
|
|
331
443
|
fall_timer = 0;
|
|
332
444
|
next_dirty = 1;
|
|
333
|
-
if (collides(piece_x, piece_y))
|
|
334
|
-
piece_active = 0;
|
|
335
|
-
state = ST_OVER;
|
|
336
|
-
}
|
|
445
|
+
if (collides(piece_x, piece_y)) game_over();
|
|
337
446
|
}
|
|
338
447
|
|
|
339
448
|
/* Flag every gem that's part of a run of 3+ same-color cells in any of the 4
|
|
@@ -423,7 +532,7 @@ static void explode_matched(void) {
|
|
|
423
532
|
uint16_t offs[8];
|
|
424
533
|
uint8_t cols[8];
|
|
425
534
|
uint8_t *o = (uint8_t *)0xC100;
|
|
426
|
-
for (i = 0; i < 12; i++) *o++ = 0; /* hide the
|
|
535
|
+
for (i = 0; i < 12; i++) *o++ = 0; /* hide the falling-piece sprites */
|
|
427
536
|
((void (*)(uint8_t))0xFF80)(0xC1);
|
|
428
537
|
n = 0;
|
|
429
538
|
for (i = 0; i < NCELL && n < 8; i++) {
|
|
@@ -513,10 +622,25 @@ static void upload_tile(uint8_t slot, const uint8_t *src) {
|
|
|
513
622
|
memcpy_vram((uint8_t *)(0x8000 + slot * 16), src, 16);
|
|
514
623
|
}
|
|
515
624
|
|
|
516
|
-
/*
|
|
625
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
626
|
+
* CGB palette RAM — the BCPS/BCPD (BG) and OCPS/OCPD (OBJ) port pairs.
|
|
627
|
+
* requires: a .gbc build (CGB flag $0143 set — the build pipeline does it);
|
|
628
|
+
* on a DMG build these registers are dead and you get 4-shade green.
|
|
629
|
+
*
|
|
630
|
+
* Palette RAM is NOT memory-mapped: it's 64 bytes (8 palettes × 4 colors ×
|
|
631
|
+
* 2 bytes, little-endian 15-bit BGR) behind an index/data port pair.
|
|
632
|
+
* BCPS = 0x80 | index — set write index; bit 7 = AUTO-INCREMENT, so a
|
|
633
|
+
* burst of BCPD writes walks the whole 64 bytes.
|
|
634
|
+
* BCPD = low byte; BCPD = high byte; ... 32 times = all 8 palettes.
|
|
635
|
+
*
|
|
636
|
+
* TIMING FOOTGUN: palette RAM belongs to the PPU. Writes during active
|
|
637
|
+
* display (mode 3) are IGNORED on real hardware — same constraint as VRAM.
|
|
638
|
+
* Load palettes with the LCD OFF (boot / transitions, as here) or inside
|
|
639
|
+
* vblank. A palette "fade" effect = a few BCPD writes per vblank, never a
|
|
640
|
+
* mid-frame burst. */
|
|
517
641
|
static void load_palettes(void) {
|
|
518
642
|
uint8_t p, i;
|
|
519
|
-
BCPS = 0x80;
|
|
643
|
+
BCPS = 0x80; /* index 0, auto-increment on */
|
|
520
644
|
for (p = 0; p < 8; p++)
|
|
521
645
|
for (i = 0; i < 4; i++) {
|
|
522
646
|
BCPD = (uint8_t)(palettes[p][i] & 0xFF);
|
|
@@ -524,8 +648,9 @@ static void load_palettes(void) {
|
|
|
524
648
|
}
|
|
525
649
|
}
|
|
526
650
|
|
|
527
|
-
/* OBJ palettes
|
|
528
|
-
*
|
|
651
|
+
/* OBJ palettes 0-5 = the six jewel colors (same table as the BG, so a
|
|
652
|
+
* falling gem matches its locked twin exactly), 6 = magic white. Color 0
|
|
653
|
+
* of every OBJ palette is transparent (the well shows through). */
|
|
529
654
|
static void load_obj_palettes(void) {
|
|
530
655
|
uint8_t p, i;
|
|
531
656
|
uint16_t col;
|
|
@@ -541,9 +666,11 @@ static void load_obj_palettes(void) {
|
|
|
541
666
|
}
|
|
542
667
|
|
|
543
668
|
/* The falling column = sprites 0-2; the NEXT preview = sprites 3-5 (sprites
|
|
544
|
-
* so their transparent corners blend with the
|
|
669
|
+
* so their transparent corners blend with the panel). Then flush OAM.
|
|
545
670
|
* MUST be the first VRAM/OAM work after wait_vblank: the OAM DMA has to
|
|
546
|
-
* land in vblank, or sprites tear on a fixed scanline near the top.
|
|
671
|
+
* land in vblank, or sprites tear on a fixed scanline near the top.
|
|
672
|
+
* A sprite's CGB palette = OAM attr bits 0-2 — that's the whole "color this
|
|
673
|
+
* sprite" story (we write piece[i]-1 straight into the attr byte). */
|
|
547
674
|
static void update_sprites(void) {
|
|
548
675
|
/* Write shadow_oam ($C100) directly with a walking pointer — calling
|
|
549
676
|
* oam_set() six times burns ~10 scanlines of vblank (SDCC call overhead),
|
|
@@ -570,9 +697,9 @@ static void update_sprites(void) {
|
|
|
570
697
|
if (state == ST_TITLE) {
|
|
571
698
|
for (i = 0; i < 12; i++) *o++ = 0;
|
|
572
699
|
} else {
|
|
573
|
-
sx = (uint8_t)((HUD_X +
|
|
700
|
+
sx = (uint8_t)((HUD_X + 1) * 8 + 8);
|
|
574
701
|
for (i = 0; i < 3; i++) {
|
|
575
|
-
*o++ = (uint8_t)((
|
|
702
|
+
*o++ = (uint8_t)((3 + i) * 8 + 16);
|
|
576
703
|
*o++ = sx;
|
|
577
704
|
*o++ = T_GEM;
|
|
578
705
|
*o++ = (uint8_t)(nextp[i] - 1);
|
|
@@ -584,8 +711,28 @@ static void update_sprites(void) {
|
|
|
584
711
|
((void (*)(uint8_t))0xFF80)(0xC1);
|
|
585
712
|
}
|
|
586
713
|
|
|
587
|
-
/*
|
|
588
|
-
*
|
|
714
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
715
|
+
* Per-tile color — the VRAM bank-1 attribute map (VBK register).
|
|
716
|
+
* requires: CGB mode (see the palette idiom above); writes in a VRAM-safe
|
|
717
|
+
* window (LCD off, or a bounded vblank batch like redraw_flush).
|
|
718
|
+
*
|
|
719
|
+
* VRAM is TWO 8KB banks behind the same $8000-$9FFF window; VBK ($FF4F)
|
|
720
|
+
* selects which one the CPU sees. Bank 0 holds what the DMG had: tile
|
|
721
|
+
* pixels + the tile-index maps. Bank 1 at the SAME map address holds one
|
|
722
|
+
* ATTRIBUTE byte per cell:
|
|
723
|
+
* bits 0-2 palette 0-7 ← this game's whole color system
|
|
724
|
+
* bit 3 tile VRAM bank
|
|
725
|
+
* bit 5/6 H/V flip
|
|
726
|
+
* bit 7 BG-over-OBJ priority
|
|
727
|
+
* So coloring a cell is a write PAIR: tile index with VBK=0, attribute with
|
|
728
|
+
* VBK=1, at the SAME offset.
|
|
729
|
+
*
|
|
730
|
+
* FOOTGUN: VBK is global state. Forget to restore VBK=0 and every later
|
|
731
|
+
* "tile" write lands in the attribute map — the screen turns into garbage
|
|
732
|
+
* colors while the tile data you wrote is simply gone. Always end VBK=0
|
|
733
|
+
* (every routine here does).
|
|
734
|
+
* (Direct, unbounded — only safe with the LCD off or in a bounded vblank
|
|
735
|
+
* batch; the in-game path queues instead — see redraw_collect/flush.) */
|
|
589
736
|
static void set_cell(uint8_t mx, uint8_t my, uint8_t tile, uint8_t pal) {
|
|
590
737
|
uint16_t off = (uint16_t)my * 32 + mx;
|
|
591
738
|
VBK = 0;
|
|
@@ -595,6 +742,16 @@ static void set_cell(uint8_t mx, uint8_t my, uint8_t tile, uint8_t pal) {
|
|
|
595
742
|
VBK = 0;
|
|
596
743
|
}
|
|
597
744
|
|
|
745
|
+
/* same write-pair, into the WINDOW's map at $9C00 (see the window idiom) */
|
|
746
|
+
static void set_wcell(uint8_t wx, uint8_t wy, uint8_t tile, uint8_t pal) {
|
|
747
|
+
uint16_t off = WIN_OFF + (uint16_t)wy * 32 + wx;
|
|
748
|
+
VBK = 0;
|
|
749
|
+
VRAM[off] = tile;
|
|
750
|
+
VBK = 1;
|
|
751
|
+
VRAM[off] = pal;
|
|
752
|
+
VBK = 0;
|
|
753
|
+
}
|
|
754
|
+
|
|
598
755
|
/* map an ASCII char to its font tile slot (digits, then A-Z); blank otherwise */
|
|
599
756
|
static uint8_t font_slot(char ch) {
|
|
600
757
|
if (ch >= '0' && ch <= '9') return FONT_BASE + (uint8_t)(ch - '0');
|
|
@@ -609,19 +766,69 @@ static void draw_text(uint8_t col, uint8_t row, const char *s) {
|
|
|
609
766
|
set_cell((uint8_t)(col + i), row, font_slot(s[i]), PAL_OUT);
|
|
610
767
|
}
|
|
611
768
|
|
|
612
|
-
/*
|
|
613
|
-
|
|
614
|
-
|
|
769
|
+
/* draw a NUL-terminated string into the WINDOW map starting at (col,row) */
|
|
770
|
+
static void draw_wtext(uint8_t col, uint8_t row, const char *s) {
|
|
771
|
+
uint8_t i;
|
|
772
|
+
for (i = 0; s[i] != 0; i++)
|
|
773
|
+
set_wcell((uint8_t)(col + i), row, font_slot(s[i]), PAL_OUT);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
777
|
+
* WINDOW-layer HUD — a fixed strip the BG scroll can never move.
|
|
778
|
+
* requires: LCDC bits 5 (window on) + 6 (window map = $9C00), WX/WY set,
|
|
779
|
+
* and HUD text written to the $9C00 map (set_wcell), not the $9800 one.
|
|
780
|
+
*
|
|
781
|
+
* The window is the GB's second BG plane: same tile data, its OWN 32x32
|
|
782
|
+
* map, drawn OVER the BG starting at screen position (WX-7, WY) and
|
|
783
|
+
* extending to the bottom-right. It ignores SCX/SCY completely — that's
|
|
784
|
+
* the point: scroll the playfield all you want, the HUD strip stays put.
|
|
785
|
+
* Classic placements: a bottom status bar (this game: WY=128 → the last
|
|
786
|
+
* 16 pixel rows) or a full-width top bar. It CANNOT be a floating box —
|
|
787
|
+
* the window always runs to the screen's bottom-right corner.
|
|
788
|
+
*
|
|
789
|
+
* Gotchas:
|
|
790
|
+
* - WX is offset by 7: WX=7 is the left edge. WX<7 glitches on hardware.
|
|
791
|
+
* - The window has its OWN line counter: it renders ITS map from window
|
|
792
|
+
* row 0 downward, regardless of WY — our HUD lives at $9C00 rows 0-1.
|
|
793
|
+
* - On CGB the window cells take bank-1 attributes exactly like the BG
|
|
794
|
+
* (set_wcell writes both banks).
|
|
795
|
+
* - This block is DMG-era hardware — it transplants to plain GB examples
|
|
796
|
+
* unchanged; only the bank-1 attribute half is CGB-specific.
|
|
797
|
+
*
|
|
798
|
+
* Window HUD layout (window map rows 0-1):
|
|
799
|
+
* row 0: SC dddddd HI dddddd row 1: LV dd
|
|
800
|
+
* Static labels drawn once at transitions; the digits go through the
|
|
801
|
+
* vblank queue (see redraw_collect) so in-game updates never tear. */
|
|
802
|
+
#define WINY 128 /* screen y where the strip starts */
|
|
803
|
+
#define HUD_SC_X 3 /* score digits, window row 0 */
|
|
804
|
+
#define HUD_HI_X 13 /* hi-score digits, window row 0 */
|
|
805
|
+
#define HUD_LV_X 3 /* level digits, window row 1 */
|
|
806
|
+
|
|
807
|
+
/* paint the whole window strip: dark backdrop + labels (LCD off only) */
|
|
808
|
+
static void draw_window_static(void) {
|
|
809
|
+
uint8_t x, y;
|
|
810
|
+
for (y = 0; y < 2; y++)
|
|
811
|
+
for (x = 0; x < 20; x++) set_wcell(x, y, T_BLANK, PAL_OUT);
|
|
812
|
+
draw_wtext(0, 0, "SC");
|
|
813
|
+
draw_wtext(10, 0, "HI");
|
|
814
|
+
draw_wtext(0, 1, "LV");
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/* draw every dynamic HUD value directly (LCD off / transitions only —
|
|
818
|
+
* in-game updates go through the queue, 4 cells per vblank) */
|
|
819
|
+
static void draw_hud_now(void) {
|
|
615
820
|
uint8_t i;
|
|
616
|
-
for (i = 0; i < 6; i++)
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
821
|
+
for (i = 0; i < 6; i++) {
|
|
822
|
+
set_wcell((uint8_t)(HUD_SC_X + i), 0, FONT_BASE + score_d[i], PAL_OUT);
|
|
823
|
+
set_wcell((uint8_t)(HUD_HI_X + i), 0, FONT_BASE + hi_d[i], PAL_OUT);
|
|
824
|
+
}
|
|
825
|
+
set_wcell(HUD_LV_X, 1, FONT_BASE + (uint8_t)(level / 10), PAL_OUT);
|
|
826
|
+
set_wcell((uint8_t)(HUD_LV_X + 1), 1, FONT_BASE + (uint8_t)(level % 10), PAL_OUT);
|
|
620
827
|
}
|
|
621
828
|
|
|
622
|
-
/* Lay down the unchanging screen: clear the whole map, draw the well's
|
|
623
|
-
* right
|
|
624
|
-
* (it writes
|
|
829
|
+
/* Lay down the unchanging screen: clear the whole BG map, draw the well's
|
|
830
|
+
* walls + floor, the right panel, and the window HUD. Only called with the
|
|
831
|
+
* LCD off (it writes entire maps at once). */
|
|
625
832
|
static void draw_static(void) {
|
|
626
833
|
uint8_t x, y;
|
|
627
834
|
uint16_t off;
|
|
@@ -638,15 +845,12 @@ static void draw_static(void) {
|
|
|
638
845
|
}
|
|
639
846
|
for (x = (uint8_t)(WELL_MX - 1); x <= (uint8_t)(WELL_MX + COLS); x++)
|
|
640
847
|
set_cell(x, (uint8_t)(WELL_MY + ROWS), T_WALL, PAL_WELL);
|
|
641
|
-
|
|
642
|
-
draw_text(HUD_X, 1, "SCORE");
|
|
643
|
-
draw_text(HUD_X, 4, "LEVEL");
|
|
644
|
-
draw_text(HUD_X, 7, "NEXT");
|
|
848
|
+
draw_window_static();
|
|
645
849
|
}
|
|
646
850
|
|
|
647
|
-
/* Full LOCKED-well
|
|
648
|
-
* LCD OFF (boot / title↔game transitions), where writing all
|
|
649
|
-
* at once is safe. */
|
|
851
|
+
/* Full LOCKED-well repaint from the grid (no piece — that's a sprite). Used
|
|
852
|
+
* only with the LCD OFF (boot / title↔game transitions), where writing all
|
|
853
|
+
* changed cells at once is safe. */
|
|
650
854
|
static void redraw_all(void) {
|
|
651
855
|
uint8_t r, c, col;
|
|
652
856
|
uint8_t i = 0;
|
|
@@ -667,19 +871,26 @@ static void redraw_all(void) {
|
|
|
667
871
|
}
|
|
668
872
|
}
|
|
669
873
|
|
|
670
|
-
/* ──
|
|
874
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
875
|
+
* Deferred well/HUD rendering — the vblank COLLECT/FLUSH queue.
|
|
876
|
+
* requires: update_sprites + redraw_flush as the FIRST two things after
|
|
877
|
+
* wait_vblank (in that order), batches capped at REDRAW_BUDGET, and no
|
|
878
|
+
* LCDC bit-7 toggling in-game.
|
|
879
|
+
*
|
|
671
880
|
* This core blanks the whole frame on ANY LCDC bit-7 toggle (a strobe we
|
|
672
881
|
* must never do), AND it occasionally drops a VRAM write even at the start
|
|
673
882
|
* of vblank. So in-game we never touch the LCD; instead:
|
|
674
|
-
* COLLECT — queue work (RAM only): changed cells after a lock, the HUD
|
|
675
|
-
* and — when idle — a rolling SCRUB of the whole well.
|
|
883
|
+
* COLLECT — queue work (RAM only): changed cells after a lock, the HUD
|
|
884
|
+
* digits, and — when idle — a rolling SCRUB of the whole well.
|
|
676
885
|
* FLUSH — write the queue to VRAM as the FIRST thing after wait_vblank.
|
|
677
886
|
* The scrub re-writes every well cell from the grid every ~0.2s, so any
|
|
678
887
|
* dropped write self-corrects instead of becoming a permanent wrong color
|
|
679
|
-
* (the "3 oranges that won't clear" bug). Idempotent ⇒ invisible.
|
|
680
|
-
|
|
681
|
-
* — overrunning into active display drops writes (a garbage "burst" on
|
|
682
|
-
* frames before the scrub heals them).
|
|
888
|
+
* (the "3 oranges that won't clear" bug). Idempotent ⇒ invisible.
|
|
889
|
+
* Batches are kept small so the whole flush fits in vblank AFTER the OAM
|
|
890
|
+
* DMA — overrunning into active display drops writes (a garbage "burst" on
|
|
891
|
+
* lock frames before the scrub heals them).
|
|
892
|
+
* Queue offsets are plain offsets from $9800, so the same queue serves the
|
|
893
|
+
* BG map (well) and the window map at $9800+$400 (HUD digits). */
|
|
683
894
|
#define REDRAW_BUDGET 4 /* changed well cells per frame (responsive) */
|
|
684
895
|
#define SCRUB_N 4 /* idle cells re-written per frame (self-heal) */
|
|
685
896
|
#define WQ_MAX 6 /* queue capacity (≤4 pushed per frame) */
|
|
@@ -711,9 +922,14 @@ static void wq_text(uint8_t col, uint8_t row, const char *s) {
|
|
|
711
922
|
wq_push((uint16_t)row * 32 + col + i, font_slot(s[i]), PAL_OUT);
|
|
712
923
|
}
|
|
713
924
|
|
|
925
|
+
/* queue one window-HUD digit cell (window map = offset $400) */
|
|
926
|
+
static void wq_wdigit(uint8_t col, uint8_t row, uint8_t digit) {
|
|
927
|
+
wq_push(WIN_OFF + (uint16_t)row * 32 + col, FONT_BASE + digit, PAL_OUT);
|
|
928
|
+
}
|
|
929
|
+
|
|
714
930
|
/* Fill the queue with the next batch of pending changes (RAM only).
|
|
715
931
|
* Each branch pushes at most REDRAW_BUDGET cells, so the flush always fits
|
|
716
|
-
* in vblank; the HUD and game-over text are split across
|
|
932
|
+
* in vblank; the HUD digits and game-over text are split across frames. */
|
|
717
933
|
static void redraw_collect(void) {
|
|
718
934
|
uint8_t col, k, r, c, i;
|
|
719
935
|
wq_n = 0;
|
|
@@ -733,14 +949,20 @@ static void redraw_collect(void) {
|
|
|
733
949
|
if (scan_i >= NCELL) { scanning = 0; hud_pending = 1; hud_phase = 0; }
|
|
734
950
|
} else if (hud_pending) {
|
|
735
951
|
if (hud_phase == 0) { /* score digits 0-3 */
|
|
736
|
-
for (i = 0; i < 4; i++)
|
|
737
|
-
wq_push((uint16_t)2 * 32 + HUD_X + i, FONT_BASE + score_d[i], PAL_OUT);
|
|
952
|
+
for (i = 0; i < 4; i++) wq_wdigit((uint8_t)(HUD_SC_X + i), 0, score_d[i]);
|
|
738
953
|
hud_phase = 1;
|
|
739
|
-
} else {
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
954
|
+
} else if (hud_phase == 1) { /* score 4-5 + level */
|
|
955
|
+
wq_wdigit(HUD_SC_X + 4, 0, score_d[4]);
|
|
956
|
+
wq_wdigit(HUD_SC_X + 5, 0, score_d[5]);
|
|
957
|
+
wq_wdigit(HUD_LV_X, 1, (uint8_t)(level / 10));
|
|
958
|
+
wq_wdigit(HUD_LV_X + 1, 1, (uint8_t)(level % 10));
|
|
959
|
+
hud_phase = 2;
|
|
960
|
+
} else if (hud_phase == 2) { /* hi-score digits 0-3 */
|
|
961
|
+
for (i = 0; i < 4; i++) wq_wdigit((uint8_t)(HUD_HI_X + i), 0, hi_d[i]);
|
|
962
|
+
hud_phase = 3;
|
|
963
|
+
} else { /* hi-score digits 4-5 */
|
|
964
|
+
wq_wdigit(HUD_HI_X + 4, 0, hi_d[4]);
|
|
965
|
+
wq_wdigit(HUD_HI_X + 5, 0, hi_d[5]);
|
|
744
966
|
hud_pending = 0;
|
|
745
967
|
if (state == ST_OVER) { over_pending = 1; over_phase = 0; }
|
|
746
968
|
}
|
|
@@ -762,9 +984,11 @@ static void redraw_collect(void) {
|
|
|
762
984
|
}
|
|
763
985
|
}
|
|
764
986
|
|
|
765
|
-
/* Write the queued cells to VRAM. MUST run first after wait_vblank
|
|
766
|
-
* MUST finish inside the ~10-line vblank window or
|
|
767
|
-
* (not array indexing) — SDCC sm83 generates far
|
|
987
|
+
/* Write the queued cells to VRAM. MUST run first after wait_vblank (right
|
|
988
|
+
* after the OAM DMA), and MUST finish inside the ~10-line vblank window or
|
|
989
|
+
* writes drop. Pointer-walk (not array indexing) — SDCC sm83 generates far
|
|
990
|
+
* tighter code for *p++. Each cell is the bank pair: tile (VBK=0) then
|
|
991
|
+
* attribute (VBK=1) at the same offset — see the per-tile color idiom. */
|
|
768
992
|
static void redraw_flush(void) {
|
|
769
993
|
uint8_t k = wq_n;
|
|
770
994
|
uint16_t *op;
|
|
@@ -782,15 +1006,17 @@ static void redraw_flush(void) {
|
|
|
782
1006
|
wq_n = 0;
|
|
783
1007
|
}
|
|
784
1008
|
|
|
785
|
-
/*
|
|
786
|
-
|
|
1009
|
+
/* ── GAME LOGIC (clay — reshape freely) ── title screen.
|
|
1010
|
+
* A jagged pile of all six gem colors dresses the well — it doubles as the
|
|
1011
|
+
* "this cart is COLOR" proof the moment the title appears. */
|
|
1012
|
+
static const uint8_t title_heights[COLS] = { 4, 6, 3, 7, 5, 6, 4, 5 };
|
|
787
1013
|
|
|
788
1014
|
static void draw_title(void) {
|
|
789
1015
|
uint8_t x, y, c, k, color;
|
|
790
|
-
/* clear the right panel (
|
|
791
|
-
for (y = 0; y <=
|
|
1016
|
+
/* clear the right panel (NEXT label from a previous game) */
|
|
1017
|
+
for (y = 0; y <= 15; y++)
|
|
792
1018
|
for (x = 10; x <= 19; x++) set_cell(x, y, T_EMPTY, PAL_OUT);
|
|
793
|
-
/* decorative gems piled at the bottom of the well */
|
|
1019
|
+
/* decorative gems piled at the bottom of the well, cycling palettes */
|
|
794
1020
|
color = 1;
|
|
795
1021
|
for (c = 0; c < COLS; c++) {
|
|
796
1022
|
for (k = 0; k < title_heights[c]; k++) {
|
|
@@ -800,20 +1026,23 @@ static void draw_title(void) {
|
|
|
800
1026
|
color++; if (color > NCOLORS) color = 1;
|
|
801
1027
|
}
|
|
802
1028
|
}
|
|
803
|
-
/*
|
|
804
|
-
draw_text(
|
|
805
|
-
draw_text(
|
|
806
|
-
draw_text(HUD_X, 10, "START");
|
|
1029
|
+
/* game name + prompt, centered across the full 20-column screen */
|
|
1030
|
+
draw_text((uint8_t)((20 - (sizeof(GAME_TITLE) - 1)) / 2), 2, GAME_TITLE);
|
|
1031
|
+
draw_text(4, 4, "PRESS START");
|
|
807
1032
|
}
|
|
808
1033
|
|
|
809
|
-
/* LCD off / on — only used to bracket the full-screen rebuilds at the title
|
|
810
|
-
* game-start transitions. NEVER call these from the in-game loop (the
|
|
811
|
-
* blanks the whole screen — a flash/strobe).
|
|
1034
|
+
/* LCD off / on — only used to bracket the full-screen rebuilds at the title
|
|
1035
|
+
* and game-start transitions. NEVER call these from the in-game loop (the
|
|
1036
|
+
* off-frame blanks the whole screen — a flash/strobe). blit_on enables BG +
|
|
1037
|
+
* OBJ + the WINDOW (map $9C00) — see the window idiom for the LCDC bits. */
|
|
812
1038
|
static void blit_off(void) { wait_vblank(); LCDC = 0; }
|
|
813
|
-
static void blit_on(void) {
|
|
1039
|
+
static void blit_on(void) {
|
|
1040
|
+
LCDC = LCDC_LCD_ON | LCDC_BG_ON | LCDC_OBJ_ON | LCDC_TILE_DATA_LO
|
|
1041
|
+
| LCDC_WINDOW_ON | LCDC_WINDOW_MAP_HI;
|
|
1042
|
+
}
|
|
814
1043
|
|
|
815
1044
|
/* zero the board and all run stats for a fresh game (shadow set to 0xFF so the
|
|
816
|
-
* first redraw repaints every cell). Does not touch music_on. */
|
|
1045
|
+
* first redraw repaints every cell). Does not touch music_on or hi_d. */
|
|
817
1046
|
static void reset_state(void) {
|
|
818
1047
|
uint8_t i;
|
|
819
1048
|
for (i = 0; i < NCELL; i++) grid[i] = 0;
|
|
@@ -837,13 +1066,14 @@ static void start_game(void) {
|
|
|
837
1066
|
blit_off();
|
|
838
1067
|
draw_static();
|
|
839
1068
|
redraw_all();
|
|
840
|
-
|
|
1069
|
+
draw_text(HUD_X, 1, "NEXT");
|
|
1070
|
+
draw_hud_now();
|
|
841
1071
|
blit_on();
|
|
842
1072
|
update_sprites();
|
|
843
1073
|
scanning = 0; hud_pending = 0; over_pending = 0; wq_n = 0;
|
|
844
1074
|
}
|
|
845
1075
|
|
|
846
|
-
/* show the title screen (
|
|
1076
|
+
/* show the title screen (gem pile + name + PRESS START + persisted HI) */
|
|
847
1077
|
static void go_title(void) {
|
|
848
1078
|
reset_state();
|
|
849
1079
|
piece_active = 0;
|
|
@@ -852,22 +1082,44 @@ static void go_title(void) {
|
|
|
852
1082
|
draw_static();
|
|
853
1083
|
redraw_all();
|
|
854
1084
|
draw_title();
|
|
1085
|
+
draw_hud_now();
|
|
855
1086
|
next_dirty = 1;
|
|
856
1087
|
blit_on();
|
|
857
1088
|
update_sprites();
|
|
858
1089
|
scanning = 0; hud_pending = 0; over_pending = 0; wq_n = 0;
|
|
859
1090
|
}
|
|
860
1091
|
|
|
1092
|
+
/* the run is over: persist a new record, then let the queue paint GAME OVER
|
|
1093
|
+
* + the updated HI digits (hud_pending → over_pending chain). */
|
|
1094
|
+
static void game_over(void) {
|
|
1095
|
+
piece_active = 0;
|
|
1096
|
+
state = ST_OVER;
|
|
1097
|
+
sfx_over();
|
|
1098
|
+
if (score_beats_hi()) {
|
|
1099
|
+
uint8_t i;
|
|
1100
|
+
for (i = 0; i < 6; i++) hi_d[i] = score_d[i];
|
|
1101
|
+
hiscore_save(); /* battery SRAM — survives power-off */
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
861
1105
|
void main(void) {
|
|
862
1106
|
uint8_t pad, prev = 0, t, rate, g;
|
|
863
1107
|
|
|
864
|
-
/*
|
|
865
|
-
*
|
|
1108
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
1109
|
+
* Boot order: LCD defaults (installs the OAM-DMA HRAM stub) → vblank IRQ
|
|
1110
|
+
* (so wait_vblank HALTs instead of busy-polling LY — the poll runs at
|
|
1111
|
+
* ~1/30 speed on this core) → APU on → LCD OFF → then all the bulk VRAM
|
|
1112
|
+
* work (tiles, palettes, maps). Tile/palette/map uploads REQUIRE a
|
|
1113
|
+
* VRAM-safe window and boot does them all at once, so LCD-off is the
|
|
1114
|
+
* only sane choice here. The window position registers are plain I/O —
|
|
1115
|
+
* set once, they hold. */
|
|
866
1116
|
lcd_init_default();
|
|
867
1117
|
enable_vblank_irq();
|
|
868
1118
|
sound_init();
|
|
869
1119
|
music_on = 1; /* background music on by default (SELECT toggles) */
|
|
870
1120
|
LCDC = 0;
|
|
1121
|
+
WY = WINY; /* window HUD strip: bottom 16 pixel rows */
|
|
1122
|
+
WX = 7; /* WX is offset by 7 — this is the left edge */
|
|
871
1123
|
|
|
872
1124
|
upload_tile(T_EMPTY, tile_empty);
|
|
873
1125
|
upload_tile(T_GEM, tile_gem);
|
|
@@ -883,6 +1135,7 @@ void main(void) {
|
|
|
883
1135
|
load_obj_palettes();
|
|
884
1136
|
oam_clear();
|
|
885
1137
|
|
|
1138
|
+
hiscore_load(); /* battery SRAM — 0 on a fresh cart */
|
|
886
1139
|
go_title();
|
|
887
1140
|
|
|
888
1141
|
/* Main loop, one pass per frame. The order is deliberate: the two VRAM/OAM
|
|
@@ -902,8 +1155,11 @@ void main(void) {
|
|
|
902
1155
|
if ((pad & PAD_SELECT) && !(prev & PAD_SELECT)) music_toggle();
|
|
903
1156
|
|
|
904
1157
|
if (state == ST_TITLE) {
|
|
1158
|
+
/* ── GAME LOGIC (clay — reshape freely) ── press-start title
|
|
1159
|
+
* (handheld: no 2P mode select — see the header note) */
|
|
905
1160
|
if ((pad & PAD_START) && !(prev & PAD_START)) start_game();
|
|
906
1161
|
} else if (state == ST_PLAY) {
|
|
1162
|
+
/* ── GAME LOGIC (clay — reshape freely) ── one frame of play */
|
|
907
1163
|
if ((pad & PAD_LEFT) && !(prev & PAD_LEFT)
|
|
908
1164
|
&& !collides((uint8_t)(piece_x - 1), piece_y)) { piece_x--; sfx_move(); }
|
|
909
1165
|
if ((pad & PAD_RIGHT) && !(prev & PAD_RIGHT)
|
|
@@ -921,25 +1177,23 @@ void main(void) {
|
|
|
921
1177
|
sfx_drop();
|
|
922
1178
|
lock_and_resolve();
|
|
923
1179
|
spawn();
|
|
924
|
-
if (state == ST_OVER) sfx_over();
|
|
925
1180
|
start_redraw();
|
|
926
1181
|
}
|
|
927
1182
|
|
|
928
1183
|
rate = (pad & PAD_DOWN) ? 3 : cur_fall_rate;
|
|
929
|
-
if (++fall_timer >= rate) {
|
|
1184
|
+
if (state == ST_PLAY && ++fall_timer >= rate) {
|
|
930
1185
|
fall_timer = 0;
|
|
931
1186
|
if (collides(piece_x, (uint8_t)(piece_y + 1))) {
|
|
932
1187
|
sfx_drop();
|
|
933
1188
|
lock_and_resolve();
|
|
934
1189
|
spawn();
|
|
935
|
-
if (state == ST_OVER) sfx_over();
|
|
936
1190
|
start_redraw();
|
|
937
1191
|
} else {
|
|
938
1192
|
piece_y++;
|
|
939
1193
|
}
|
|
940
1194
|
}
|
|
941
|
-
} else { /* ST_OVER — START
|
|
942
|
-
if ((pad & PAD_START) && !(prev & PAD_START))
|
|
1195
|
+
} else { /* ST_OVER — START returns to the title (shows the new HI) */
|
|
1196
|
+
if ((pad & PAD_START) && !(prev & PAD_START)) go_title();
|
|
943
1197
|
}
|
|
944
1198
|
|
|
945
1199
|
redraw_collect(); /* queue next frame's VRAM writes (RAM only) */
|