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,257 +1,780 @@
|
|
|
1
|
-
/* ── racing.c — Genesis
|
|
1
|
+
/* ── racing.c — Genesis top-down road racer (complete example game) ──────────
|
|
2
2
|
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* MIRAGE MILE — a COMPLETE, working game: title screen, 1P endless race with
|
|
4
|
+
* speed control, 2P simultaneous SPLIT-LANE VERSUS (both cars on screen at
|
|
5
|
+
* once — player 2 on CONTROLLER 2), a vertically-scrolling road done the
|
|
6
|
+
* Genesis way (full-plane hardware VSCROLL), streamed roadside scenery
|
|
7
|
+
* through the DMA queue, crash/lives rules, persistent best distance
|
|
8
|
+
* (cartridge SRAM), music + SFX — and a LIVE per-scanline HSCROLL_LINE
|
|
9
|
+
* heat-haze band shimmering across the asphalt, the deluxe scroll variant
|
|
10
|
+
* the platformer template only documents.
|
|
7
11
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
12
|
+
* THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
|
|
13
|
+
* very different one. The markers tell you what's what:
|
|
14
|
+
* HARDWARE IDIOM (load-bearing) — dodges a documented Genesis footgun;
|
|
15
|
+
* reshape your gameplay around it (see TROUBLESHOOTING before changing).
|
|
16
|
+
* GAME LOGIC (clay) — traffic patterns, speeds, tuning, art: reshape
|
|
17
|
+
* freely.
|
|
13
18
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
19
|
+
* What depends on what:
|
|
20
|
+
* genesis_sfx.{h,c} — PSG sound wrapper (tones + noise + a background
|
|
21
|
+
* melody loop). For full FM music, see the xgm2_demo template.
|
|
22
|
+
* rom_header.c (SGDK) — the Sega header at $100. Its 'RA' block at $1B0
|
|
23
|
+
* DECLARES the cartridge SRAM that best_load/save below depend on (see
|
|
24
|
+
* the SRAM idiom). The build assembles it automatically.
|
|
25
|
+
*
|
|
26
|
+
* THE DESIGN (read before reshaping):
|
|
27
|
+
* Scrolling — the road is PLANE A, scrolled down by decrementing one
|
|
28
|
+
* vertical-scroll value per frame. Compare the NES version of this
|
|
29
|
+
* game (examples/nes/templates/racing.c): there a nametable is 240 px
|
|
30
|
+
* tall, scroll_y 240-255 fetches attribute bytes as tiles (garbage
|
|
31
|
+
* rows), and every scroll change goes through a wrap helper. The
|
|
32
|
+
* Genesis plane is 256 px tall and the VDP masks the scroll value to
|
|
33
|
+
* the plane IN HARDWARE: `vs -= speed` on a plain u16 is the entire
|
|
34
|
+
* idiom (65536 is a multiple of 256, so overflow is seamless forever).
|
|
35
|
+
* Streamed scenery — rows re-entering at the top get restamped with
|
|
36
|
+
* fresh random roadside through the DMA queue, hidden under the
|
|
37
|
+
* 16-px WINDOW HUD (the same curtain trick the NES game plays with
|
|
38
|
+
* the overscan-cropped top band).
|
|
39
|
+
* Heat haze — HSCROLL_LINE mode: the VDP fetches one hscroll entry PER
|
|
40
|
+
* SCANLINE, so a 32-line band of the road ripples ±2 px in a moving
|
|
41
|
+
* wave while the rest of the screen holds still. 64 bytes/frame of
|
|
42
|
+
* vblank DMA. Sprites are NOT displaced — per-line hscroll bends
|
|
43
|
+
* planes only.
|
|
44
|
+
* HUD — the WINDOW plane: a hardware-fixed status bar that ignores all
|
|
45
|
+
* scrolling (no raster tricks needed — one register).
|
|
46
|
+
* 2P VERSUS — ONE VDP means ONE road scroll, so both players share one
|
|
47
|
+
* road at a fixed speed and only steer (the same constraint the NES
|
|
48
|
+
* version explains): solid center divider, P1 (blue, pad 1) owns the
|
|
49
|
+
* left two lanes, P2 (green, pad 2) the right two. Each starts with 3
|
|
50
|
+
* crashes; first to use them all LOSES.
|
|
51
|
+
* 1P RACE — all four lanes, A/UP accelerates, B/DOWN brakes (speed 1-4);
|
|
52
|
+
* 3 crashes end the run. Persistent stat: best DISTANCE (u16, one
|
|
53
|
+
* unit = 16 scrolled pixels ≈ one car length) via best_load/save.
|
|
54
|
+
*
|
|
55
|
+
* Frame budget (NTSC, 60 fps): 6 traffic × 2 cars of AABB, one 64-cell row
|
|
56
|
+
* restamp at most every other frame (128 B), the 32-entry haze table (64 B)
|
|
57
|
+
* and 8 SAT entries (64 B) queued for vblank — ~300 bytes of the ~7 KB
|
|
58
|
+
* H40 vblank DMA ceiling. The 68000 barely notices.
|
|
17
59
|
*/
|
|
18
60
|
|
|
19
61
|
#include <genesis.h>
|
|
20
62
|
#include "genesis_sfx.h"
|
|
21
63
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
#define
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
64
|
+
/* The title screen renders this — examples({op:'fork'}) stamps your game's
|
|
65
|
+
* name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
|
|
66
|
+
#define GAME_TITLE "MIRAGE MILE"
|
|
67
|
+
|
|
68
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
69
|
+
* CONTROLLER MAPPING — two layers, both bite:
|
|
70
|
+
*
|
|
71
|
+
* On the pad: SGDK's JOY_readJoypad(JOY_1/JOY_2) returns BUTTON_A/B/C/
|
|
72
|
+
* START/UP/DOWN/LEFT/RIGHT as a bitmask. Gas is BUTTON_A or UP, brake is
|
|
73
|
+
* BUTTON_B or DOWN (real Genesis racers double the face buttons onto the
|
|
74
|
+
* d-pad so either thumb works).
|
|
75
|
+
*
|
|
76
|
+
* Driving this game HEADLESSLY through an emulator (libretro/gpgx): the
|
|
77
|
+
* core maps Genesis A/B/C onto libretro Y/B/A. So setInput({y:true})
|
|
78
|
+
* presses GENESIS A (gas/1P start here), setInput({b:true}) presses
|
|
79
|
+
* GENESIS B (brake/2P select), and setInput({a:true}) presses GENESIS C —
|
|
80
|
+
* NOT Genesis A. Getting this wrong looks like "the game ignores input".
|
|
81
|
+
* START is start.
|
|
82
|
+
*/
|
|
83
|
+
#define BTN_GAS (BUTTON_A | BUTTON_UP)
|
|
84
|
+
#define BTN_BRAKE (BUTTON_B | BUTTON_DOWN)
|
|
85
|
+
|
|
86
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
87
|
+
* Tile art. Genesis tiles are 4bpp: each u32 row = 8 pixels, one hex nibble
|
|
88
|
+
* per pixel = a colour index into the tile's palette line (0 = transparent).
|
|
89
|
+
* Road plane (A) uses PAL1; the HUD band on plane B uses PAL2; sprites pick
|
|
90
|
+
* their line per-sprite (P1 PAL0, traffic PAL2, P2 PAL3) so ONE car tile
|
|
91
|
+
* serves three liveries. */
|
|
92
|
+
#define T_GRASS (TILE_USER_INDEX + 0) /* plane A: roadside base */
|
|
93
|
+
#define T_TUFT (TILE_USER_INDEX + 1) /* plane A: scenery, common */
|
|
94
|
+
#define T_TREE (TILE_USER_INDEX + 2) /* plane A: scenery, rare */
|
|
95
|
+
#define T_ASPHALT (TILE_USER_INDEX + 3) /* plane A: road surface */
|
|
96
|
+
#define T_SPECK (TILE_USER_INDEX + 4) /* plane A: textured asphalt */
|
|
97
|
+
#define T_EDGE (TILE_USER_INDEX + 5) /* plane A: solid shoulder line */
|
|
98
|
+
#define T_DASH (TILE_USER_INDEX + 6) /* plane A: dashed lane line */
|
|
99
|
+
#define T_DIVIDE (TILE_USER_INDEX + 7) /* plane A: double center line */
|
|
100
|
+
#define T_BAND (TILE_USER_INDEX + 8) /* plane B: flat band behind HUD */
|
|
101
|
+
#define T_CAR (TILE_USER_INDEX + 9) /* sprite: player car, nose up */
|
|
102
|
+
#define T_TRAFFIC (TILE_USER_INDEX + 10) /* sprite: slow traffic, tail up */
|
|
103
|
+
|
|
104
|
+
static const u32 tile_grass[8] = { /* speckles make motion visible */
|
|
105
|
+
0x11111111, 0x11121111, 0x11111111, 0x21111112,
|
|
106
|
+
0x11111111, 0x11112111, 0x11111111, 0x12111111,
|
|
107
|
+
};
|
|
108
|
+
static const u32 tile_tuft[8] = {
|
|
109
|
+
0x11111111, 0x11121111, 0x11222111, 0x12222211,
|
|
110
|
+
0x11222111, 0x11121111, 0x11111111, 0x12111121,
|
|
111
|
+
};
|
|
112
|
+
static const u32 tile_tree[8] = {
|
|
113
|
+
0x11166111, 0x11666611, 0x16666661, 0x16666661,
|
|
114
|
+
0x11666611, 0x11122111, 0x11122111, 0x11111111,
|
|
38
115
|
};
|
|
39
|
-
static const u32
|
|
40
|
-
|
|
41
|
-
|
|
116
|
+
static const u32 tile_asphalt[8] = { /* a flat colour shifted N px */
|
|
117
|
+
0x44444444, 0x44445444, 0x44444444, /* looks identical to itself — */
|
|
118
|
+
0x54444444, 0x44444444, 0x44444454, /* the speckle is what makes the */
|
|
119
|
+
0x44444444, 0x44544444, /* scroll readable */
|
|
42
120
|
};
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
static const u32 tile_lane[8] = {
|
|
47
|
-
0x00022000, 0x00022000, 0x00022000, 0x00000000,
|
|
48
|
-
0x00000000, 0x00022000, 0x00022000, 0x00022000,
|
|
121
|
+
static const u32 tile_speck[8] = {
|
|
122
|
+
0x44444444, 0x44544444, 0x44455444, 0x44444444,
|
|
123
|
+
0x44444454, 0x45444444, 0x44444444, 0x44445444,
|
|
49
124
|
};
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
0x00000022, 0x00000022, 0x00000022, 0x00000022,
|
|
54
|
-
0x00000022, 0x00000022, 0x00000022, 0x00000022,
|
|
125
|
+
static const u32 tile_edge[8] = { /* solid white shoulder stripe */
|
|
126
|
+
0x44334444, 0x44334444, 0x44334444, 0x44334444,
|
|
127
|
+
0x44334444, 0x44334444, 0x44334444, 0x44334444,
|
|
55
128
|
};
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
0x55555555, 0x55555565, 0x55555555, 0x55655555,
|
|
129
|
+
static const u32 tile_dash[8] = { /* 4 px on, 4 off: stacked tiles */
|
|
130
|
+
0x44433444, 0x44433444, 0x44433444, /* read as a dashed lane line */
|
|
131
|
+
0x44433444, 0x44444444, 0x44444444,
|
|
132
|
+
0x44444444, 0x44444444,
|
|
61
133
|
};
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
0x66666666, 0x66666656, 0x66666666, 0x66566666,
|
|
134
|
+
static const u32 tile_divide[8] = { /* double line = 2P territory */
|
|
135
|
+
0x43344334, 0x43344334, 0x43344334, /* border */
|
|
136
|
+
0x43344334, 0x43344334, 0x43344334,
|
|
137
|
+
0x43344334, 0x43344334,
|
|
67
138
|
};
|
|
139
|
+
static const u32 tile_band[8] = {
|
|
140
|
+
0x11111111, 0x11111111, 0x11111111, 0x11111111,
|
|
141
|
+
0x11111111, 0x11111111, 0x11111111, 0x11111111,
|
|
142
|
+
};
|
|
143
|
+
static const u32 tile_car[8] = { /* nose up; 1 = body, 2 = glass */
|
|
144
|
+
0x00111100, 0x01111110, 0x11222211, 0x11111111,
|
|
145
|
+
0x01111110, 0x11222211, 0x11111111, 0x01100110,
|
|
146
|
+
};
|
|
147
|
+
static const u32 tile_traffic[8] = { /* tail up (it's slower traffic */
|
|
148
|
+
0x03300330, 0x33333333, 0x33444433, /* you overtake, so you see its */
|
|
149
|
+
0x03333330, 0x33333333, 0x33444433, /* rear). Colours 3/4, NOT 1/2: */
|
|
150
|
+
0x03333330, 0x00333300, /* it shares PAL2 with the HUD */
|
|
151
|
+
}; /* band, whose dark is index 1. */
|
|
68
152
|
|
|
69
|
-
/*
|
|
70
|
-
*
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
#define
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
153
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
154
|
+
* Road geometry. Four 4-cell-wide lanes between shoulders, a double center
|
|
155
|
+
* divider (it's also the 2P territory line). Plane columns (cells):
|
|
156
|
+
* 12 = left shoulder, 16/24 = dashed lane lines, 20 = center divider,
|
|
157
|
+
* 28 = right shoulder; grass outside. The plane is 64 cells wide — paint
|
|
158
|
+
* ALL 64 (the haze wobble slides up to 2 px of the plane's wrap onto the
|
|
159
|
+
* screen edge; bare cells there would flash black). */
|
|
160
|
+
#define COL_EDGE_L 12
|
|
161
|
+
#define COL_DASH_1 16
|
|
162
|
+
#define COL_DIVIDER 20
|
|
163
|
+
#define COL_DASH_2 24
|
|
164
|
+
#define COL_EDGE_R 28
|
|
165
|
+
/* Lane center X for the 8px-wide car sprite (lane i spans 32 px). */
|
|
166
|
+
static const s16 lane_x[4] = { 108, 140, 172, 204 };
|
|
167
|
+
|
|
168
|
+
#define MAX_TRAFFIC 6
|
|
169
|
+
#define CAR_Y 192 /* both players' fixed screen Y */
|
|
170
|
+
#define SPAWN_Y 20 /* traffic entry Y — just below the HUD */
|
|
171
|
+
#define DESPAWN_Y 216 /* traffic exits past the player */
|
|
172
|
+
#define START_LIVES 3 /* crashes per run / per player */
|
|
173
|
+
#define SPAWN_PERIOD 40 /* frames between traffic spawns — traffic
|
|
174
|
+
* moves at road speed, so per-meter density
|
|
175
|
+
* stays constant whatever the player does */
|
|
176
|
+
#define SPEED_2P 2 /* fixed road speed in versus (one VDP =
|
|
177
|
+
* one scroll = one shared speed) */
|
|
178
|
+
#define MAX_SPEED 4 /* px/frame — MUST stay under 8: the row
|
|
179
|
+
* streamer restamps one row per crossing
|
|
180
|
+
* and a >8 px step could skip a row */
|
|
181
|
+
#define HUD_ROWS 2 /* window rows reserved for the HUD */
|
|
182
|
+
|
|
183
|
+
/* Players: index 0 = P1 (pad 1), 1 = P2 (pad 2, versus only). */
|
|
184
|
+
static u8 car_lane[2];
|
|
185
|
+
static u8 car_active[2];
|
|
186
|
+
static u8 crashes_left[2];
|
|
187
|
+
static u8 invuln[2]; /* post-crash blink/no-collide frames */
|
|
188
|
+
static u16 prev_pads[2];
|
|
189
|
+
static u8 lane_min[2], lane_max[2]; /* 2P: split territories */
|
|
190
|
+
static u8 two_player;
|
|
191
|
+
static u8 winner; /* versus result: 0 = P1, 1 = P2 */
|
|
192
|
+
|
|
193
|
+
static u8 traffic_alive[MAX_TRAFFIC];
|
|
194
|
+
static u8 traffic_lane[MAX_TRAFFIC];
|
|
195
|
+
static s16 traffic_y[MAX_TRAFFIC];
|
|
196
|
+
|
|
197
|
+
static u8 speed; /* road px/frame, 1..MAX_SPEED */
|
|
198
|
+
static u16 dist; /* 1P distance, 1 unit = 16 scrolled px */
|
|
199
|
+
static u8 dist_frac;
|
|
200
|
+
static u16 best; /* persisted best 1P distance */
|
|
201
|
+
static u8 spawn_timer;
|
|
202
|
+
static u16 vs; /* vertical scroll. NEVER wrapped by hand: *
|
|
203
|
+
* the plane is 256 px tall, the VDP masks *
|
|
204
|
+
* the scroll value to the plane, and 65536 *
|
|
205
|
+
* is a multiple of 256 — plain u16 *
|
|
206
|
+
* overflow keeps the road seamless forever *
|
|
207
|
+
* (the NES needs a 240-wrap helper here). */
|
|
208
|
+
static u8 prev_top_row; /* last restamped plane row */
|
|
209
|
+
static u8 start_pause; /* freeze frames at green light */
|
|
210
|
+
static u16 rng = 0xC0DE;
|
|
211
|
+
|
|
212
|
+
/* Game states — the shell every example shares: title → play → game over. */
|
|
213
|
+
#define ST_TITLE 0
|
|
214
|
+
#define ST_PLAY 1
|
|
215
|
+
#define ST_OVER 2
|
|
216
|
+
static u8 state;
|
|
217
|
+
|
|
218
|
+
/* ── GAME LOGIC (clay) — xorshift16 PRNG (a few 68k instructions) ── */
|
|
219
|
+
static u8 random8(void) {
|
|
220
|
+
u16 r = rng;
|
|
221
|
+
r ^= r << 7;
|
|
222
|
+
r ^= r >> 9;
|
|
223
|
+
r ^= r << 8;
|
|
224
|
+
rng = r;
|
|
225
|
+
return (u8)r;
|
|
88
226
|
}
|
|
89
227
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
228
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
229
|
+
* CARTRIDGE SRAM — the Genesis battery-save mechanism, three parts:
|
|
230
|
+
*
|
|
231
|
+
* 1. The ROM HEADER declares it: bytes $1B0.. hold 'R','A', a type word
|
|
232
|
+
* ($F820 = battery-backed, byte-wide on ODD addresses — the classic
|
|
233
|
+
* cart wiring), then start/end addresses $200000/$20FFFF. SGDK's
|
|
234
|
+
* rom_header.c (assembled into every build) already declares exactly
|
|
235
|
+
* this — no linker work needed. Emulators allocate the save RAM by
|
|
236
|
+
* READING THIS HEADER; no 'RA' block = writes to $200000+ go nowhere.
|
|
237
|
+
* 2. The MAPPER GATE: writing 1 to $A130F1 banks SRAM into $200000+,
|
|
238
|
+
* 0 banks the ROM back in. SGDK's SRAM_enable()/SRAM_disable() do
|
|
239
|
+
* this. ALWAYS disable after access — on carts >2 MB the SRAM window
|
|
240
|
+
* shadows ROM, and leaving it enabled corrupts later ROM fetches.
|
|
241
|
+
* 3. ODD-BYTE ADDRESSING: SRAM_readByte/writeByte(offset) access 68k
|
|
242
|
+
* address $200001 + offset*2. Headlessly, the emulator's save_ram
|
|
243
|
+
* region interleaves with dead even bytes: SGDK offset k lives at
|
|
244
|
+
* save_ram[k*2 + 1] (the even bytes read back $FF).
|
|
245
|
+
*
|
|
246
|
+
* Best-distance record layout (SGDK offsets): 0='B' 1='D' 2=lo 3=hi
|
|
247
|
+
* 4=checksum(lo^hi^$A5). Fresh SRAM is all $FF — the magic+checksum
|
|
248
|
+
* rejects it (and any corruption) so first boot shows 0, not 65535.
|
|
249
|
+
*
|
|
250
|
+
* Emulator note (verified against gpgx): the core sizes its save_ram
|
|
251
|
+
* region by scanning for the last non-$FF byte, so the region reads as
|
|
252
|
+
* EMPTY until the first write below lands — that's why best_init runs
|
|
253
|
+
* at the very top of main(). Real hardware and .srm-restoring frontends
|
|
254
|
+
* have no such wrinkle. */
|
|
255
|
+
static u16 best_load(void) {
|
|
256
|
+
u8 m0, m1, lo, hi, ck;
|
|
257
|
+
SRAM_enableRO();
|
|
258
|
+
m0 = SRAM_readByte(0);
|
|
259
|
+
m1 = SRAM_readByte(1);
|
|
260
|
+
lo = SRAM_readByte(2);
|
|
261
|
+
hi = SRAM_readByte(3);
|
|
262
|
+
ck = SRAM_readByte(4);
|
|
263
|
+
SRAM_disable();
|
|
264
|
+
if (m0 == 'B' && m1 == 'D' && ck == (u8)(lo ^ hi ^ 0xA5))
|
|
265
|
+
return ((u16)hi << 8) | lo;
|
|
266
|
+
return 0;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
static void best_save(u16 d) {
|
|
270
|
+
u8 lo = (u8)d, hi = (u8)(d >> 8);
|
|
271
|
+
SRAM_enable();
|
|
272
|
+
SRAM_writeByte(0, 'B');
|
|
273
|
+
SRAM_writeByte(1, 'D');
|
|
274
|
+
SRAM_writeByte(2, lo);
|
|
275
|
+
SRAM_writeByte(3, hi);
|
|
276
|
+
SRAM_writeByte(4, (u8)(lo ^ hi ^ 0xA5));
|
|
277
|
+
SRAM_disable();
|
|
101
278
|
}
|
|
102
279
|
|
|
103
|
-
|
|
280
|
+
/* Format-on-first-boot: if the magic is absent (fresh battery), write a
|
|
281
|
+
* valid zero record immediately so the save file exists from frame one. */
|
|
282
|
+
static void best_init(void) {
|
|
283
|
+
best = best_load();
|
|
284
|
+
if (best == 0) best_save(0);
|
|
285
|
+
}
|
|
104
286
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
287
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
288
|
+
* FULL-PLANE VERTICAL SCROLL + STREAMED ROWS — the Genesis road. With
|
|
289
|
+
* VSCROLL_PLANE mode, one VSRAM value scrolls the whole plane vertically;
|
|
290
|
+
* the VDP wraps it inside the 256-px plane in hardware. Screen line y shows
|
|
291
|
+
* plane line (y + vs) & 255, so DECREMENTING vs slides the road DOWN — the
|
|
292
|
+
* driving-up illusion — for the cost of one register write per frame.
|
|
293
|
+
* Zero tilemap writes for the motion itself (rewriting tilemaps in the
|
|
294
|
+
* loop is the #1 "choppy movement" bug).
|
|
295
|
+
*
|
|
296
|
+
* The plane's 32 rows recycle as vs shrinks: the row crossing into the top
|
|
297
|
+
* of the screen is plane row (vs >> 3) & 31. The moment it changes we
|
|
298
|
+
* restamp that ONE row with fresh random roadside, so the 256-px loop
|
|
299
|
+
* never shows the same scenery twice. Three hard rules:
|
|
300
|
+
* 1. DMA_QUEUE only — the queued write lands in vblank, never mid-frame
|
|
301
|
+
* (SYS_doVBlankProcess flushes the queue; raw mid-frame VRAM writes
|
|
302
|
+
* tear). The data buffer must be STATIC: the queue reads it AT FLUSH
|
|
303
|
+
* TIME — a stack buffer is gone by then, shipping garbage.
|
|
304
|
+
* 2. The restamped row enters under the 16-px WINDOW HUD, which hides
|
|
305
|
+
* the swap (the NES version uses the overscan-cropped top band as
|
|
306
|
+
* its curtain; the window is ours). Restamp rows anywhere lower and
|
|
307
|
+
* the player sees tiles pop.
|
|
308
|
+
* 3. Road speed stays under 8 px/frame (MAX_SPEED) so a frame never
|
|
309
|
+
* skips past a whole row crossing.
|
|
310
|
+
*/
|
|
311
|
+
static u16 rowbuf[64]; /* static — the DMA queue reads it at flush time */
|
|
112
312
|
|
|
113
|
-
static
|
|
313
|
+
static u16 road_cell(u16 c) {
|
|
314
|
+
u8 r;
|
|
315
|
+
if (c == COL_EDGE_L || c == COL_EDGE_R)
|
|
316
|
+
return TILE_ATTR_FULL(PAL1, 0, 0, 0, T_EDGE);
|
|
317
|
+
if (c == COL_DIVIDER)
|
|
318
|
+
return TILE_ATTR_FULL(PAL1, 0, 0, 0, T_DIVIDE);
|
|
319
|
+
if (c == COL_DASH_1 || c == COL_DASH_2)
|
|
320
|
+
return TILE_ATTR_FULL(PAL1, 0, 0, 0, T_DASH);
|
|
321
|
+
r = random8();
|
|
322
|
+
if (c > COL_EDGE_L && c < COL_EDGE_R) /* tarmac */
|
|
323
|
+
return TILE_ATTR_FULL(PAL1, 0, 0, 0, (r & 7) == 0 ? T_SPECK : T_ASPHALT);
|
|
324
|
+
if ((r & 31) == 0) return TILE_ATTR_FULL(PAL1, 0, 0, 0, T_TREE);
|
|
325
|
+
if ((r & 7) == 0) return TILE_ATTR_FULL(PAL1, 0, 0, 0, T_TUFT);
|
|
326
|
+
return TILE_ATTR_FULL(PAL1, 0, 0, 0, T_GRASS); /* roadside */
|
|
327
|
+
}
|
|
114
328
|
|
|
115
|
-
static
|
|
116
|
-
|
|
117
|
-
|
|
329
|
+
static void build_road_row(void) {
|
|
330
|
+
u16 c;
|
|
331
|
+
for (c = 0; c < 64; c++) rowbuf[c] = road_cell(c);
|
|
118
332
|
}
|
|
119
333
|
|
|
120
|
-
|
|
334
|
+
/* Initial paint: all 32 plane rows, immediate CPU writes (init-time only —
|
|
335
|
+
* inside the frame loop everything goes through the DMA queue). */
|
|
336
|
+
static void paint_road(void) {
|
|
337
|
+
u16 r;
|
|
338
|
+
for (r = 0; r < 32; r++) {
|
|
339
|
+
build_road_row();
|
|
340
|
+
VDP_setTileMapData(VDP_BG_A, rowbuf, r * 64, 64, 2, CPU);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/* Advance the road by px pixels: one VSRAM write + at most one queued row
|
|
345
|
+
* restamp. Called every frame the road moves (play AND the title drift). */
|
|
346
|
+
static void advance_road(u8 px) {
|
|
347
|
+
u8 top_row;
|
|
348
|
+
vs -= px; /* hardware wraps — see idiom */
|
|
349
|
+
VDP_setVerticalScroll(BG_A, (s16)vs);
|
|
350
|
+
top_row = (u8)((vs >> 3) & 31);
|
|
351
|
+
if (top_row != prev_top_row) {
|
|
352
|
+
prev_top_row = top_row;
|
|
353
|
+
build_road_row();
|
|
354
|
+
VDP_setTileMapData(VDP_BG_A, rowbuf, (u16)top_row * 64, 64, 2, DMA_QUEUE);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
359
|
+
* PER-SCANLINE HSCROLL — the heat-haze band, LIVE. This game runs in
|
|
360
|
+
* HSCROLL_LINE mode: the VDP fetches one hscroll entry per SCANLINE from
|
|
361
|
+
* the hscroll table in VRAM (interleaved words: plane A's value for the
|
|
362
|
+
* line, then plane B's). The platformer template runs the cheaper
|
|
363
|
+
* HSCROLL_TILE (one entry per 8-line strip) and only documents this
|
|
364
|
+
* variant — here it earns its keep: a traveling ±2 px sine wave across a
|
|
365
|
+
* 32-line band of the road reads as heat shimmer rising off the asphalt.
|
|
366
|
+
*
|
|
367
|
+
* Requires: HSCROLL_LINE set BEFORE any scroll-table write (the mode
|
|
368
|
+
* decides the table layout the VDP reads); the value array STATIC (the
|
|
369
|
+
* DMA queue reads it at flush time); and only the band's lines need
|
|
370
|
+
* updating each frame — the other 192 entries stay 0 in VRAM (SGDK's
|
|
371
|
+
* boot cleared VRAM, and a console reset re-runs that boot), so the
|
|
372
|
+
* cost is 32 words = 64 bytes/frame of the ~7 KB vblank budget. The
|
|
373
|
+
* FULL table at one entry per line per plane would be ~1.8 KB/frame —
|
|
374
|
+
* budget it before scaling this up.
|
|
375
|
+
* Sprites are not displaced — per-line hscroll bends PLANES only. The
|
|
376
|
+
* cars drive through the shimmer untouched, which is exactly how real
|
|
377
|
+
* carts looked (and why effect bands avoid gameplay-critical rows). */
|
|
378
|
+
#define HAZE_TOP 96 /* first shimmering scanline */
|
|
379
|
+
#define HAZE_LINES 32
|
|
380
|
+
static s16 haze[HAZE_LINES]; /* static — DMA queue reads at flush time */
|
|
381
|
+
static const s16 haze_wave[8] = { 0, 1, 2, 1, 0, -1, -2, -1 };
|
|
382
|
+
static u16 haze_phase;
|
|
383
|
+
|
|
384
|
+
static void update_haze(void) {
|
|
121
385
|
u16 i;
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
386
|
+
haze_phase++;
|
|
387
|
+
for (i = 0; i < HAZE_LINES; i++)
|
|
388
|
+
haze[i] = haze_wave[(i + (haze_phase >> 1)) & 7];
|
|
389
|
+
VDP_setHorizontalScrollLine(BG_A, HAZE_TOP, haze, HAZE_LINES, DMA_QUEUE);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
393
|
+
* WINDOW-PLANE HUD — the fixed status bar. The window is a third tilemap
|
|
394
|
+
* that REPLACES plane A wherever it's shown and IGNORES ALL SCROLLING —
|
|
395
|
+
* a hardware-fixed HUD with zero per-frame cost over a road that never
|
|
396
|
+
* stops moving (the NES version needs sprite digits for this; on Genesis
|
|
397
|
+
* it's one register). Two footguns:
|
|
398
|
+
* - The window only lives at screen edges (top/bottom N rows or left/
|
|
399
|
+
* right N columns) — it cannot float mid-screen.
|
|
400
|
+
* - It replaces plane A ONLY: plane B and sprites still render behind/
|
|
401
|
+
* over it. Plane B's top rows are painted with a flat dark band so
|
|
402
|
+
* HUD text always reads, and traffic spawns BELOW y=16.
|
|
403
|
+
* Bonus idiom on display here: the title/results text lives on PLANE B
|
|
404
|
+
* with the text priority bit SET, floating over the LOW-priority road on
|
|
405
|
+
* plane A — priority trumps plane order on the Genesis (high-pri B draws
|
|
406
|
+
* above low-pri A), which is how text sits on a busy foreground plane
|
|
407
|
+
* without repainting it. */
|
|
408
|
+
static void hud_init(void) {
|
|
409
|
+
VDP_setWindowOnTop(HUD_ROWS);
|
|
410
|
+
VDP_setTextPriority(1); /* window + plane-B text above the road */
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/* ── GAME LOGIC (clay) — HUD text (window plane, redrawn only on change) ── */
|
|
414
|
+
static void draw_u16(VDPPlane plane, u16 v, u16 x, u16 y) {
|
|
415
|
+
char buf[8];
|
|
416
|
+
uintToStr(v, buf, 5);
|
|
417
|
+
VDP_drawTextBG(plane, buf, x, y);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
static void draw_hud(void) {
|
|
421
|
+
char b[4];
|
|
422
|
+
VDP_clearTextAreaBG(WINDOW, 0, 0, 40, 1);
|
|
423
|
+
if (two_player) {
|
|
424
|
+
b[0] = 'P'; b[1] = '1'; b[2] = 0;
|
|
425
|
+
VDP_drawTextBG(WINDOW, b, 1, 0);
|
|
426
|
+
b[0] = 'x'; b[1] = '0' + crashes_left[0]; b[2] = 0;
|
|
427
|
+
VDP_drawTextBG(WINDOW, b, 4, 0);
|
|
428
|
+
b[0] = 'P'; b[1] = '2'; b[2] = 0;
|
|
429
|
+
VDP_drawTextBG(WINDOW, b, 34, 0);
|
|
430
|
+
b[0] = 'x'; b[1] = '0' + crashes_left[1]; b[2] = 0;
|
|
431
|
+
VDP_drawTextBG(WINDOW, b, 37, 0);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
b[0] = 'x'; b[1] = '0' + crashes_left[0]; b[2] = 0;
|
|
435
|
+
VDP_drawTextBG(WINDOW, b, 1, 0);
|
|
436
|
+
VDP_drawTextBG(WINDOW, "SPD", 5, 0);
|
|
437
|
+
b[0] = '0' + speed; b[1] = 0;
|
|
438
|
+
VDP_drawTextBG(WINDOW, b, 9, 0);
|
|
439
|
+
VDP_drawTextBG(WINDOW, "DIST", 12, 0);
|
|
440
|
+
draw_u16(WINDOW, dist, 17, 0);
|
|
441
|
+
VDP_drawTextBG(WINDOW, "BEST", 24, 0);
|
|
442
|
+
draw_u16(WINDOW, best, 29, 0);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
static void draw_hud_title(void) {
|
|
446
|
+
VDP_clearTextAreaBG(WINDOW, 0, 0, 40, HUD_ROWS);
|
|
447
|
+
VDP_drawTextBG(WINDOW, "BEST", 24, 0);
|
|
448
|
+
draw_u16(WINDOW, best, 29, 0);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/* ── GAME LOGIC (clay) — plane B cards (title / results) ────────────────────
|
|
452
|
+
* Plane B never scrolls: rows 0-1 hold the dark band behind the window HUD,
|
|
453
|
+
* the rest holds high-priority text floating over the live road. Repainted
|
|
454
|
+
* on state changes only. */
|
|
455
|
+
static void paint_band(void) {
|
|
456
|
+
VDP_fillTileMapRect(BG_B, TILE_ATTR_FULL(PAL2, 0, 0, 0, T_BAND),
|
|
457
|
+
0, 0, 64, HUD_ROWS);
|
|
130
458
|
}
|
|
131
459
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (lsb) rng_state ^= 0xB8;
|
|
141
|
-
return rng_state;
|
|
460
|
+
static void paint_title(void) {
|
|
461
|
+
VDP_clearPlane(BG_B, TRUE);
|
|
462
|
+
paint_band();
|
|
463
|
+
VDP_drawTextBG(BG_B, GAME_TITLE, (40 - (sizeof(GAME_TITLE) - 1)) / 2, 6);
|
|
464
|
+
VDP_drawTextBG(BG_B, "1P RACE - A", 14, 12);
|
|
465
|
+
VDP_drawTextBG(BG_B, "2P VERSUS - B", 13, 14);
|
|
466
|
+
VDP_drawTextBG(BG_B, "STEER L R - GAS A - BRAKE B", 6, 20);
|
|
467
|
+
draw_hud_title();
|
|
142
468
|
}
|
|
143
469
|
|
|
144
|
-
static void
|
|
470
|
+
static void paint_over(void) {
|
|
471
|
+
VDP_clearPlane(BG_B, TRUE);
|
|
472
|
+
paint_band();
|
|
473
|
+
if (two_player) {
|
|
474
|
+
VDP_drawTextBG(BG_B, winner ? "P2 WINS" : "P1 WINS", 16, 8);
|
|
475
|
+
VDP_drawTextBG(BG_B, "RIVAL WRECKED", 13, 12);
|
|
476
|
+
} else {
|
|
477
|
+
VDP_drawTextBG(BG_B, "WRECKED", 16, 8);
|
|
478
|
+
VDP_drawTextBG(BG_B, "DIST", 13, 12);
|
|
479
|
+
draw_u16(BG_B, dist, 18, 12);
|
|
480
|
+
VDP_drawTextBG(BG_B, "BEST", 13, 14);
|
|
481
|
+
draw_u16(BG_B, best, 18, 14);
|
|
482
|
+
}
|
|
483
|
+
VDP_drawTextBG(BG_B, "START - TITLE", 13, 20);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/* ── GAME LOGIC (clay) — traffic pool (fixed slots, no allocation) ── */
|
|
487
|
+
static void spawn_traffic(void) {
|
|
145
488
|
u16 i;
|
|
146
|
-
for (i = 0; i <
|
|
147
|
-
if (!
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
489
|
+
for (i = 0; i < MAX_TRAFFIC; i++) {
|
|
490
|
+
if (!traffic_alive[i]) {
|
|
491
|
+
traffic_alive[i] = 1;
|
|
492
|
+
traffic_lane[i] = random8() & 3;
|
|
493
|
+
traffic_y[i] = SPAWN_Y;
|
|
151
494
|
return;
|
|
152
495
|
}
|
|
153
496
|
}
|
|
154
497
|
}
|
|
155
498
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
499
|
+
/* AABB, both boxes 8x8 (s16 math — sprite coords go negative off-screen). */
|
|
500
|
+
static u8 hits(s16 ax, s16 ay, s16 bx, s16 by) {
|
|
501
|
+
return ax < bx + 8 && ax + 8 > bx && ay < by + 8 && ay + 8 > by;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/* ── GAME LOGIC (clay) — start a run ── */
|
|
505
|
+
static void start_game(u8 versus) {
|
|
506
|
+
u16 i;
|
|
507
|
+
two_player = versus;
|
|
508
|
+
for (i = 0; i < MAX_TRAFFIC; i++) traffic_alive[i] = 0;
|
|
509
|
+
for (i = 0; i < 2; i++) {
|
|
510
|
+
crashes_left[i] = START_LIVES;
|
|
511
|
+
invuln[i] = 0;
|
|
512
|
+
prev_pads[i] = 0xFFFF; /* swallow buttons held across the change */
|
|
513
|
+
}
|
|
514
|
+
if (versus) {
|
|
515
|
+
car_active[0] = 1; car_active[1] = 1;
|
|
516
|
+
lane_min[0] = 0; lane_max[0] = 1; car_lane[0] = 0; /* P1: left half */
|
|
517
|
+
lane_min[1] = 2; lane_max[1] = 3; car_lane[1] = 3; /* P2: right half */
|
|
518
|
+
speed = SPEED_2P; /* shared road, fixed speed (see header) */
|
|
519
|
+
} else {
|
|
520
|
+
car_active[0] = 1; car_active[1] = 0;
|
|
521
|
+
lane_min[0] = 0; lane_max[0] = 3; car_lane[0] = 1; /* whole road */
|
|
522
|
+
speed = 1;
|
|
523
|
+
}
|
|
524
|
+
dist = 0; dist_frac = 0;
|
|
525
|
+
spawn_timer = 0;
|
|
526
|
+
start_pause = 30; /* green-light breather */
|
|
527
|
+
VDP_clearPlane(BG_B, TRUE); /* drop the title card — road shows clear */
|
|
528
|
+
paint_band();
|
|
529
|
+
draw_hud();
|
|
530
|
+
sfx_tone(0, 523, 10); /* start jingle (C5) */
|
|
531
|
+
state = ST_PLAY;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
static void game_over(void) {
|
|
535
|
+
if (!two_player && dist > best) {
|
|
536
|
+
best = dist;
|
|
537
|
+
best_save(best); /* battery SRAM — see the SRAM idiom */
|
|
538
|
+
}
|
|
539
|
+
state = ST_OVER;
|
|
540
|
+
paint_over();
|
|
541
|
+
draw_hud_title(); /* window shows BEST — may have changed */
|
|
542
|
+
sfx_noise(20);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/* ── GAME LOGIC (clay) — crash rules ── */
|
|
546
|
+
static void crash(u8 p) {
|
|
547
|
+
sfx_noise(14);
|
|
548
|
+
invuln[p] = 60; /* blink + no-collide grace */
|
|
549
|
+
if (!two_player) speed = 1; /* a wreck kills your momentum */
|
|
550
|
+
if (crashes_left[p] > 0) --crashes_left[p];
|
|
551
|
+
if (crashes_left[p] == 0) {
|
|
552
|
+
winner = (u8)(1 - p); /* versus: the OTHER player wins */
|
|
553
|
+
game_over();
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
draw_hud();
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/* ── GAME LOGIC (clay) — per-player input ───────────────────────────────────
|
|
560
|
+
* LEFT/RIGHT steer between lanes (edge-detected — held d-pad shouldn't
|
|
561
|
+
* machine-gun across the road). 1P only: A/UP accelerate, B/DOWN brake
|
|
562
|
+
* (speed is shared in versus — see the design note). */
|
|
563
|
+
static void update_player(u8 p) {
|
|
564
|
+
u16 pad = JOY_readJoypad(p ? JOY_2 : JOY_1);
|
|
565
|
+
u16 pressed = pad & ~prev_pads[p];
|
|
566
|
+
prev_pads[p] = pad;
|
|
567
|
+
if (!car_active[p]) return;
|
|
568
|
+
if ((pressed & BUTTON_LEFT) && car_lane[p] > lane_min[p]) {
|
|
569
|
+
--car_lane[p];
|
|
570
|
+
sfx_tone(0, 880, 3); /* lane tick */
|
|
571
|
+
}
|
|
572
|
+
if ((pressed & BUTTON_RIGHT) && car_lane[p] < lane_max[p]) {
|
|
573
|
+
++car_lane[p];
|
|
574
|
+
sfx_tone(0, 880, 3);
|
|
575
|
+
}
|
|
576
|
+
if (!two_player) {
|
|
577
|
+
if ((pressed & BTN_GAS) && speed < MAX_SPEED) {
|
|
578
|
+
++speed;
|
|
579
|
+
sfx_tone(1, (u16)(700 - speed * 120), 8); /* engine rev */
|
|
580
|
+
draw_hud();
|
|
581
|
+
}
|
|
582
|
+
if ((pressed & BTN_BRAKE) && speed > 1) {
|
|
583
|
+
--speed;
|
|
584
|
+
sfx_tone(1, 220, 5); /* brake blip */
|
|
585
|
+
draw_hud();
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if (invuln[p] > 0) --invuln[p];
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/* ── GAME LOGIC (clay) — stage this frame's sprites ─────────────────────────
|
|
592
|
+
* Fixed SAT slots: 0 = P1, 1 = P2, 2-7 = traffic. Hidden sprites park at
|
|
593
|
+
* y = -16 (above the screen). NEVER hide with x = -128..0 — a SAT x of 0
|
|
594
|
+
* is the VDP's sprite-masking trigger and silently blanks every lower-
|
|
595
|
+
* priority sprite on those scanlines. */
|
|
596
|
+
#define HIDE_Y (-16)
|
|
597
|
+
static void stage_sprites(void) {
|
|
598
|
+
u16 i;
|
|
599
|
+
u8 p;
|
|
600
|
+
for (p = 0; p < 2; p++) {
|
|
601
|
+
u8 vis = (state == ST_PLAY) && car_active[p] && !(invuln[p] & 2);
|
|
602
|
+
VDP_setSprite(p, lane_x[car_lane[p]], vis ? (s16)CAR_Y : (s16)HIDE_Y,
|
|
603
|
+
SPRITE_SIZE(1, 1),
|
|
604
|
+
TILE_ATTR_FULL(p ? PAL3 : PAL0, 1, 0, 0, T_CAR));
|
|
605
|
+
}
|
|
606
|
+
for (i = 0; i < MAX_TRAFFIC; i++) {
|
|
607
|
+
u8 vis = (state == ST_PLAY) && traffic_alive[i];
|
|
608
|
+
VDP_setSprite(2 + i, lane_x[traffic_lane[i]],
|
|
609
|
+
vis ? traffic_y[i] : (s16)HIDE_Y,
|
|
610
|
+
SPRITE_SIZE(1, 1),
|
|
611
|
+
TILE_ATTR_FULL(PAL2, 1, 0, 0, T_TRAFFIC));
|
|
612
|
+
}
|
|
613
|
+
/* ── HARDWARE IDIOM (load-bearing) — CHAIN the sprite list before
|
|
614
|
+
* uploading. VDP_setSprite does NOT set the SAT link byte, and link 0
|
|
615
|
+
* means "end of list": skip this and the VDP draws sprite 0 only.
|
|
616
|
+
* VDP_linkSprites(0, 8) links slots 0..7; the queued DMA flushes the
|
|
617
|
+
* 8 SAT entries during vblank. ── */
|
|
618
|
+
VDP_linkSprites(0, 8);
|
|
619
|
+
VDP_updateSprites(8, DMA_QUEUE);
|
|
162
620
|
}
|
|
163
621
|
|
|
164
622
|
int main(bool hard) {
|
|
623
|
+
u16 i, pad, fresh;
|
|
624
|
+
u8 p;
|
|
165
625
|
(void)hard;
|
|
166
626
|
|
|
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
|
-
|
|
627
|
+
/* SRAM first — before any VDP work. The save file then exists within
|
|
628
|
+
* the game's first frames of life, which is what lets a frontend (or
|
|
629
|
+
* a headless host) see a non-empty save_ram region as early as
|
|
630
|
+
* possible (see the SRAM idiom note on gpgx's size scan). */
|
|
631
|
+
best_init();
|
|
632
|
+
|
|
633
|
+
/* ── HARDWARE IDIOM (load-bearing — see TROUBLESHOOTING) ──
|
|
634
|
+
* Init order: scrolling MODE before scroll VALUES (the mode decides
|
|
635
|
+
* the hscroll-table layout the VDP reads — see the haze idiom), tiles
|
|
636
|
+
* + palettes before tilemaps that reference them, window size before
|
|
637
|
+
* window text. SGDK's boot already did the dangerous part (VDP regs,
|
|
638
|
+
* Z80, vblank int, VRAM clear). */
|
|
639
|
+
VDP_setScrollingMode(HSCROLL_LINE, VSCROLL_PLANE);
|
|
640
|
+
hud_init();
|
|
641
|
+
|
|
642
|
+
/* Palettes: PAL0 P1 car + font, PAL1 road plane, PAL2 traffic + HUD
|
|
643
|
+
* band, PAL3 P2 car. Colours are BGR, 3 bits per channel: 0x0BGR with
|
|
644
|
+
* E = full. */
|
|
645
|
+
PAL_setColor( 1, 0x0E44); /* P1 body electric blue */
|
|
646
|
+
PAL_setColor( 2, 0x0420); /* P1 glass dark */
|
|
647
|
+
PAL_setColor(15, 0x0EEE); /* font white (index 15 = SGDK font) */
|
|
648
|
+
PAL_setColor(16 + 1, 0x0292); /* grass green */
|
|
649
|
+
PAL_setColor(16 + 2, 0x0161); /* tuft dark green */
|
|
650
|
+
PAL_setColor(16 + 3, 0x0EEE); /* road markings white */
|
|
651
|
+
PAL_setColor(16 + 4, 0x0444); /* asphalt grey */
|
|
652
|
+
PAL_setColor(16 + 5, 0x0666); /* asphalt speck */
|
|
653
|
+
PAL_setColor(16 + 6, 0x0040); /* tree foliage deep green*/
|
|
654
|
+
PAL_setColor(32 + 1, 0x0202); /* HUD band near-black */
|
|
655
|
+
PAL_setColor(32 + 3, 0x022E); /* traffic body red */
|
|
656
|
+
PAL_setColor(32 + 4, 0x0CCC); /* traffic glass light */
|
|
657
|
+
PAL_setColor(48 + 1, 0x04C4); /* P2 body green */
|
|
658
|
+
PAL_setColor(48 + 2, 0x0420); /* P2 glass dark */
|
|
659
|
+
|
|
660
|
+
VDP_loadTileData(tile_grass, T_GRASS, 1, DMA);
|
|
661
|
+
VDP_loadTileData(tile_tuft, T_TUFT, 1, DMA);
|
|
662
|
+
VDP_loadTileData(tile_tree, T_TREE, 1, DMA);
|
|
663
|
+
VDP_loadTileData(tile_asphalt, T_ASPHALT, 1, DMA);
|
|
664
|
+
VDP_loadTileData(tile_speck, T_SPECK, 1, DMA);
|
|
665
|
+
VDP_loadTileData(tile_edge, T_EDGE, 1, DMA);
|
|
666
|
+
VDP_loadTileData(tile_dash, T_DASH, 1, DMA);
|
|
667
|
+
VDP_loadTileData(tile_divide, T_DIVIDE, 1, DMA);
|
|
668
|
+
VDP_loadTileData(tile_band, T_BAND, 1, DMA);
|
|
669
|
+
VDP_loadTileData(tile_car, T_CAR, 1, DMA);
|
|
670
|
+
VDP_loadTileData(tile_traffic, T_TRAFFIC, 1, DMA);
|
|
671
|
+
|
|
672
|
+
paint_road(); /* plane A: 32 rows, then streamed forever */
|
|
673
|
+
sfx_init(); /* PSG: sfx channels + background melody */
|
|
674
|
+
|
|
675
|
+
vs = 0;
|
|
676
|
+
prev_top_row = 0;
|
|
677
|
+
state = ST_TITLE;
|
|
678
|
+
paint_title();
|
|
679
|
+
prev_pads[0] = 0xFFFF;
|
|
193
680
|
|
|
194
681
|
while (TRUE) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if (
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
if (obstacles[i].y > 224) obstacles[i].alive = FALSE;
|
|
682
|
+
if (state == ST_TITLE) {
|
|
683
|
+
/* ── GAME LOGIC (clay) — title: A = 1P race, B = 2P versus ──
|
|
684
|
+
* The road idles under the title card so the screen sells the
|
|
685
|
+
* scroll + the heat haze before anyone presses a button. */
|
|
686
|
+
advance_road(1);
|
|
687
|
+
update_haze();
|
|
688
|
+
stage_sprites();
|
|
689
|
+
pad = JOY_readJoypad(JOY_1);
|
|
690
|
+
fresh = pad & ~prev_pads[0];
|
|
691
|
+
if (fresh & (BUTTON_A | BUTTON_C | BUTTON_START)) start_game(0);
|
|
692
|
+
else if (fresh & BUTTON_B) start_game(1);
|
|
693
|
+
else prev_pads[0] = pad;
|
|
694
|
+
sfx_update();
|
|
695
|
+
SYS_doVBlankProcess();
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (state == ST_OVER) {
|
|
700
|
+
/* Results card; the road freezes, the haze keeps shimmering.
|
|
701
|
+
* START or A returns to the title. */
|
|
702
|
+
update_haze();
|
|
703
|
+
stage_sprites();
|
|
704
|
+
pad = JOY_readJoypad(JOY_1);
|
|
705
|
+
fresh = pad & ~prev_pads[0];
|
|
706
|
+
if (fresh & (BUTTON_START | BUTTON_A | BUTTON_C)) {
|
|
707
|
+
state = ST_TITLE;
|
|
708
|
+
prev_pads[0] = 0xFFFF; /* swallow the held START */
|
|
709
|
+
paint_title();
|
|
710
|
+
} else {
|
|
711
|
+
prev_pads[0] = pad;
|
|
226
712
|
}
|
|
713
|
+
sfx_update();
|
|
714
|
+
SYS_doVBlankProcess();
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
227
717
|
|
|
228
|
-
|
|
718
|
+
/* ── ST_PLAY ──────────────────────────────────────────────────── */
|
|
719
|
+
stage_sprites();
|
|
720
|
+
update_haze();
|
|
229
721
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
722
|
+
if (start_pause) { /* green light: freeze gameplay, */
|
|
723
|
+
--start_pause; /* keep frames honest (sprites + */
|
|
724
|
+
sfx_update(); /* haze staged) */
|
|
725
|
+
SYS_doVBlankProcess();
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
advance_road(speed);
|
|
730
|
+
|
|
731
|
+
/* ── GAME LOGIC (clay) from here down ── */
|
|
732
|
+
update_player(0);
|
|
733
|
+
if (two_player) update_player(1);
|
|
734
|
+
|
|
735
|
+
/* Distance (1P stat): 1 unit per 16 scrolled pixels. A chime every
|
|
736
|
+
* 256 units marks a checkpoint. */
|
|
737
|
+
if (!two_player) {
|
|
738
|
+
dist_frac = (u8)(dist_frac + speed);
|
|
739
|
+
if (dist_frac >= 16) {
|
|
740
|
+
dist_frac -= 16;
|
|
741
|
+
if (dist < 65535u) ++dist;
|
|
742
|
+
draw_u16(WINDOW, dist, 17, 0);
|
|
743
|
+
if (dist != 0 && (dist & 0xFF) == 0)
|
|
744
|
+
sfx_tone(0, 1047, 8); /* checkpoint chime (C6) */
|
|
236
745
|
}
|
|
746
|
+
}
|
|
237
747
|
|
|
238
|
-
|
|
748
|
+
/* Traffic flows down at road speed (it reads as slower cars you're
|
|
749
|
+
* overtaking); despawn past the player with a little pass tick. */
|
|
750
|
+
for (i = 0; i < MAX_TRAFFIC; i++) {
|
|
751
|
+
if (!traffic_alive[i]) continue;
|
|
752
|
+
traffic_y[i] += speed;
|
|
753
|
+
if (traffic_y[i] > DESPAWN_Y) {
|
|
754
|
+
traffic_alive[i] = 0;
|
|
755
|
+
sfx_tone(1, 660, 2);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
if (++spawn_timer >= SPAWN_PERIOD) {
|
|
759
|
+
spawn_timer = 0;
|
|
760
|
+
spawn_traffic();
|
|
239
761
|
}
|
|
240
762
|
|
|
241
|
-
/*
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
763
|
+
/* Traffic ↔ cars. Crash grace: a just-wrecked car blinks and can't
|
|
764
|
+
* collide for 60 frames. */
|
|
765
|
+
for (i = 0; i < MAX_TRAFFIC && state == ST_PLAY; i++) {
|
|
766
|
+
if (!traffic_alive[i]) continue;
|
|
767
|
+
for (p = 0; p < 2; p++) {
|
|
768
|
+
if (!car_active[p] || invuln[p]) continue;
|
|
769
|
+
if (hits(lane_x[traffic_lane[i]], traffic_y[i],
|
|
770
|
+
lane_x[car_lane[p]], CAR_Y)) {
|
|
771
|
+
traffic_alive[i] = 0;
|
|
772
|
+
crash(p);
|
|
773
|
+
break;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
248
776
|
}
|
|
249
|
-
/* Link slots 0..slot-1 so the VDP's SAT walk draws all of them — without
|
|
250
|
-
* this the link bytes stay 0 (= end-of-list) and only slot 0 renders. */
|
|
251
|
-
VDP_linkSprites(0, slot);
|
|
252
|
-
VDP_updateSprites(slot, DMA);
|
|
253
777
|
|
|
254
|
-
render_score();
|
|
255
778
|
sfx_update();
|
|
256
779
|
SYS_doVBlankProcess();
|
|
257
780
|
}
|