romdevtools 0.28.0 → 0.30.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 +53 -43
- package/CHANGELOG.md +91 -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 +12 -12
- package/src/cores/wasm/bluemsx_libretro.js +1 -1
- package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
- package/src/cores/wasm/fceumm_libretro.js +1 -1
- package/src/cores/wasm/fceumm_libretro.wasm +0 -0
- package/src/cores/wasm/gambatte_libretro.js +1 -1
- package/src/cores/wasm/gambatte_libretro.wasm +0 -0
- package/src/cores/wasm/geargrafx_libretro.js +1 -1
- package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
- package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
- package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
- package/src/cores/wasm/handy_libretro.js +1 -1
- package/src/cores/wasm/handy_libretro.wasm +0 -0
- package/src/cores/wasm/mgba_libretro.js +1 -1
- package/src/cores/wasm/mgba_libretro.wasm +0 -0
- package/src/cores/wasm/prosystem_libretro.js +1 -1
- package/src/cores/wasm/prosystem_libretro.wasm +0 -0
- package/src/cores/wasm/snes9x_libretro.js +1 -1
- package/src/cores/wasm/snes9x_libretro.wasm +0 -0
- package/src/cores/wasm/stella2014_libretro.js +1 -1
- package/src/cores/wasm/stella2014_libretro.wasm +0 -0
- package/src/cores/wasm/vice_x64_libretro.js +1 -1
- package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
- package/src/host/LibretroHost.js +84 -8
- 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/memory.js +131 -24
- 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/record.js +6 -7
- package/src/mcp/tools/rom-id.js +5 -1
- package/src/mcp/tools/run-until.js +12 -4
- 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 +53 -10
- 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/_guides/ROMHACKING_PLAYBOOK.md +32 -3
- 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,207 +1,971 @@
|
|
|
1
|
-
/* ── platformer.c — Game Boy Color
|
|
1
|
+
/* ── platformer.c — SPECTRA BOUND: Game Boy Color side-scrolling platformer ──
|
|
2
2
|
*
|
|
3
|
-
* A
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* A COMPLETE, working game — title screen, gravity + jump physics with
|
|
4
|
+
* sub-pixel precision, one-way platforms, pits and spikes, coins + distance
|
|
5
|
+
* scoring, persistent battery hi-score (MBC1+RAM+BATTERY SRAM), music + SFX,
|
|
6
|
+
* the Game Boy's signature WINDOW-layer fixed HUD over an SCX-scrolling
|
|
7
|
+
* looping level — and the GBC's signature feature on top of all of it:
|
|
8
|
+
* TRUE per-tile color. Sky, grass, dirt, platforms and hazards are FIVE
|
|
9
|
+
* REAL CGB palettes (15-bit BGR, loaded through BCPS/BCPD), assigned per BG
|
|
10
|
+
* cell through the VRAM bank-1 attribute map, and the player / coins / spikes
|
|
11
|
+
* are their own OBJ palettes through OCPS — not a colorized monochrome game.
|
|
7
12
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
13
|
+
* THE GAME: an endless one-way runner. Hold RIGHT to gallop; the world
|
|
14
|
+
* scrolls past a scroll wall (the classic runner camera). A=jump (with
|
|
15
|
+
* coyote-free, grounded-only launch). Hop the lethal pits and the drifting
|
|
16
|
+
* spikes, scoop coins, and the longer you survive the higher your distance
|
|
17
|
+
* score climbs. Three lives; the battery remembers your best run forever.
|
|
18
|
+
* SELECT toggles the music.
|
|
10
19
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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) — level layout, physics tuning, scoring, art: reshape
|
|
25
|
+
* freely.
|
|
13
26
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
27
|
+
* SINGLE-PLAYER, honestly: the Game Boy's "player 2" is a LINK CABLE, which
|
|
28
|
+
* one emulator instance cannot provide — so handheld examples ship a
|
|
29
|
+
* press-start title and no 2P mode instead of faking one.
|
|
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 level is a 256-px-wide COLUMN MAP painted ONCE into the wrapping
|
|
42
|
+
* 32-wide BG map (bank-0 tiles + bank-1 palette attributes), so the uint8
|
|
43
|
+
* SCX scroll wraps PERFECTLY seamless — an endless looping run. The color
|
|
44
|
+
* travels with the tiles: each cell's bank-1 attribute byte scrolls along
|
|
45
|
+
* with its tile, so a grass cell stays green wherever it slides on 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.
|
|
17
50
|
*/
|
|
18
51
|
|
|
19
52
|
#include "gb_hardware.h"
|
|
20
53
|
#include "gb_runtime.h"
|
|
54
|
+
#include "font.h"
|
|
21
55
|
|
|
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 "SPECTRA BOUND"
|
|
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 grass
|
|
65
|
+
* tile reads green or any other palette purely by its attribute byte. */
|
|
22
66
|
static const uint8_t tile_blank[16] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 };
|
|
23
|
-
static const uint8_t tile_player[16] = {
|
|
24
|
-
|
|
25
|
-
0xFF,
|
|
67
|
+
static const uint8_t tile_player[16] = { /* round body + face */
|
|
68
|
+
0x3C,0x00, 0x7E,0x24, 0xFF,0x24, 0xFF,0x00,
|
|
69
|
+
0xFF,0x00, 0x7E,0x00, 0x66,0x00, 0x66,0x00,
|
|
70
|
+
};
|
|
71
|
+
static const uint8_t tile_player_jump[16] = { /* arms up, mid-leap */
|
|
72
|
+
0x18,0x00, 0x7E,0x24, 0xFF,0x24, 0xFF,0x00,
|
|
73
|
+
0xE7,0x00, 0xC3,0x00, 0x81,0x00, 0x00,0x00,
|
|
74
|
+
};
|
|
75
|
+
static const uint8_t tile_coin[16] = { /* faceted gem disc */
|
|
76
|
+
0x00,0x3C, 0x30,0x4E, 0x60,0x9F, 0x40,0xBF,
|
|
77
|
+
0x02,0xFF, 0x06,0xFF, 0x1C,0x7E, 0x00,0x3C,
|
|
78
|
+
};
|
|
79
|
+
static const uint8_t tile_spike[16] = { /* solid spike (value 3) */
|
|
80
|
+
0x00,0x00, 0x18,0x18, 0x18,0x18, 0x3C,0x3C,
|
|
81
|
+
0x3C,0x3C, 0x7E,0x7E, 0x7E,0x7E, 0xFF,0xFF,
|
|
82
|
+
};
|
|
83
|
+
/* Backdrop tiles. tile_sky carries two value-1 dot pixels so even "empty"
|
|
84
|
+
* sky is never one flat colour (the render-health floor every example
|
|
85
|
+
* keeps), and the dots make horizontal scroll motion visible everywhere. */
|
|
86
|
+
static const uint8_t tile_sky[16] = { /* faint specks (value 1) */
|
|
87
|
+
0x20,0x00, 0x00,0x00, 0x00,0x00, 0x02,0x00,
|
|
88
|
+
0x00,0x00, 0x08,0x00, 0x00,0x00, 0x40,0x00,
|
|
89
|
+
};
|
|
90
|
+
static const uint8_t tile_cloud[16] = { /* value-3 puff */
|
|
91
|
+
0x00,0x00, 0x18,0x18, 0x3C,0x3C, 0x7E,0x7E,
|
|
92
|
+
0x7E,0x7E, 0x00,0x00, 0x00,0x00, 0x00,0x00,
|
|
26
93
|
};
|
|
27
|
-
static const uint8_t
|
|
28
|
-
|
|
29
|
-
|
|
94
|
+
static const uint8_t tile_dirt[16] = { /* value-2 fill, value-1 grit*/
|
|
95
|
+
0x00,0xFF, 0x20,0xDF, 0x00,0xFF, 0x04,0xFB,
|
|
96
|
+
0x00,0xFF, 0x80,0x7F, 0x00,0xFF, 0x08,0xF7,
|
|
30
97
|
};
|
|
31
|
-
/*
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
* sky; tile_ground is a textured dirt fill under the floor line. */
|
|
35
|
-
static const uint8_t tile_sky[16] = {
|
|
36
|
-
0x00,0x00, 0x00,0x00, 0x00,0x00, 0x20,0x20,
|
|
37
|
-
0x00,0x00, 0x00,0x00, 0x02,0x02, 0x00,0x00,
|
|
98
|
+
static const uint8_t tile_grass[16] = { /* value-3 turf over dirt */
|
|
99
|
+
0xFF,0xFF, 0xFF,0xFF, 0x00,0xFF, 0x20,0xDF,
|
|
100
|
+
0x00,0xFF, 0x04,0xFB, 0x00,0xFF, 0x00,0xFF,
|
|
38
101
|
};
|
|
39
|
-
static const uint8_t
|
|
40
|
-
0xFF,
|
|
41
|
-
0xFF,0x00,
|
|
102
|
+
static const uint8_t tile_plat[16] = { /* one-way slab top edge */
|
|
103
|
+
0xFF,0xFF, 0xFF,0xFF, 0x00,0xFF, 0x00,0xDB,
|
|
104
|
+
0x00,0xFF, 0x00,0x00, 0x00,0x00, 0x00,0x00,
|
|
42
105
|
};
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
#define
|
|
55
|
-
#define
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
106
|
+
static const uint8_t tile_hudbar[16] = { /* solid value-3 divider */
|
|
107
|
+
0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF,
|
|
108
|
+
0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/* Tile indices ($8000 unsigned addressing — LCDC bit 4 set below). Sprites
|
|
112
|
+
* and BG share the $8000 table in this layout, so one upload serves both.
|
|
113
|
+
* Font glyphs follow at FONT_BASE (digits 0-9, then A-Z). */
|
|
114
|
+
#define T_BLANK 0
|
|
115
|
+
#define T_PLAYER 1
|
|
116
|
+
#define T_JUMP 2
|
|
117
|
+
#define T_COIN 3
|
|
118
|
+
#define T_SPIKE 4
|
|
119
|
+
#define T_SKY 5
|
|
120
|
+
#define T_CLOUD 6
|
|
121
|
+
#define T_DIRT 7
|
|
122
|
+
#define T_GRASS 8
|
|
123
|
+
#define T_PLAT 9
|
|
124
|
+
#define T_HUDBAR 10
|
|
125
|
+
#define FONT_BASE 16 /* digit d = 16+d, letter L = 16+10+idx (see font.h) */
|
|
126
|
+
|
|
127
|
+
/* ── GAME LOGIC (clay — reshape freely) ── the CGB palette TABLE (the colours
|
|
128
|
+
* themselves are art; the LOADER below is the hardware idiom).
|
|
129
|
+
* 15-bit BGR: 5 bits each, blue in the high bits — RGB() packs it. Colour 0
|
|
130
|
+
* of a BG palette is the cell's "background" shade; for OBJ palettes colour 0
|
|
131
|
+
* is transparent (the scene shows through). */
|
|
132
|
+
#define RGB(r,g,b) ((uint16_t)(((uint16_t)(b)<<10)|((uint16_t)(g)<<5)|(r)))
|
|
133
|
+
|
|
134
|
+
/* BG palette slots (bank-1 attribute byte bits 0-2 select one of these). */
|
|
135
|
+
#define PAL_SKY 0 /* daytime sky + clouds */
|
|
136
|
+
#define PAL_GRASS 1 /* grass turf top */
|
|
137
|
+
#define PAL_DIRT 2 /* dirt fill / pit walls */
|
|
138
|
+
#define PAL_PLAT 3 /* floating slabs */
|
|
139
|
+
#define PAL_HUD 4 /* HUD bar + all text */
|
|
140
|
+
|
|
141
|
+
static const uint16_t bg_palettes[8][4] = {
|
|
142
|
+
/* 0 sky */ { RGB(18,26,31), RGB(28,31,31), RGB(10,18,28), RGB(31,31,31) },
|
|
143
|
+
/* 1 grass */ { RGB(6,18,8), RGB(12,28,10), RGB(3,12,4), RGB(20,31,16) },
|
|
144
|
+
/* 2 dirt */ { RGB(10,7,4), RGB(18,12,6), RGB(6,4,2), RGB(24,17,9) },
|
|
145
|
+
/* 3 plat */ { RGB(14,8,20), RGB(24,16,31), RGB(8,3,14), RGB(30,24,31) },
|
|
146
|
+
/* 4 hud */ { RGB(2,2,6), RGB(8,9,16), RGB(2,2,6), RGB(31,31,31) },
|
|
147
|
+
/* 5 spare */ { RGB(0,0,0), RGB(10,10,10), RGB(20,20,20), RGB(31,31,31) },
|
|
148
|
+
/* 6 spare */ { RGB(0,0,0), RGB(10,10,10), RGB(20,20,20), RGB(31,31,31) },
|
|
149
|
+
/* 7 spare */ { RGB(0,0,0), RGB(10,10,10), RGB(20,20,20), RGB(31,31,31) },
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/* OBJ palette slots (OAM attr bits 0-2 select one of these). Colour 0 is
|
|
153
|
+
* always transparent. */
|
|
154
|
+
#define OPAL_PLAYER 0 /* sky-blue hero, white face */
|
|
155
|
+
#define OPAL_COIN 1 /* golden gem */
|
|
156
|
+
#define OPAL_SPIKE 2 /* danger red */
|
|
157
|
+
|
|
158
|
+
static const uint16_t obj_palettes[8][4] = {
|
|
159
|
+
/* 0 player */ { 0, RGB(10,20,31), RGB(31,31,31), RGB(2,6,16) },
|
|
160
|
+
/* 1 coin */ { 0, RGB(31,28,6), RGB(31,20,2), RGB(20,12,0) },
|
|
161
|
+
/* 2 spike */ { 0, RGB(31,8,8), RGB(20,2,2), RGB(31,24,16) },
|
|
162
|
+
/* 3 spare */ { 0, RGB(20,20,20), RGB(31,31,31), RGB(10,10,10) },
|
|
163
|
+
/* 4 spare */ { 0, RGB(20,20,20), RGB(31,31,31), RGB(10,10,10) },
|
|
164
|
+
/* 5 spare */ { 0, RGB(20,20,20), RGB(31,31,31), RGB(10,10,10) },
|
|
165
|
+
/* 6 spare */ { 0, RGB(20,20,20), RGB(31,31,31), RGB(10,10,10) },
|
|
166
|
+
/* 7 spare */ { 0, RGB(20,20,20), RGB(31,31,31), RGB(10,10,10) },
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
170
|
+
* THE WINDOW-LAYER HUD — the Game Boy's signature "fixed HUD over a
|
|
171
|
+
* scrolling world" technique. The window is a second BG plane with its own
|
|
172
|
+
* 32×32 tile map and NO scroll registers: it always draws its map from
|
|
173
|
+
* (0,0), pinned to the screen, on top of the BG. So the HUD lives in the
|
|
174
|
+
* window and the playfield lives in the BG — SCX scrolls the world all it
|
|
175
|
+
* likes and the HUD never moves. No raster splits, no IRQ timing (the NES
|
|
176
|
+
* needs a sprite-0 polling dance for this exact effect; on GB it's three
|
|
177
|
+
* register writes). On CGB the window cells take bank-1 palette attributes
|
|
178
|
+
* exactly like the BG (set_wcell writes both banks).
|
|
179
|
+
*
|
|
180
|
+
* The three registers, and their two famous footguns:
|
|
181
|
+
* WY ($FF4A) — first screen LINE the window covers. We use 128: lines
|
|
182
|
+
* 0-127 are playfield, 128-143 (two tile rows) are the HUD strip.
|
|
183
|
+
* WX ($FF4B) — screen column PLUS SEVEN. WX=7 means "left edge". The -7
|
|
184
|
+
* offset is hardware fact: WX=0..6 glitches, WX≥167 is off-screen.
|
|
185
|
+
* LCDC bit 5 — window enable; bit 6 — which map it reads ($9800/$9C00).
|
|
186
|
+
*
|
|
187
|
+
* FOOTGUN 1 — "the window ate the bottom of my screen": once the window
|
|
188
|
+
* starts on a line it covers EVERY line from there DOWN, full width. There
|
|
189
|
+
* is no window height register. That is why GB HUDs sit at the BOTTOM of the
|
|
190
|
+
* screen. A TOP HUD needs a STAT-interrupt LYC trick — a different, fragile
|
|
191
|
+
* idiom; don't drift into it by accident by setting WY=0.
|
|
192
|
+
*
|
|
193
|
+
* FOOTGUN 2 — sprites are NOT clipped by the window. OBJs draw over it, so a
|
|
194
|
+
* sprite below line 128 sits ON the HUD. Gameplay keeps every object above
|
|
195
|
+
* PLAY_H (spikes stand on the ground; a player falling into a pit dies at
|
|
196
|
+
* PLAY_H-8, the frame before its sprite would touch the HUD).
|
|
197
|
+
*
|
|
198
|
+
* Requires: window map at $9C00 (LCDC bit 6), tile data at $8000 (bit 4),
|
|
199
|
+
* WX=7, WY=PLAY_H, LCDC bit 5 set during play (title turns the window off). */
|
|
200
|
+
#define PLAY_H 128 /* first HUD line = window top */
|
|
201
|
+
#define LCDC_TITLE (LCDC_LCD_ON | LCDC_BG_ON | LCDC_OBJ_ON | LCDC_TILE_DATA_LO)
|
|
202
|
+
#define LCDC_PLAY (LCDC_TITLE | LCDC_WINDOW_ON | LCDC_WINDOW_MAP_HI)
|
|
203
|
+
|
|
204
|
+
#define VRAM ((volatile uint8_t *)0x9800) /* BG map $9800 base */
|
|
205
|
+
#define WIN_OFF 0x400 /* window map $9C00 = $9800 + $400 */
|
|
206
|
+
|
|
207
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
208
|
+
* BATTERY SRAM — persistent hi-score. MBC1 cart RAM is 8KB at $A000-$BFFF,
|
|
209
|
+
* but it boots DISABLED and writes to a disabled bank are silently
|
|
210
|
+
* discarded (reads float). The gate is the MBC's RAM-enable register: any
|
|
211
|
+
* WRITE to ROM space $0000-$1FFF with $0A in the low nibble enables the RAM;
|
|
212
|
+
* writing $00 disables it again. (Writing "into ROM" feels wrong the first
|
|
213
|
+
* time — ROM-area writes never touch ROM, they talk to the mapper chip.)
|
|
214
|
+
* Leaving RAM enabled all the time "works" in emulators but on real hardware
|
|
215
|
+
* risks corruption at power-off — battery carts since forever do
|
|
216
|
+
* enable → touch → disable, so we do too.
|
|
217
|
+
*
|
|
218
|
+
* First boot is GARBAGE, not zeros: battery RAM holds whatever the silicon
|
|
219
|
+
* woke up with. The magic 'H','S' + checksum is how the load path tells "my
|
|
220
|
+
* save" from "factory noise" — without it a fresh cart shows a junk hi-score.
|
|
221
|
+
*
|
|
222
|
+
* Save block at $A000: 'H' 'S' lo hi (lo^hi^$A5)
|
|
223
|
+
*
|
|
224
|
+
* Requires: gb_crt0.s declaring $0147=$03 (MBC1+RAM+BATTERY) + $0149=$02
|
|
225
|
+
* (8KB) — those header bytes are how the emulator knows to allocate and
|
|
226
|
+
* persist SAVE_RAM. Verify headlessly: play, game over, then
|
|
227
|
+
* memory({op:'read', region:'save_ram'}) shows the block, and the hi-score
|
|
228
|
+
* survives host.hardReset(). */
|
|
229
|
+
#define MBC_RAM_ENABLE (*(volatile uint8_t *)0x0000)
|
|
230
|
+
#define SRAM ((volatile uint8_t *)0xA000)
|
|
231
|
+
|
|
232
|
+
static uint16_t hiscore_load(void) {
|
|
233
|
+
uint16_t v = 0;
|
|
234
|
+
MBC_RAM_ENABLE = 0x0A; /* unlock cart RAM */
|
|
235
|
+
if (SRAM[0] == 'H' && SRAM[1] == 'S' &&
|
|
236
|
+
SRAM[4] == (uint8_t)(SRAM[2] ^ SRAM[3] ^ 0xA5)) {
|
|
237
|
+
v = (uint16_t)(SRAM[2] | ((uint16_t)SRAM[3] << 8));
|
|
238
|
+
}
|
|
239
|
+
MBC_RAM_ENABLE = 0x00; /* re-lock (battery hygiene) */
|
|
240
|
+
return v;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
static void hiscore_save(uint16_t v) {
|
|
244
|
+
uint8_t lo = (uint8_t)(v & 0xFF), hi = (uint8_t)(v >> 8);
|
|
245
|
+
MBC_RAM_ENABLE = 0x0A;
|
|
246
|
+
SRAM[0] = 'H'; SRAM[1] = 'S';
|
|
247
|
+
SRAM[2] = lo; SRAM[3] = hi;
|
|
248
|
+
SRAM[4] = (uint8_t)(lo ^ hi ^ 0xA5);
|
|
249
|
+
MBC_RAM_ENABLE = 0x00;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
253
|
+
* The level — a 32-column map; world x = (screen x + scroll_x) mod 256.
|
|
254
|
+
* ground_row[c] — BG-map row of the ground's grass top, 0xFF = pit.
|
|
255
|
+
* plat_row[c] — row of a one-way floating platform, 0 = none.
|
|
256
|
+
* Rows are BG-map rows (y = row*8). The playfield is rows 0..15 (128 px,
|
|
257
|
+
* everything below is under the window HUD). Pits are 4 columns wide on
|
|
258
|
+
* purpose: at this gravity a 2 px/frame run skims anything narrower (the
|
|
259
|
+
* landing probe's +4 px catch window forgives small sink — see land_top). */
|
|
260
|
+
#define NO_GROUND 0xFF
|
|
261
|
+
#define GROUND 13 /* grass-top row, y = 104 */
|
|
262
|
+
static const uint8_t ground_row[32] = {
|
|
263
|
+
GROUND, GROUND, GROUND, GROUND, GROUND, GROUND, GROUND, GROUND, /* runway */
|
|
264
|
+
NO_GROUND, NO_GROUND, NO_GROUND, NO_GROUND, /* pit 1 */
|
|
265
|
+
GROUND, GROUND, GROUND, GROUND, GROUND, GROUND, GROUND,
|
|
266
|
+
NO_GROUND, NO_GROUND, NO_GROUND, NO_GROUND, /* pit 2 */
|
|
267
|
+
GROUND, GROUND, GROUND, GROUND, GROUND, GROUND, GROUND, GROUND, GROUND,
|
|
66
268
|
};
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
269
|
+
static const uint8_t plat_row[32] = {
|
|
270
|
+
0, 0, 0, 0, 10, 10, 10, 0, /* slab on the runway */
|
|
271
|
+
0, 9, 9, 0, 0, 0, 10, 10, /* stepping stone over pit 1 */
|
|
272
|
+
10, 0, 0, 0, 9, 9, 0, 0, /* stone over pit 2 */
|
|
273
|
+
0, 0, 10, 10, 10, 0, 0, 0, /* slab before the loop seam */
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
/* ── GAME LOGIC (clay) — physics + tuning (Q4.4 fixed point) ── */
|
|
277
|
+
#define GRAVITY_Q44 2 /* +1/8 px per frame per frame */
|
|
278
|
+
#define JUMP_VEL_Q44 (-52) /* launch vy → ~42 px apex (~5 tile rows) */
|
|
279
|
+
#define MAX_VY_Q44 80 /* terminal velocity, 5 px/frame — MUST stay *
|
|
280
|
+
* under 6: the landing probe's 6-px window *
|
|
281
|
+
* can't catch a faster fall (tunnelling) */
|
|
282
|
+
#define MOVE_SPEED 2 /* px/frame walk + scroll speed */
|
|
283
|
+
#define SCROLL_WALL 72 /* px: past this the world scrolls, not you */
|
|
284
|
+
#define GROUND_TOP 104 /* GROUND row * 8 */
|
|
285
|
+
#define SPIKE_Y 96 /* spikes stand on the ground */
|
|
286
|
+
#define NUM_COINS 3
|
|
287
|
+
#define NUM_SPIKES 2
|
|
288
|
+
#define START_LIVES 3
|
|
289
|
+
|
|
290
|
+
static uint8_t px; /* player screen x */
|
|
291
|
+
static uint16_t py_q44; /* player y, Q4.4 fixed point — gravity
|
|
292
|
+
* adds <1 px/frame near the jump apex,
|
|
293
|
+
* so we need sub-pixel precision */
|
|
294
|
+
static int8_t vy_q44;
|
|
295
|
+
static uint8_t on_ground;
|
|
296
|
+
static uint8_t scroll_x; /* level scroll — uint8 wraps at 256 = *
|
|
297
|
+
* exactly one level loop (seamless) */
|
|
298
|
+
static uint8_t dist_sub; /* sub-counter: 64 px scrolled = +1 pt */
|
|
299
|
+
static uint8_t coin_x[NUM_COINS], coin_y[NUM_COINS];
|
|
300
|
+
static uint8_t spike_x[NUM_SPIKES], spike_active[NUM_SPIKES];
|
|
301
|
+
static uint8_t lives;
|
|
302
|
+
static uint16_t score;
|
|
303
|
+
static uint16_t hiscore; /* live HUD readout: max(score, record) */
|
|
304
|
+
static uint16_t record; /* what the battery SRAM actually holds */
|
|
305
|
+
static uint8_t respawn_pause; /* freeze + blink frames after a death */
|
|
306
|
+
static uint8_t prev_pad;
|
|
307
|
+
static uint8_t hud_dirty; /* queue VRAM writes; vblank commits them */
|
|
308
|
+
static uint8_t msg_stage; /* game-over text: 2 = line 1 pending, 1 = line 2 */
|
|
309
|
+
static uint8_t msg_col; /* BG map col for GAME OVER (scroll-aware) */
|
|
310
|
+
|
|
311
|
+
/* Game states — the shell every example shares: title → play → game over.
|
|
312
|
+
* (Handheld adaptation: title is press-start; consoles add a 1P/2P pick.) */
|
|
313
|
+
#define ST_TITLE 0
|
|
314
|
+
#define ST_PLAY 1
|
|
315
|
+
#define ST_OVER 2
|
|
316
|
+
static uint8_t state;
|
|
317
|
+
|
|
318
|
+
/* ── GAME LOGIC (clay) — Galois LFSR (taps $B8), period 255 ── */
|
|
319
|
+
static uint8_t rng_state = 0xA5;
|
|
320
|
+
static uint8_t rand8(void) {
|
|
321
|
+
uint8_t lsb = (uint8_t)(rng_state & 1);
|
|
322
|
+
rng_state >>= 1;
|
|
323
|
+
if (lsb) rng_state ^= 0xB8;
|
|
324
|
+
return rng_state;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
static uint8_t dist8(uint8_t a, uint8_t b) {
|
|
328
|
+
return (a > b) ? (uint8_t)(a - b) : (uint8_t)(b - a);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
332
|
+
* CGB palette RAM — the BCPS/BCPD (BG) and OCPS/OCPD (OBJ) port pairs.
|
|
333
|
+
* requires: a .gbc build (CGB flag $0143 set — the build pipeline does it);
|
|
334
|
+
* on a DMG build these registers are dead and you get 4-shade green.
|
|
335
|
+
*
|
|
336
|
+
* Palette RAM is NOT memory-mapped: it's 64 bytes (8 palettes × 4 colours ×
|
|
337
|
+
* 2 bytes, little-endian 15-bit BGR) behind an index/data port pair.
|
|
338
|
+
* BCPS = 0x80 | index — set write index; bit 7 = AUTO-INCREMENT, so a
|
|
339
|
+
* burst of BCPD writes walks the whole 64 bytes.
|
|
340
|
+
* BCPD = low byte; BCPD = high byte; ... 32 times = all 8 palettes.
|
|
341
|
+
*
|
|
342
|
+
* TIMING FOOTGUN: palette RAM belongs to the PPU. Writes during active
|
|
343
|
+
* display (mode 3) are IGNORED on real hardware — same constraint as VRAM.
|
|
344
|
+
* Load palettes with the LCD OFF (boot / transitions, as here) or inside
|
|
345
|
+
* vblank. A palette "fade" = a few BCPD writes per vblank, never a mid-frame
|
|
346
|
+
* burst. */
|
|
347
|
+
static void load_bg_palettes(void) {
|
|
348
|
+
uint8_t p, i;
|
|
349
|
+
BCPS = 0x80; /* index 0, auto-increment on */
|
|
350
|
+
for (p = 0; p < 8; p++)
|
|
351
|
+
for (i = 0; i < 4; i++) {
|
|
352
|
+
BCPD = (uint8_t)(bg_palettes[p][i] & 0xFF);
|
|
353
|
+
BCPD = (uint8_t)((bg_palettes[p][i] >> 8) & 0xFF);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
static void load_obj_palettes(void) {
|
|
358
|
+
uint8_t p, i;
|
|
359
|
+
OCPS = 0x80;
|
|
360
|
+
for (p = 0; p < 8; p++)
|
|
361
|
+
for (i = 0; i < 4; i++) {
|
|
362
|
+
OCPD = (uint8_t)(obj_palettes[p][i] & 0xFF);
|
|
363
|
+
OCPD = (uint8_t)((obj_palettes[p][i] >> 8) & 0xFF);
|
|
75
364
|
}
|
|
76
|
-
return 0;
|
|
77
365
|
}
|
|
78
366
|
|
|
367
|
+
/* ── GAME LOGIC (clay) — VRAM upload + text helpers ──────────────────────────
|
|
368
|
+
* All of these write VRAM, so they run with the LCD OFF (boot/repaints) or
|
|
369
|
+
* inside vblank (the HUD digit commit). memcpy_vram walks a pointer
|
|
370
|
+
* (*dst++ = v) — never index dst[i] through a VRAM pointer (SDCC's sm83 port
|
|
371
|
+
* miscompiles indexed stores through VRAM-pointing pointers). */
|
|
79
372
|
static void upload_tile(uint8_t slot, const uint8_t *src) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
373
|
+
memcpy_vram((uint8_t *)(0x8000 + (uint16_t)slot * 16), src, 16);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
static void upload_font(void) {
|
|
377
|
+
uint8_t g;
|
|
378
|
+
/* font.h glyphs are already 2bpp (16 bytes each) — straight copy. */
|
|
379
|
+
for (g = 0; g < FONT_GLYPHS; g++)
|
|
380
|
+
memcpy_vram((uint8_t *)(0x8000 + (uint16_t)(FONT_BASE + g) * 16),
|
|
381
|
+
&font_data[g * 16], 16);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
static uint8_t char_tile(char ch) {
|
|
385
|
+
if (ch >= '0' && ch <= '9') return (uint8_t)(FONT_BASE + (ch - '0'));
|
|
386
|
+
if (ch >= 'A' && ch <= 'Z') return (uint8_t)(FONT_BASE + 10 + (ch - 'A'));
|
|
387
|
+
return T_BLANK; /* space / unknown → blank */
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/* Pre-convert a string to tile indices at full-frame time, so the vblank
|
|
391
|
+
* commit (commit_bg_text) is a dumb byte copy — see game_over(). */
|
|
392
|
+
static uint8_t msg_q[20]; /* 9 "GAME OVER" + 11 "PRESS START" */
|
|
393
|
+
static void stage_text(const char *s, uint8_t *out) {
|
|
394
|
+
while (*s) *out++ = char_tile(*s++);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
398
|
+
* Per-tile color — the VRAM bank-1 attribute map (VBK register).
|
|
399
|
+
* requires: CGB mode (see the palette idiom above); writes in a VRAM-safe
|
|
400
|
+
* window (LCD off, or a bounded vblank batch).
|
|
401
|
+
*
|
|
402
|
+
* VRAM is TWO 8KB banks behind the same $8000-$9FFF window; VBK ($FF4F)
|
|
403
|
+
* selects which one the CPU sees. Bank 0 holds what the DMG had: tile pixels
|
|
404
|
+
* + the tile-index maps. Bank 1 at the SAME map address holds one ATTRIBUTE
|
|
405
|
+
* byte per cell:
|
|
406
|
+
* bits 0-2 palette 0-7 ← this game's whole color system
|
|
407
|
+
* bit 3 tile VRAM bank
|
|
408
|
+
* bit 5/6 H/V flip
|
|
409
|
+
* bit 7 BG-over-OBJ priority
|
|
410
|
+
* So coloring a cell is a write PAIR: tile index with VBK=0, attribute with
|
|
411
|
+
* VBK=1, at the SAME offset.
|
|
412
|
+
*
|
|
413
|
+
* FOOTGUN: VBK is global state. Forget to restore VBK=0 and every later
|
|
414
|
+
* "tile" write lands in the attribute map — the screen turns into garbage
|
|
415
|
+
* colors while the tile data you wrote is simply gone. Always end VBK=0
|
|
416
|
+
* (every routine here does). */
|
|
417
|
+
static void set_cell(uint8_t mx, uint8_t my, uint8_t tile, uint8_t pal) {
|
|
418
|
+
uint16_t off = (uint16_t)my * 32 + mx;
|
|
419
|
+
VBK = 0;
|
|
420
|
+
VRAM[off] = tile;
|
|
421
|
+
VBK = 1;
|
|
422
|
+
VRAM[off] = pal;
|
|
423
|
+
VBK = 0;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/* same write-pair, into the WINDOW's map at $9C00 (window HUD idiom) */
|
|
427
|
+
static void set_wcell(uint8_t wx, uint8_t wy, uint8_t tile, uint8_t pal) {
|
|
428
|
+
uint16_t off = WIN_OFF + (uint16_t)wy * 32 + wx;
|
|
429
|
+
VBK = 0;
|
|
430
|
+
VRAM[off] = tile;
|
|
431
|
+
VBK = 1;
|
|
432
|
+
VRAM[off] = pal;
|
|
433
|
+
VBK = 0;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/* draw a NUL-terminated string into the BG map (palette PAL_HUD = readable). */
|
|
437
|
+
static void draw_text(uint8_t col, uint8_t row, const char *s) {
|
|
438
|
+
uint8_t i;
|
|
439
|
+
for (i = 0; s[i] != 0; i++)
|
|
440
|
+
set_cell((uint8_t)(col + i), row, char_tile(s[i]), PAL_HUD);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/* draw a NUL-terminated string into the WINDOW map. */
|
|
444
|
+
static void draw_wtext(uint8_t col, uint8_t row, const char *s) {
|
|
445
|
+
uint8_t i;
|
|
446
|
+
for (i = 0; s[i] != 0; i++)
|
|
447
|
+
set_wcell((uint8_t)(col + i), row, char_tile(s[i]), PAL_HUD);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/* Decimal digits WITHOUT divide/modulo (the sm83 has neither — SDCC's
|
|
451
|
+
* software % costs ~700 cycles a call). Repeated power-of-ten subtraction
|
|
452
|
+
* caps at 36 SUBs for any u16. Writes 5 tile slots into out5. */
|
|
453
|
+
static void u16_to_tiles(uint16_t v, uint8_t *out5) {
|
|
454
|
+
static const uint16_t pow10[4] = { 10000, 1000, 100, 10 };
|
|
455
|
+
uint8_t i, d;
|
|
456
|
+
for (i = 0; i < 4; i++) {
|
|
457
|
+
d = 0;
|
|
458
|
+
while (v >= pow10[i]) { v -= pow10[i]; ++d; }
|
|
459
|
+
*out5++ = (uint8_t)(FONT_BASE + d);
|
|
460
|
+
}
|
|
461
|
+
*out5 = (uint8_t)(FONT_BASE + (uint8_t)v);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/* ── GAME LOGIC (clay) — screen painters (LCD off = free VRAM access) ─────────
|
|
465
|
+
* Paint the level scene into BG rows 0..17 (the SCY=0 screen window; this
|
|
466
|
+
* game never scrolls vertically). For EACH cell we write the tile (bank 0)
|
|
467
|
+
* AND its palette attribute (bank 1) — that pairing is the whole CGB colour
|
|
468
|
+
* story. Clouds use a running divide-free pattern counter (the sm83 has no
|
|
469
|
+
* divide; treat every / and % in a loop as a red flag). */
|
|
470
|
+
static uint8_t tile_pal(uint8_t t) {
|
|
471
|
+
if (t == T_GRASS) return PAL_GRASS;
|
|
472
|
+
if (t == T_DIRT) return PAL_DIRT;
|
|
473
|
+
if (t == T_PLAT) return PAL_PLAT;
|
|
474
|
+
return PAL_SKY; /* sky + clouds */
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
static void paint_scene(uint8_t with_plats) {
|
|
478
|
+
uint8_t r, c, t, g;
|
|
479
|
+
uint8_t cl, clr = 0;
|
|
480
|
+
uint16_t off;
|
|
481
|
+
for (r = 0; r < 18; r++) {
|
|
482
|
+
cl = clr;
|
|
483
|
+
for (c = 0; c < 32; c++) {
|
|
484
|
+
g = ground_row[c];
|
|
485
|
+
t = T_SKY;
|
|
486
|
+
if (with_plats && r == plat_row[c]) t = T_PLAT;
|
|
487
|
+
else if (g != NO_GROUND) {
|
|
488
|
+
if (r == g) t = T_GRASS;
|
|
489
|
+
else if (r > g) t = T_DIRT;
|
|
490
|
+
} else if (r >= 16) {
|
|
491
|
+
t = T_DIRT; /* pit walls below the playfield */
|
|
492
|
+
}
|
|
493
|
+
if (t == T_SKY && r >= 2 && r <= 6 && cl == 0) t = T_CLOUD;
|
|
494
|
+
off = (uint16_t)r * 32 + c;
|
|
495
|
+
VBK = 0; VRAM[off] = t;
|
|
496
|
+
VBK = 1; VRAM[off] = tile_pal(t);
|
|
497
|
+
VBK = 0;
|
|
498
|
+
cl += 5; if (cl >= 13) cl -= 13;
|
|
109
499
|
}
|
|
500
|
+
clr += 7; if (clr >= 13) clr -= 13;
|
|
501
|
+
}
|
|
110
502
|
}
|
|
111
503
|
|
|
112
|
-
void
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
uint8_t
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
504
|
+
static void paint_title(void) {
|
|
505
|
+
paint_scene(0); /* plain scene — text owns the sky */
|
|
506
|
+
draw_text((uint8_t)((20 - (sizeof(GAME_TITLE) - 1)) / 2), 3, GAME_TITLE);
|
|
507
|
+
draw_text(4, 6, "PRESS START");
|
|
508
|
+
draw_text(6, 8, "HI");
|
|
509
|
+
{
|
|
510
|
+
uint8_t d[5], i;
|
|
511
|
+
u16_to_tiles(hiscore, d);
|
|
512
|
+
for (i = 0; i < 5; i++) set_cell((uint8_t)(9 + i), 8, d[i], PAL_HUD);
|
|
513
|
+
}
|
|
514
|
+
draw_text(6, 11, "1P ONLY"); /* see header: no link 2P */
|
|
515
|
+
SCX = 0; SCY = 0;
|
|
516
|
+
scroll_x = 0;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/* HUD strip = window rows 0-1: a solid divider bar, then the text row.
|
|
520
|
+
* Columns 0-19 are the visible 20 (WX=7 pins map col 0 to screen x 0). */
|
|
521
|
+
static void paint_hud(void) {
|
|
522
|
+
uint8_t c, d[5], i;
|
|
523
|
+
for (c = 0; c < 20; c++) set_wcell(c, 0, T_HUDBAR, PAL_HUD);
|
|
524
|
+
for (c = 0; c < 20; c++) set_wcell(c, 1, T_BLANK, PAL_HUD);
|
|
525
|
+
draw_wtext(0, 1, "SC");
|
|
526
|
+
u16_to_tiles(score, d);
|
|
527
|
+
for (i = 0; i < 5; i++) set_wcell((uint8_t)(3 + i), 1, d[i], PAL_HUD);
|
|
528
|
+
draw_wtext(9, 1, "HI");
|
|
529
|
+
u16_to_tiles(hiscore, d);
|
|
530
|
+
for (i = 0; i < 5; i++) set_wcell((uint8_t)(12 + i), 1, d[i], PAL_HUD);
|
|
531
|
+
draw_wtext(18, 1, "L");
|
|
532
|
+
set_wcell(19, 1, (uint8_t)(FONT_BASE + lives), PAL_HUD);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
536
|
+
* LCD-off repaints. Bulk VRAM rewrites (full title/level repaints) happen
|
|
537
|
+
* with the LCD OFF — free access, no per-byte timing worries. The rule:
|
|
538
|
+
* only flip LCDC bit 7 to 0 DURING VBLANK. Killing the LCD mid-scanline is
|
|
539
|
+
* the classic "damages real DMG hardware" move; emulators shrug, real units
|
|
540
|
+
* can be permanently marked. wait_vblank() first, always.
|
|
541
|
+
* Requires: enable_vblank_irq() already called (wait_vblank HALT path);
|
|
542
|
+
* lcd_off-safe runtime (gb_runtime's wait_vblank bails if LCD is off). */
|
|
543
|
+
static void repaint_with_lcd_off(uint8_t to_title) {
|
|
544
|
+
msg_stage = 0; /* a queued game-over line must not land on
|
|
545
|
+
* the freshly painted screen a frame later */
|
|
546
|
+
wait_vblank(); /* never cut the LCD outside vblank */
|
|
547
|
+
LCDC = 0;
|
|
548
|
+
if (to_title) {
|
|
549
|
+
paint_title();
|
|
550
|
+
oam_clear(); /* hide every sprite slot before re-enable */
|
|
551
|
+
LCDC = LCDC_TITLE; /* window OFF on the title */
|
|
552
|
+
} else {
|
|
553
|
+
paint_scene(1);
|
|
554
|
+
paint_hud();
|
|
555
|
+
LCDC = LCDC_PLAY; /* window ON below WY — the HUD appears */
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/* ── GAME LOGIC (clay) — sound: frame-ticked tune + jump/coin/death SFX ───────
|
|
560
|
+
* Channel plan keeps SFX from cutting the music: ch2 = music (one
|
|
561
|
+
* sound_play_tone trigger per note, the APU sustains it), ch1 = jump and
|
|
562
|
+
* coin blips, ch4 = noise for deaths. music_tick() runs once per frame from
|
|
563
|
+
* the main loop; the APU needs no other upkeep. Periods are the 11-bit GB
|
|
564
|
+
* frequency code: 2048 - (131072 / Hz). 0 = rest. SELECT toggles it. */
|
|
565
|
+
static const uint16_t tune[16] = {
|
|
566
|
+
1714, 0, 1750, 0, 1783, 0, 1798, 0, /* G4 A4 B4 C5 */
|
|
567
|
+
1825, 0, 1798, 0, 1783, 0, 1750, 0, /* D5 C5 B4 A4 */
|
|
568
|
+
};
|
|
569
|
+
static uint8_t music_on = 1, music_pos, music_timer;
|
|
570
|
+
static void music_tick(void) {
|
|
571
|
+
uint16_t n;
|
|
572
|
+
if (!music_on) return;
|
|
573
|
+
if (++music_timer < 14) return;
|
|
574
|
+
music_timer = 0;
|
|
575
|
+
n = tune[music_pos];
|
|
576
|
+
music_pos = (uint8_t)((music_pos + 1) & 15);
|
|
577
|
+
if (n) sound_play_tone(2, n, 12);
|
|
578
|
+
}
|
|
579
|
+
static void music_toggle(void) {
|
|
580
|
+
music_on = (uint8_t)(!music_on);
|
|
581
|
+
if (!music_on) { NR21 = 0x00; NR22 = 0x00; NR24 = 0x80; } /* silence ch2 */
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/* ── GAME LOGIC (clay) — coins + spikes (sprite objects in the world) ─────────
|
|
585
|
+
* Both live in SCREEN coords and drift left with the scroll delta (world-
|
|
586
|
+
* anchored while visible). Coins respawn at the right edge at a random
|
|
587
|
+
* height; spikes only spawn when the level column entering at the right edge
|
|
588
|
+
* has ground under it (never floating over a pit). */
|
|
589
|
+
static const uint8_t coin_heights[4] = { 80, 64, 48, 72 };
|
|
590
|
+
static void respawn_coin(uint8_t i) {
|
|
591
|
+
coin_x[i] = (uint8_t)(144 + (rand8() & 15)); /* enter at the right */
|
|
592
|
+
coin_y[i] = coin_heights[rand8() & 3];
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
static void try_spawn_spike(uint8_t i) {
|
|
596
|
+
uint8_t c = (uint8_t)((uint8_t)(scroll_x + 160) >> 3);
|
|
597
|
+
if (ground_row[c] == NO_GROUND) return;
|
|
598
|
+
if (rand8() > 4) return; /* ~2% per frame */
|
|
599
|
+
spike_x[i] = 152;
|
|
600
|
+
spike_active[i] = 1;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/* ── GAME LOGIC (clay) — landing probe against the column map ──────────────────
|
|
604
|
+
* One-way platforms, classic style: only catch the player while FALLING
|
|
605
|
+
* through a narrow window at the surface. The window is 6 px tall — top-1
|
|
606
|
+
* (the standing snap parks feet at top, and gravity's sub-pixel trickle
|
|
607
|
+
* doesn't move the integer Y every frame; without the -1 slack the player
|
|
608
|
+
* "stands" with on_ground=0 most frames, so jumps only register on lucky
|
|
609
|
+
* frames and the idle/jump sprite flickers) through top+4 (so a 5 px/frame
|
|
610
|
+
* terminal-velocity fall can't step over it). */
|
|
611
|
+
static uint8_t land_top(uint8_t c, uint8_t feet) {
|
|
612
|
+
uint8_t r, top;
|
|
613
|
+
r = plat_row[c];
|
|
614
|
+
if (r) {
|
|
615
|
+
top = (uint8_t)(r << 3);
|
|
616
|
+
if ((uint8_t)(feet + 1) >= top && feet <= (uint8_t)(top + 4)) return top;
|
|
617
|
+
}
|
|
618
|
+
r = ground_row[c];
|
|
619
|
+
if (r != NO_GROUND) {
|
|
620
|
+
top = (uint8_t)(r << 3);
|
|
621
|
+
if ((uint8_t)(feet + 1) >= top && feet <= (uint8_t)(top + 4)) return top;
|
|
622
|
+
}
|
|
623
|
+
return 0;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/* ── GAME LOGIC (clay) — state transitions ── */
|
|
627
|
+
static void begin_life(void) {
|
|
628
|
+
uint8_t i;
|
|
629
|
+
px = 24;
|
|
630
|
+
py_q44 = (uint16_t)(GROUND_TOP - 8) << 4;
|
|
631
|
+
vy_q44 = 0;
|
|
632
|
+
on_ground = 1;
|
|
633
|
+
scroll_x = 0;
|
|
634
|
+
dist_sub = 0;
|
|
635
|
+
coin_x[0] = 88; coin_y[0] = 80;
|
|
636
|
+
coin_x[1] = 120; coin_y[1] = 64;
|
|
637
|
+
coin_x[2] = 144; coin_y[2] = 48;
|
|
638
|
+
for (i = 0; i < NUM_SPIKES; i++) spike_active[i] = 0;
|
|
639
|
+
respawn_pause = 48; /* ready breather — player blinks */
|
|
640
|
+
prev_pad = 0xFF; /* swallow held buttons across the reset */
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
static void start_game(void) {
|
|
644
|
+
lives = START_LIVES;
|
|
645
|
+
score = 0;
|
|
646
|
+
hud_dirty = 1; /* restage hud digits — a stale game-over stage
|
|
647
|
+
* queued before the repaint would overwrite the
|
|
648
|
+
* fresh zeros next vblank otherwise */
|
|
649
|
+
begin_life();
|
|
650
|
+
state = ST_PLAY;
|
|
651
|
+
repaint_with_lcd_off(0);
|
|
652
|
+
sound_play_tone(1, 1798, 8); /* start jingle (C5) */
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
static void game_over(void) {
|
|
656
|
+
/* Compare against the SAVED record, not the live `hiscore` readout — the
|
|
657
|
+
* scoring path already raised `hiscore` to track the run, so testing
|
|
658
|
+
* `score > hiscore` here would never fire. */
|
|
659
|
+
if (score > record) {
|
|
660
|
+
record = score;
|
|
661
|
+
hiscore_save(record); /* battery write — survives power-off */
|
|
662
|
+
}
|
|
663
|
+
state = ST_OVER;
|
|
664
|
+
/* The BG has scrolled: map col 0 is no longer screen col 0. Anchor the
|
|
665
|
+
* text relative to the CURRENT scroll so it lands mid-screen. Pre-convert
|
|
666
|
+
* the strings to tile indices HERE (full-frame time) into msg_q — the
|
|
667
|
+
* vblank commit is then a DUMB byte copy. char_tile's per-char compare
|
|
668
|
+
* chain is exactly the work that blows the ~1140-cycle vblank budget; doing
|
|
669
|
+
* it inside the commit dropped the middle of the 11-char PRESS START line
|
|
670
|
+
* (verified). Stage out here, copy in there. */
|
|
671
|
+
msg_col = (uint8_t)(((scroll_x >> 3) + 5) & 31);
|
|
672
|
+
stage_text("GAME OVER", msg_q);
|
|
673
|
+
stage_text("PRESS START", msg_q + 9);
|
|
674
|
+
msg_stage = 2;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
static void kill_player(void) {
|
|
678
|
+
sound_play_noise(20);
|
|
679
|
+
if (lives) --lives;
|
|
680
|
+
hud_dirty = 1;
|
|
681
|
+
if (lives == 0) { game_over(); return; }
|
|
682
|
+
begin_life(); /* back to the runway, scroll rewinds */
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/* ── GAME LOGIC (clay) — per-state update (runs OUTSIDE vblank) ── */
|
|
686
|
+
static void update_play(uint8_t pad) {
|
|
687
|
+
uint8_t i, delta, y8, feet, c0, c1, top;
|
|
688
|
+
|
|
689
|
+
delta = 0;
|
|
690
|
+
if (pad & PAD_RIGHT) {
|
|
691
|
+
/* One-way camera: walk until the scroll wall, then the world moves. */
|
|
692
|
+
if (px < SCROLL_WALL) px += MOVE_SPEED;
|
|
693
|
+
else { scroll_x += MOVE_SPEED; delta = MOVE_SPEED; }
|
|
694
|
+
}
|
|
695
|
+
if ((pad & PAD_LEFT) && px > 8) px -= MOVE_SPEED;
|
|
696
|
+
if ((pad & PAD_A) && !(prev_pad & PAD_A) && on_ground) {
|
|
697
|
+
vy_q44 = JUMP_VEL_Q44;
|
|
698
|
+
on_ground = 0;
|
|
699
|
+
sound_play_tone(1, 1849, 6); /* jump whoop (E5) */
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/* World objects drift left as the level scrolls (world-anchored). */
|
|
703
|
+
if (delta) {
|
|
704
|
+
dist_sub += delta;
|
|
705
|
+
if (dist_sub >= 64) { /* distance pay */
|
|
706
|
+
dist_sub -= 64;
|
|
707
|
+
if (score <= 65525u) ++score;
|
|
708
|
+
if (score > hiscore) hiscore = score; /* live HI readout; SRAM
|
|
709
|
+
* write waits for game over */
|
|
710
|
+
hud_dirty = 1;
|
|
142
711
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
712
|
+
for (i = 0; i < NUM_COINS; i++) {
|
|
713
|
+
if (coin_x[i] < 8 + delta) respawn_coin(i);
|
|
714
|
+
else coin_x[i] -= delta;
|
|
715
|
+
}
|
|
716
|
+
for (i = 0; i < NUM_SPIKES; i++) {
|
|
717
|
+
if (!spike_active[i]) continue;
|
|
718
|
+
if (spike_x[i] < 8 + delta) spike_active[i] = 0;
|
|
719
|
+
else spike_x[i] -= delta;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
for (i = 0; i < NUM_SPIKES; i++)
|
|
723
|
+
if (!spike_active[i]) try_spawn_spike(i);
|
|
724
|
+
|
|
725
|
+
/* Physics: gravity + sub-pixel Y. */
|
|
726
|
+
if (vy_q44 < MAX_VY_Q44) vy_q44 += GRAVITY_Q44;
|
|
727
|
+
py_q44 += vy_q44;
|
|
728
|
+
y8 = (uint8_t)(py_q44 >> 4);
|
|
729
|
+
|
|
730
|
+
/* Fell into a pit — die at PLAY_H-8, the frame BEFORE the sprite would
|
|
731
|
+
* overlap the window HUD (footgun 2 above: OBJs draw over the window). */
|
|
732
|
+
if (y8 >= PLAY_H - 8) {
|
|
733
|
+
kill_player();
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/* Landing — probe the two level columns under the player's feet.
|
|
738
|
+
* uint8 px+scroll_x wraps at 256 exactly like the level does. */
|
|
739
|
+
if (vy_q44 >= 0) {
|
|
740
|
+
feet = (uint8_t)(y8 + 8);
|
|
741
|
+
c0 = (uint8_t)((uint8_t)(px + scroll_x) >> 3);
|
|
742
|
+
c1 = (uint8_t)((uint8_t)(px + scroll_x + 7) >> 3);
|
|
743
|
+
top = land_top(c0, feet);
|
|
744
|
+
if (top == 0) top = land_top(c1, feet);
|
|
745
|
+
if (top) {
|
|
746
|
+
py_q44 = (uint16_t)(top - 8) << 4;
|
|
747
|
+
vy_q44 = 0;
|
|
748
|
+
if (!on_ground) sound_play_tone(1, 1602, 3); /* landing thud */
|
|
749
|
+
on_ground = 1;
|
|
750
|
+
} else {
|
|
751
|
+
on_ground = 0; /* walked off an edge */
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/* Coins (collect) + spikes (death). */
|
|
756
|
+
for (i = 0; i < NUM_COINS; i++) {
|
|
757
|
+
if (dist8(coin_x[i], px) < 8 && dist8(coin_y[i], y8) < 8) {
|
|
758
|
+
if (score <= 65525u) score += 10;
|
|
759
|
+
if (score > hiscore) hiscore = score;
|
|
760
|
+
sound_play_tone(1, 1923, 5); /* coin ping (C6) */
|
|
761
|
+
hud_dirty = 1;
|
|
762
|
+
respawn_coin(i);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
for (i = 0; i < NUM_SPIKES; i++) {
|
|
766
|
+
if (!spike_active[i]) continue;
|
|
767
|
+
if (dist8(spike_x[i], px) < 7 && dist8(SPIKE_Y, y8) < 7) {
|
|
768
|
+
kill_player();
|
|
769
|
+
return;
|
|
147
770
|
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/* ── GAME LOGIC (clay) — stage the shadow OAM for THIS frame ──────────────────
|
|
775
|
+
* Pure WRAM writes (shadow_oam at $C100) — safe any time; only the DMA flush
|
|
776
|
+
* is vblank-sensitive. OAM coords are hardware coords: +16 on Y, +8 on X.
|
|
777
|
+
* A sprite's CGB palette = OAM attr bits 0-2 — that's the whole "color this
|
|
778
|
+
* sprite" story. Slot plan (40 hardware slots, we use 6): 0 = player,
|
|
779
|
+
* 1-3 coins, 4-5 spikes — well under the 10-OBJ/line hardware drop. */
|
|
780
|
+
static void stage_sprites(void) {
|
|
781
|
+
uint8_t i, y8;
|
|
782
|
+
oam_clear();
|
|
783
|
+
if (state == ST_TITLE) {
|
|
784
|
+
/* Guaranteed-visible sprite from the first title frame — proof the OAM
|
|
785
|
+
* pipeline (shadow → HRAM DMA stub → OAM) is alive before any gameplay
|
|
786
|
+
* complicates the picture. */
|
|
787
|
+
oam_set(0, 96 + 16, 76 + 8, T_PLAYER, OPAL_PLAYER);
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
y8 = (uint8_t)(py_q44 >> 4);
|
|
791
|
+
if (respawn_pause == 0 || (respawn_pause & 4)) /* ready-blink */
|
|
792
|
+
oam_set(0, (uint8_t)(y8 + 16), (uint8_t)(px + 8),
|
|
793
|
+
on_ground ? T_PLAYER : T_JUMP, OPAL_PLAYER);
|
|
794
|
+
for (i = 0; i < NUM_COINS; i++)
|
|
795
|
+
oam_set((uint8_t)(1 + i), (uint8_t)(coin_y[i] + 16),
|
|
796
|
+
(uint8_t)(coin_x[i] + 8), T_COIN, OPAL_COIN);
|
|
797
|
+
for (i = 0; i < NUM_SPIKES; i++)
|
|
798
|
+
if (spike_active[i])
|
|
799
|
+
oam_set((uint8_t)(4 + i), SPIKE_Y + 16,
|
|
800
|
+
(uint8_t)(spike_x[i] + 8), T_SPIKE, OPAL_SPIKE);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
804
|
+
* Queued VRAM commits — and the bank-0-only HUD write. Two-phase update,
|
|
805
|
+
* mirroring the shadow-OAM discipline: game logic only sets hud_dirty /
|
|
806
|
+
* msg_stage. stage_hud() (full-frame time) does the digit math into hud_q;
|
|
807
|
+
* commit_vram() (vblank time) writes bytes — AT MOST ONE queued item/vblank.
|
|
808
|
+
*
|
|
809
|
+
* THE CGB TWIST (load-bearing): a naive set_wcell() per HUD cell toggles VBK
|
|
810
|
+
* twice + writes two banks PER cell — for 11 HUD cells that's ~33 VBK writes
|
|
811
|
+
* in one vblank, which OVERRUNS the ~1140-cycle window and silently drops the
|
|
812
|
+
* tail writes (the lives digit at col 19 vanished — verified). The fix: the
|
|
813
|
+
* window HUD cells' bank-1 ATTRIBUTE bytes are constant PAL_HUD (painted once
|
|
814
|
+
* by paint_hud at LCD-off and never changed), so the per-frame commit only
|
|
815
|
+
* needs to rewrite bank-0 TILE bytes. We set VBK=0 ONCE and pointer-walk the
|
|
816
|
+
* digit cells — a tight write that fits vblank with room to spare. (Pointer
|
|
817
|
+
* walk, not map[i] indexing — the SDCC VRAM footgun.) */
|
|
818
|
+
static uint8_t hud_q[11]; /* 5 score digits, 5 hi digits, lives tile */
|
|
819
|
+
static uint8_t hud_ready;
|
|
820
|
+
#define WIN_TILE ((volatile uint8_t *)0x9C00) /* window map, bank 0 */
|
|
821
|
+
|
|
822
|
+
static void stage_hud(void) {
|
|
823
|
+
if (!hud_dirty) return;
|
|
824
|
+
hud_dirty = 0;
|
|
825
|
+
u16_to_tiles(score, hud_q);
|
|
826
|
+
u16_to_tiles(hiscore, hud_q + 5);
|
|
827
|
+
hud_q[10] = (uint8_t)(FONT_BASE + lives);
|
|
828
|
+
hud_ready = 1;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/* Write a scroll-anchored, pre-staged BG-map line (msg_q tiles) as a single
|
|
832
|
+
* BANK-0 tile copy — a dumb byte walk, no char_tile work and no per-cell VBK
|
|
833
|
+
* toggling. We DELIBERATELY leave the cells' bank-1 attribute alone: the scene
|
|
834
|
+
* painted them PAL_SKY, whose colour-3 (the font ink value) is white — so the
|
|
835
|
+
* text reads white-on-sky with ZERO attribute writes. That halves the vblank
|
|
836
|
+
* cost: an 11-char line as tile+attr pairs overran mode 3 and dropped its
|
|
837
|
+
* middle (verified); tile-only fits with room to spare. col wraps at the
|
|
838
|
+
* 32-col map seam (the text is scroll-anchored, so it can straddle the wrap). */
|
|
839
|
+
static void commit_bg_text(uint8_t row, uint8_t col, const uint8_t *q, uint8_t len) {
|
|
840
|
+
volatile uint8_t *base = VRAM + (uint16_t)row * 32;
|
|
841
|
+
volatile uint8_t *p = base + col;
|
|
842
|
+
uint8_t n = (uint8_t)(32 - col);
|
|
843
|
+
VBK = 0;
|
|
844
|
+
if (n > len) n = len; /* run 1: up to the map seam */
|
|
845
|
+
len -= n;
|
|
846
|
+
while (n--) *p++ = *q++;
|
|
847
|
+
p = base; /* run 2: wrapped remainder */
|
|
848
|
+
while (len--) *p++ = *q++;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
static void commit_vram(void) {
|
|
852
|
+
uint8_t i;
|
|
853
|
+
if (hud_ready) { /* item 1: HUD digits (bank 0) */
|
|
854
|
+
hud_ready = 0;
|
|
855
|
+
VBK = 0; /* attributes already PAL_HUD */
|
|
856
|
+
for (i = 0; i < 5; i++) WIN_TILE[32 + 3 + i] = hud_q[i]; /* score */
|
|
857
|
+
for (i = 0; i < 5; i++) WIN_TILE[32 + 12 + i] = hud_q[5 + i]; /* hi */
|
|
858
|
+
WIN_TILE[32 + 19] = hud_q[10]; /* lives */
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
if (msg_stage == 2) { /* item 2: GAME OVER line */
|
|
862
|
+
msg_stage = 1;
|
|
863
|
+
commit_bg_text(5, msg_col, msg_q, 9);
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
if (msg_stage == 1) { /* item 3: PRESS START line */
|
|
867
|
+
msg_stage = 0;
|
|
868
|
+
commit_bg_text(7, (uint8_t)((msg_col + 31) & 31), msg_q + 9, 11);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
void main(void) {
|
|
873
|
+
uint8_t pad;
|
|
874
|
+
|
|
875
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
876
|
+
* Boot order. Three load-bearing calls, in this order:
|
|
877
|
+
* 1. lcd_init_default() — sane LCD state AND it installs the OAM-DMA stub
|
|
878
|
+
* into HRAM ($FF80). During OAM DMA the CPU can only fetch from HRAM;
|
|
879
|
+
* the broken alternative (spinning in ROM) fetches $FF = rst $38 and
|
|
880
|
+
* corrupts the stack — the classic "sprites never show / game dies
|
|
881
|
+
* after a while" GB death. Every oam_dma_flush() depends on this stub.
|
|
882
|
+
* 2. enable_vblank_irq() — flips wait_vblank() from LY-polling to
|
|
883
|
+
* HALT-until-vblank-IRQ. The polling fallback runs at ~1/30 speed on
|
|
884
|
+
* the WASM emulator; the HALT path is full speed everywhere.
|
|
885
|
+
* 3. LCD off (inside vblank) for the bulk VRAM uploads — tiles, font,
|
|
886
|
+
* palettes, first screen — then back on. Tile/palette/map uploads
|
|
887
|
+
* REQUIRE a VRAM-safe window; boot does them all at once, so LCD-off
|
|
888
|
+
* is the only sane choice here. */
|
|
889
|
+
lcd_init_default();
|
|
890
|
+
enable_vblank_irq();
|
|
891
|
+
sound_init();
|
|
148
892
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
py = np;
|
|
204
|
-
if (py > 144 << 4) { py = 0; vy = 0; }
|
|
205
|
-
done: ;
|
|
893
|
+
wait_vblank();
|
|
894
|
+
LCDC = 0; /* LCD off — free VRAM access from here */
|
|
895
|
+
|
|
896
|
+
upload_tile(T_BLANK, tile_blank);
|
|
897
|
+
upload_tile(T_PLAYER, tile_player);
|
|
898
|
+
upload_tile(T_JUMP, tile_player_jump);
|
|
899
|
+
upload_tile(T_COIN, tile_coin);
|
|
900
|
+
upload_tile(T_SPIKE, tile_spike);
|
|
901
|
+
upload_tile(T_SKY, tile_sky);
|
|
902
|
+
upload_tile(T_CLOUD, tile_cloud);
|
|
903
|
+
upload_tile(T_DIRT, tile_dirt);
|
|
904
|
+
upload_tile(T_GRASS, tile_grass);
|
|
905
|
+
upload_tile(T_PLAT, tile_plat);
|
|
906
|
+
upload_tile(T_HUDBAR, tile_hudbar);
|
|
907
|
+
upload_font();
|
|
908
|
+
|
|
909
|
+
load_bg_palettes(); /* the CGB BG palettes — sky/grass/dirt/... */
|
|
910
|
+
load_obj_palettes(); /* player / coin / spike OBJ palettes */
|
|
911
|
+
|
|
912
|
+
/* Window position — set once; LCDC bit 5 decides if it shows. */
|
|
913
|
+
WX = 7; /* the +7 quirk: 7 = screen left edge */
|
|
914
|
+
WY = PLAY_H; /* HUD owns lines 128-143 */
|
|
915
|
+
|
|
916
|
+
record = hiscore_load(); /* battery SRAM — 0 on first boot */
|
|
917
|
+
hiscore = record;
|
|
918
|
+
state = ST_TITLE;
|
|
919
|
+
paint_title();
|
|
920
|
+
oam_clear();
|
|
921
|
+
LCDC = LCDC_TITLE;
|
|
922
|
+
|
|
923
|
+
for (;;) {
|
|
924
|
+
/* ── full-frame work: input, game state, shadow-OAM staging ── */
|
|
925
|
+
pad = joypad_read();
|
|
926
|
+
|
|
927
|
+
/* SELECT toggles the background music, in any state. */
|
|
928
|
+
if ((pad & PAD_SELECT) && !(prev_pad & PAD_SELECT)) music_toggle();
|
|
929
|
+
|
|
930
|
+
if (state == ST_TITLE) {
|
|
931
|
+
if ((pad & (PAD_START | PAD_A)) && !(prev_pad & (PAD_START | PAD_A))) start_game();
|
|
932
|
+
prev_pad = pad;
|
|
933
|
+
} else if (state == ST_PLAY) {
|
|
934
|
+
if (respawn_pause) { /* ready-blink: freeze gameplay, stay honest */
|
|
935
|
+
--respawn_pause;
|
|
936
|
+
prev_pad = pad;
|
|
937
|
+
} else {
|
|
938
|
+
update_play(pad);
|
|
939
|
+
prev_pad = pad;
|
|
940
|
+
}
|
|
941
|
+
} else { /* ST_OVER — freeze the field; START/A returns to title */
|
|
942
|
+
if ((pad & (PAD_START | PAD_A)) && !(prev_pad & (PAD_START | PAD_A))) {
|
|
943
|
+
state = ST_TITLE;
|
|
944
|
+
repaint_with_lcd_off(1);
|
|
945
|
+
}
|
|
946
|
+
prev_pad = pad;
|
|
206
947
|
}
|
|
948
|
+
stage_sprites();
|
|
949
|
+
stage_hud(); /* digit math out here, not in vblank */
|
|
950
|
+
|
|
951
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
952
|
+
* The vblank slice. wait_vblank() wakes at the START of vblank
|
|
953
|
+
* (~1140 cycles of safe OAM/VRAM access). Order is everything:
|
|
954
|
+
* oam_dma_flush() FIRST — the DMA takes ~165 cycles and MUST finish
|
|
955
|
+
* inside vblank; pushing it later (after VRAM writes that grow over
|
|
956
|
+
* time) slides it into active display, where the PPU is reading OAM
|
|
957
|
+
* = one frame of torn/invisible sprites, intermittent and miserable
|
|
958
|
+
* to debug.
|
|
959
|
+
* commit_vram() second — the few queued HUD/map bytes (one item/frame).
|
|
960
|
+
* SCX last — scroll latches per-scanline, so writing it during vblank
|
|
961
|
+
* (before line 0 renders) moves the WHOLE next frame consistently;
|
|
962
|
+
* the window ignores it by design (the HUD idiom).
|
|
963
|
+
* Game logic above NEVER touches VRAM directly — it sets the dirty flags
|
|
964
|
+
* and shadow OAM, and this slice commits them. Keep that split. */
|
|
965
|
+
wait_vblank();
|
|
966
|
+
oam_dma_flush();
|
|
967
|
+
commit_vram();
|
|
968
|
+
SCX = scroll_x; /* title resets scroll_x to 0; over freezes it */
|
|
969
|
+
music_tick();
|
|
970
|
+
}
|
|
207
971
|
}
|