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,43 +1,68 @@
|
|
|
1
|
-
/* ── racing.c — NES top-down
|
|
1
|
+
/* ── racing.c — NES top-down road racer (complete example game) ──────────────
|
|
2
2
|
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* THROTTLE FEUD — a COMPLETE, working game: title screen, 1P endless race and
|
|
4
|
+
* 2P simultaneous VERSUS, a vertically-scrolling road (the real thing — BG
|
|
5
|
+
* scroll, not falling sprites), streamed roadside scenery through the queued
|
|
6
|
+
* tile path, crash/lives rules, persistent best distance (battery SRAM),
|
|
7
|
+
* music + SFX.
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* - AABB collision; on hit, game-over state pauses for 60 frames then
|
|
14
|
-
* resets the run
|
|
15
|
-
* - Score = frames-survived-in-current-run, rendered with the
|
|
16
|
-
* digit tiles
|
|
9
|
+
* THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
|
|
10
|
+
* very different one. The markers tell you what's what:
|
|
11
|
+
* HARDWARE IDIOM (load-bearing) — dodges a documented NES footgun; reshape
|
|
12
|
+
* your gameplay around it (see TROUBLESHOOTING before changing).
|
|
13
|
+
* GAME LOGIC (clay) — traffic patterns, speeds, tuning, art: reshape freely.
|
|
17
14
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
15
|
+
* What depends on what:
|
|
16
|
+
* nes_runtime.{h,c} — rendering/input/sound/text/hi-score library.
|
|
17
|
+
* chr-ram-runtime.crt0.s — boot + NMI + iNES header (BATTERY bit feeds
|
|
18
|
+
* hiscore_load/save; vertical mirroring makes the Y-wrap seamless).
|
|
19
|
+
* Load-bearing; edit with TROUBLESHOOTING open.
|
|
21
20
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* THE DESIGN (read before reshaping):
|
|
22
|
+
* Scrolling — the road is the BACKGROUND, scrolled down by decrementing
|
|
23
|
+
* scroll_y each frame (the crt0 NMI commits scroll_x AND scroll_y every
|
|
24
|
+
* vblank). Cars/traffic are sprites with their own Y. See the Y-WRAP
|
|
25
|
+
* idiom below: NES vertical scroll wraps at 240, NOT 256.
|
|
26
|
+
* HUD — sprite digits on a fixed scanline. With the whole BG scrolling
|
|
27
|
+
* vertically, a fixed BG HUD would need a mid-frame Y-scroll change:
|
|
28
|
+
* unlike the X-only sprite-0 split in shmup.c, mid-frame Y needs the
|
|
29
|
+
* 4-write $2006/$2005 sequence (the advanced variant — see
|
|
30
|
+
* TROUBLESHOOTING). Sprite HUD is the simple honest option, so that's
|
|
31
|
+
* what this game uses. Budget rule: max 8 sprites per scanline.
|
|
32
|
+
* 2P VERSUS — ONE PPU means ONE road scroll, so both players share one
|
|
33
|
+
* road at a fixed speed and only steer: solid center divider, P1 (blue,
|
|
34
|
+
* port 0) owns the left two lanes, P2 (green, port 1) the right two.
|
|
35
|
+
* Each starts with 3 crashes; first to use them all LOSES.
|
|
36
|
+
* 1P RACE — all four lanes, A/UP accelerates, B/DOWN brakes (speed 1-4);
|
|
37
|
+
* 3 crashes end the run. Persistent stat: best DISTANCE (uint16, one
|
|
38
|
+
* unit = 16 scrolled pixels ≈ one car length) via hiscore_load/save.
|
|
39
|
+
*
|
|
40
|
+
* Frame budget (NTSC, 60fps): 6 traffic × 2 cars AABB = 12 checks, ≤4
|
|
41
|
+
* queued tile writes per row crossing, and HUD digits recomputed only when
|
|
42
|
+
* the distance value changes — comfortably inside one frame.
|
|
24
43
|
*/
|
|
25
44
|
|
|
26
45
|
#include "nes_runtime.h"
|
|
27
46
|
|
|
28
|
-
/*
|
|
47
|
+
/* The title screen renders this — examples({op:'fork'}) stamps your game's
|
|
48
|
+
* name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
|
|
49
|
+
#define GAME_TITLE "THROTTLE FEUD"
|
|
50
|
+
|
|
51
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
52
|
+
* Sprite tile art ($0000 pattern table). Each 8x8 tile = 16 bytes: 8 plane-0
|
|
53
|
+
* rows then 8 plane-1 rows (2bpp — plane0-only = colour 1, both = colour 3). */
|
|
29
54
|
static const uint8_t tile_blank[16] = { 0 };
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
0x3C, 0x7E, 0x42, 0x7E, 0x7E, 0x7E, 0x42, 0x66,
|
|
55
|
+
static const uint8_t tile_car[16] = { /* player car, nose up */
|
|
56
|
+
0x18, 0x7E, 0x5A, 0x7E, 0x3C, 0x7E, 0x5A, 0x66,
|
|
33
57
|
0, 0, 0, 0, 0, 0, 0, 0,
|
|
34
58
|
};
|
|
35
|
-
/*
|
|
36
|
-
|
|
37
|
-
0x66, 0x42, 0x7E, 0x7E, 0x7E, 0x42, 0x7E, 0x3C,
|
|
59
|
+
static const uint8_t tile_traffic[16] = { /* slow traffic, tail up */
|
|
60
|
+
0x66, 0x5A, 0x7E, 0x3C, 0x7E, 0x5A, 0x7E, 0x18,
|
|
38
61
|
0, 0, 0, 0, 0, 0, 0, 0,
|
|
39
62
|
};
|
|
40
|
-
/*
|
|
63
|
+
/* Compact 3x5 digits for the sprite HUD. font_upload() only serves the
|
|
64
|
+
* BACKGROUND pattern table, and sprites read from $0000 — so the HUD gets
|
|
65
|
+
* its own digit tiles on the sprite side. */
|
|
41
66
|
static const uint8_t tile_digits[10 * 16] = {
|
|
42
67
|
/* 0 */ 0xE0,0xA0,0xA0,0xA0,0xE0,0x00,0x00,0x00, 0,0,0,0,0,0,0,0,
|
|
43
68
|
/* 1 */ 0x40,0xC0,0x40,0x40,0xE0,0x00,0x00,0x00, 0,0,0,0,0,0,0,0,
|
|
@@ -50,249 +75,518 @@ static const uint8_t tile_digits[10 * 16] = {
|
|
|
50
75
|
/* 8 */ 0xE0,0xA0,0xE0,0xA0,0xE0,0x00,0x00,0x00, 0,0,0,0,0,0,0,0,
|
|
51
76
|
/* 9 */ 0xE0,0xA0,0xE0,0x20,0xE0,0x00,0x00,0x00, 0,0,0,0,0,0,0,0,
|
|
52
77
|
};
|
|
78
|
+
#define TILE_CAR 1
|
|
79
|
+
#define TILE_TRAFFIC 2
|
|
80
|
+
#define TILE_DIGIT0 3 /* sprite tiles 3-12 */
|
|
53
81
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
* (white) draws the markings, colour 2 (green) the grass, colour 3 the
|
|
64
|
-
* dark seam in the tarmac.
|
|
65
|
-
*
|
|
66
|
-
* BG_T_EDGE: a solid 2px vertical stripe — the road shoulder line.
|
|
67
|
-
* BG_T_LANE: a 2px vertical dash (on 4 rows / off 4) — the dashed centre
|
|
68
|
-
* lane marking when stacked down a column.
|
|
69
|
-
* BG_T_GRASS: a textured green roadside (colour 2 hatch) so the area
|
|
70
|
-
* outside the shoulders isn't flat — fills the screen sides.
|
|
71
|
-
* BG_T_ROAD: a faint tarmac texture (a couple of colour-3 specks) tiled
|
|
72
|
-
* across the driving surface so the road doesn't read as one
|
|
73
|
-
* solid grey block. */
|
|
74
|
-
#define BG_T_EDGE 1
|
|
75
|
-
#define BG_T_LANE 2
|
|
76
|
-
#define BG_T_GRASS 3
|
|
77
|
-
#define BG_T_ROAD 4
|
|
78
|
-
static const uint8_t bg_tile_edge[16] = {
|
|
79
|
-
0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, /* plane 0 (colour bit 0) */
|
|
82
|
+
/* ── GAME LOGIC (clay) — road BG tiles (BACKGROUND pattern table $1000 —
|
|
83
|
+
* separate from the sprite table at $0000; the runtime's PPUCTRL setup makes
|
|
84
|
+
* that split). Colour 0 = the grey backdrop = the asphalt itself. */
|
|
85
|
+
static const uint8_t bg_edge[16] = { /* solid shoulder/divider line */
|
|
86
|
+
0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
|
|
87
|
+
0, 0, 0, 0, 0, 0, 0, 0,
|
|
88
|
+
};
|
|
89
|
+
static const uint8_t bg_dash[16] = { /* lane dash: 4 px on, 4 off */
|
|
90
|
+
0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00,
|
|
80
91
|
0, 0, 0, 0, 0, 0, 0, 0,
|
|
81
92
|
};
|
|
82
|
-
static const uint8_t
|
|
83
|
-
0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, /* dash: 4 on, 4 off */
|
|
93
|
+
static const uint8_t bg_grass[16] = { /* roadside hatch (colour 2) */
|
|
84
94
|
0, 0, 0, 0, 0, 0, 0, 0,
|
|
95
|
+
0xEE, 0xBB, 0xEE, 0xBB, 0xEE, 0xBB, 0xEE, 0xBB,
|
|
85
96
|
};
|
|
86
|
-
static const uint8_t
|
|
87
|
-
|
|
88
|
-
0xEE, 0xBB, 0xEE,
|
|
97
|
+
static const uint8_t bg_tuft[16] = { /* scenery: grass tuft */
|
|
98
|
+
0x00, 0x00, 0x00, 0x24, 0x5A, 0x00, 0x00, 0x00,
|
|
99
|
+
0xEE, 0xBB, 0xEE, 0x9B, 0xA4, 0xBB, 0xEE, 0xBB,
|
|
89
100
|
};
|
|
90
|
-
static const uint8_t
|
|
91
|
-
|
|
92
|
-
|
|
101
|
+
static const uint8_t bg_tree[16] = { /* scenery: bush/tree */
|
|
102
|
+
0x18, 0x3C, 0x7E, 0x7E, 0x3C, 0x18, 0x18, 0x00,
|
|
103
|
+
0x18, 0x3C, 0x7E, 0x7E, 0x3C, 0x18, 0x18, 0xBB,
|
|
93
104
|
};
|
|
105
|
+
static const uint8_t bg_speck[16] = { /* tarmac texture speck */
|
|
106
|
+
0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00,
|
|
107
|
+
0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00,
|
|
108
|
+
};
|
|
109
|
+
#define BG_EDGE 1
|
|
110
|
+
#define BG_DASH 2
|
|
111
|
+
#define BG_GRASS 3
|
|
112
|
+
#define BG_TUFT 4
|
|
113
|
+
#define BG_TREE 5
|
|
114
|
+
#define BG_SPECK 6
|
|
94
115
|
|
|
95
116
|
static const uint8_t palette[32] = {
|
|
96
|
-
/* BG
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
117
|
+
/* BG: dark-grey asphalt backdrop, white markings, green grass,
|
|
118
|
+
* light-grey specks. One palette everywhere = no attribute scrolling
|
|
119
|
+
* headaches (attribute bytes cover 16x16 zones and scroll WITH the BG). */
|
|
120
|
+
0x00, 0x30, 0x1A, 0x10,
|
|
121
|
+
0x00, 0x30, 0x1A, 0x10,
|
|
122
|
+
0x00, 0x30, 0x1A, 0x10,
|
|
123
|
+
0x00, 0x30, 0x1A, 0x10,
|
|
124
|
+
/* Sprites: P1 blue, P2 green, traffic red, HUD white */
|
|
125
|
+
0x00, 0x21, 0x11, 0x30,
|
|
126
|
+
0x00, 0x2A, 0x1A, 0x30,
|
|
127
|
+
0x00, 0x16, 0x06, 0x30,
|
|
128
|
+
0x00, 0x30, 0x10, 0x00,
|
|
106
129
|
};
|
|
130
|
+
#define PAL_P1 0
|
|
131
|
+
#define PAL_P2 1
|
|
132
|
+
#define PAL_TRAFFIC 2
|
|
133
|
+
#define PAL_HUD 3
|
|
134
|
+
|
|
135
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
136
|
+
* Road geometry. Four 4-tile-wide lanes between shoulders, solid divider in
|
|
137
|
+
* the middle (it's also the 2P territory line). Tile columns:
|
|
138
|
+
* 7 = left shoulder, 12/20 = dashed lane lines, 16 = solid center divider,
|
|
139
|
+
* 24 = right shoulder; grass outside. */
|
|
140
|
+
#define COL_EDGE_L 7
|
|
141
|
+
#define COL_DASH_1 12
|
|
142
|
+
#define COL_DIVIDER 16
|
|
143
|
+
#define COL_DASH_2 20
|
|
144
|
+
#define COL_EDGE_R 24
|
|
145
|
+
/* Lane center X for the 8px-wide car sprite (lane i spans 32 px). */
|
|
146
|
+
static const uint8_t lane_x[4] = { 76, 108, 140, 172 };
|
|
107
147
|
|
|
108
|
-
#define
|
|
109
|
-
#define
|
|
110
|
-
#define
|
|
111
|
-
#define
|
|
148
|
+
#define MAX_TRAFFIC 6
|
|
149
|
+
#define CAR_Y 200 /* both players' fixed screen Y */
|
|
150
|
+
#define HUD_Y 9 /* sprite HUD scanline (top 8 are overscan-cropped) */
|
|
151
|
+
#define SPAWN_Y 18 /* traffic entry Y — BELOW the HUD scanlines so
|
|
152
|
+
* traffic never shares them (8 sprites/scanline
|
|
153
|
+
* is a hard PPU limit; the 1P HUD already puts
|
|
154
|
+
* 6 there) */
|
|
155
|
+
#define START_LIVES 3 /* crashes per run/per player */
|
|
156
|
+
#define SPAWN_PERIOD 40 /* frames between traffic spawns — traffic moves
|
|
157
|
+
* at road speed, so per-meter density stays
|
|
158
|
+
* constant whatever the player's speed is */
|
|
159
|
+
#define SPEED_2P 2 /* fixed road speed in versus (one PPU = one
|
|
160
|
+
* scroll = one shared speed; see header) */
|
|
112
161
|
|
|
113
|
-
|
|
162
|
+
/* Players: index 0 = P1 (port 0), 1 = P2 (port 1, versus only). */
|
|
163
|
+
static uint8_t car_lane[2];
|
|
164
|
+
static uint8_t car_active[2];
|
|
165
|
+
static uint8_t crashes_left[2];
|
|
166
|
+
static uint8_t invuln[2]; /* post-crash blink/no-collide frames */
|
|
167
|
+
static uint8_t prev_pad[2];
|
|
168
|
+
static uint8_t lane_min[2], lane_max[2]; /* 2P: split territories */
|
|
169
|
+
static uint8_t two_player;
|
|
170
|
+
static uint8_t winner; /* versus result: 0 = P1, 1 = P2 */
|
|
114
171
|
|
|
115
|
-
|
|
172
|
+
static uint8_t traffic_alive[MAX_TRAFFIC];
|
|
173
|
+
static uint8_t traffic_lane[MAX_TRAFFIC];
|
|
174
|
+
static uint8_t traffic_y[MAX_TRAFFIC];
|
|
116
175
|
|
|
117
|
-
static
|
|
118
|
-
static
|
|
119
|
-
static
|
|
176
|
+
static uint8_t speed; /* road px/frame, 1-4 */
|
|
177
|
+
static uint16_t dist; /* 1P distance, 1 unit = 16 scrolled px */
|
|
178
|
+
static uint8_t dist_frac;
|
|
179
|
+
static uint16_t best; /* persisted best 1P distance */
|
|
120
180
|
static uint8_t spawn_timer;
|
|
121
|
-
static uint8_t
|
|
122
|
-
static uint8_t
|
|
181
|
+
static uint8_t road_scroll; /* BG scroll_y, ALWAYS kept in 0..239 */
|
|
182
|
+
static uint8_t prev_top_row; /* last streamed nametable row */
|
|
183
|
+
static uint16_t rng = 0xC0DE;
|
|
184
|
+
|
|
185
|
+
/* HUD digit cache — cc65's 16-bit div/mod helpers cost hundreds of cycles
|
|
186
|
+
* each; recompute the 5 digits only when dist actually changes. */
|
|
187
|
+
static uint8_t hud_digits[5];
|
|
188
|
+
static uint16_t hud_cached = 0xFFFF;
|
|
189
|
+
|
|
190
|
+
/* Game states — the shell every example shares: title → play → game over. */
|
|
191
|
+
#define ST_TITLE 0
|
|
192
|
+
#define ST_PLAY 1
|
|
193
|
+
#define ST_OVER 2
|
|
194
|
+
static uint8_t state;
|
|
123
195
|
|
|
124
|
-
|
|
125
|
-
static uint8_t
|
|
196
|
+
/* ── GAME LOGIC (clay) — xorshift16 PRNG (~tens of cycles per call) ── */
|
|
197
|
+
static uint8_t random8(void) {
|
|
198
|
+
uint16_t r = rng;
|
|
199
|
+
r ^= r << 7;
|
|
200
|
+
r ^= r >> 9;
|
|
201
|
+
r ^= r << 8;
|
|
202
|
+
rng = r;
|
|
203
|
+
return (uint8_t)r;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
207
|
+
* Vertical scroll Y-WRAP. A nametable is 32x30 tiles = 240 pixels tall, so
|
|
208
|
+
* vertical scroll wraps at 240, NOT 256. scroll_y values 240-255 make the
|
|
209
|
+
* PPU fetch ATTRIBUTE-table bytes as tile indices — rows of garbage tiles.
|
|
210
|
+
* Plain uint8_t arithmetic happily produces 240-255, so every change to
|
|
211
|
+
* road_scroll goes through this helper. (Scrolling DOWN = the road slides
|
|
212
|
+
* toward the player = scroll_y DECREASES.) The crt0's iNES header sets
|
|
213
|
+
* vertical mirroring, so the nametable below $2000 mirrors $2000 and the
|
|
214
|
+
* wrap is seamless. */
|
|
215
|
+
static void scroll_road_down(uint8_t px) {
|
|
216
|
+
if (road_scroll >= px) road_scroll -= px;
|
|
217
|
+
else road_scroll = (uint8_t)(road_scroll + 240 - px);
|
|
218
|
+
ppu_scroll(0, road_scroll); /* NMI commits scroll_x AND scroll_y */
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
222
|
+
* Streaming-row scenery through the QUEUED tile path. As the road scrolls
|
|
223
|
+
* down, nametable rows re-enter at the top of the screen; the moment row R
|
|
224
|
+
* becomes the top row we restamp its roadside scenery cells with fresh
|
|
225
|
+
* random tiles, so the wrap never shows the same 240px loop twice. Classic
|
|
226
|
+
* streaming-row technique — same trick big scrollers use, just downward.
|
|
227
|
+
* Two hard rules:
|
|
228
|
+
* 1. QUEUED writes only (tile_set) — raw $2007 traffic while rendering
|
|
229
|
+
* corrupts the scroll/address latch. The NMI drains 16 queue entries
|
|
230
|
+
* per vblank; we stamp 4 cells per row crossing, and at max speed (4
|
|
231
|
+
* px/frame) a crossing happens at most every other frame. Stay under
|
|
232
|
+
* the 16/vblank budget when adding cells.
|
|
233
|
+
* 2. The restamped row sits in the overscan-cropped top band (most NTSC
|
|
234
|
+
* displays/cores hide the top 8 scanlines) when the queue commits, so
|
|
235
|
+
* the swap is invisible. Restamp rows anywhere lower and the player
|
|
236
|
+
* sees tiles pop. */
|
|
237
|
+
static void stream_road_row(uint8_t row) {
|
|
238
|
+
uint8_t r;
|
|
239
|
+
r = random8(); tile_set(0, 2, row, (r & 7) == 0 ? BG_TREE : ((r & 3) == 0 ? BG_TUFT : BG_GRASS));
|
|
240
|
+
r = random8(); tile_set(0, 5, row, (r & 7) == 0 ? BG_TREE : ((r & 3) == 0 ? BG_TUFT : BG_GRASS));
|
|
241
|
+
r = random8(); tile_set(0, 26, row, (r & 7) == 0 ? BG_TREE : ((r & 3) == 0 ? BG_TUFT : BG_GRASS));
|
|
242
|
+
r = random8(); tile_set(0, 29, row, (r & 7) == 0 ? BG_TREE : ((r & 3) == 0 ? BG_TUFT : BG_GRASS));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/* AABB, both boxes 8x8. */
|
|
246
|
+
static uint8_t hits(uint8_t ax, uint8_t ay, uint8_t bx, uint8_t by) {
|
|
247
|
+
uint8_t dx = (ax > bx) ? (ax - bx) : (bx - ax);
|
|
248
|
+
uint8_t dy = (ay > by) ? (ay - by) : (by - ay);
|
|
249
|
+
return (dx < 8) && (dy < 8);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/* ── GAME LOGIC (clay) — traffic pool (fixed slots, no allocation) ── */
|
|
253
|
+
static void spawn_traffic(void) {
|
|
254
|
+
uint8_t i;
|
|
255
|
+
for (i = 0; i < MAX_TRAFFIC; i++) {
|
|
256
|
+
if (!traffic_alive[i]) {
|
|
257
|
+
traffic_alive[i] = 1;
|
|
258
|
+
traffic_lane[i] = random8() & 3;
|
|
259
|
+
traffic_y[i] = SPAWN_Y;
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
126
264
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
265
|
+
/* ── GAME LOGIC (clay) — sprite HUD ─────────────────────────────────────────
|
|
266
|
+
* All HUD glyphs are SPRITES on one fixed scanline (see header for why not
|
|
267
|
+
* a BG HUD). 1P: lives digit left + 5-digit distance right = 6 sprites on
|
|
268
|
+
* the line; 2P: one crashes-left digit per player = 2. Traffic spawns below
|
|
269
|
+
* this scanline, so the 8-sprites-per-scanline PPU limit is never hit. */
|
|
270
|
+
static void stage_hud(void) {
|
|
271
|
+
uint8_t i;
|
|
272
|
+
if (two_player) {
|
|
273
|
+
oam_spr(8, HUD_Y, (uint8_t)(TILE_DIGIT0 + crashes_left[0]), PAL_P1);
|
|
274
|
+
oam_spr(240, HUD_Y, (uint8_t)(TILE_DIGIT0 + crashes_left[1]), PAL_P2);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
oam_spr(8, HUD_Y, (uint8_t)(TILE_DIGIT0 + crashes_left[0]), PAL_HUD);
|
|
278
|
+
if (dist != hud_cached) { /* recompute digits only on change */
|
|
279
|
+
uint16_t v = dist;
|
|
280
|
+
for (i = 0; i < 5; i++) { hud_digits[4 - i] = (uint8_t)(v % 10); v /= 10; }
|
|
281
|
+
hud_cached = dist;
|
|
282
|
+
}
|
|
283
|
+
for (i = 0; i < 5; i++)
|
|
284
|
+
oam_spr((uint8_t)(192 + i * 8), HUD_Y, (uint8_t)(TILE_DIGIT0 + hud_digits[i]), PAL_HUD);
|
|
130
285
|
}
|
|
131
286
|
|
|
132
|
-
/*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
#define ROAD_EDGE_L 9
|
|
140
|
-
#define ROAD_EDGE_R 21
|
|
141
|
-
#define ROAD_DIV_1 13
|
|
142
|
-
#define ROAD_DIV_2 17
|
|
143
|
-
static void draw_road(void) {
|
|
144
|
-
uint8_t row, col;
|
|
287
|
+
/* ── GAME LOGIC (clay) — paint the road into nametable 0 ───────────────────
|
|
288
|
+
* Whole-screen paint with the PPU OFF (vram_unsafe_set — the queued path
|
|
289
|
+
* would deadlock with rendering disabled; see TROUBLESHOOTING). The dashed
|
|
290
|
+
* lane lines are painted ONCE and never touched again: they live in the BG,
|
|
291
|
+
* so the scroll moves them with the road for free. */
|
|
292
|
+
static void paint_road(void) {
|
|
293
|
+
uint8_t row, col, tile;
|
|
145
294
|
uint16_t base;
|
|
146
|
-
/* Fill the WHOLE nametable so nothing reads as flat colour: grass on the
|
|
147
|
-
* roadside (outside the shoulders) and a faint tarmac texture on the
|
|
148
|
-
* driving surface. Then stamp the shoulder + lane markings on top. */
|
|
149
295
|
for (row = 0; row < 30; row++) {
|
|
150
296
|
base = (uint16_t)(0x2000 + (uint16_t)row * 32);
|
|
151
297
|
for (col = 0; col < 32; col++) {
|
|
152
|
-
if (col <
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
298
|
+
if (col < COL_EDGE_L || col > COL_EDGE_R) {
|
|
299
|
+
tile = BG_GRASS; /* roadside */
|
|
300
|
+
if (((row * 7 + col * 13) % 31) == 0) tile = BG_TREE;
|
|
301
|
+
else if (((row * 5 + col * 3) % 11) == 0) tile = BG_TUFT;
|
|
302
|
+
} else if (col == COL_EDGE_L || col == COL_EDGE_R) {
|
|
303
|
+
tile = BG_EDGE; /* shoulders */
|
|
304
|
+
} else if (col == COL_DIVIDER) {
|
|
305
|
+
tile = BG_EDGE; /* solid center line */
|
|
306
|
+
} else if (col == COL_DASH_1 || col == COL_DASH_2) {
|
|
307
|
+
tile = BG_DASH; /* dashed lane lines */
|
|
308
|
+
} else {
|
|
309
|
+
tile = (((row * 5 + col * 3) % 13) == 0) ? BG_SPECK : 0; /* tarmac */
|
|
156
310
|
}
|
|
157
|
-
|
|
311
|
+
vram_unsafe_set((uint16_t)(base + col), tile);
|
|
158
312
|
}
|
|
159
313
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/* ── GAME LOGIC (clay) — the title screen ──────────────────────────────────
|
|
317
|
+
* Painted with the PPU OFF (text_draw_unsafe = raw VRAM writes). The road
|
|
318
|
+
* itself is the backdrop; text cells overwrite road cells (font pixels are
|
|
319
|
+
* colour 1 = white over the colour-0 asphalt backdrop). */
|
|
320
|
+
static void paint_title(void) {
|
|
321
|
+
uint8_t i;
|
|
322
|
+
uint16_t v;
|
|
323
|
+
uint8_t d[5];
|
|
324
|
+
ppu_off();
|
|
325
|
+
paint_road();
|
|
326
|
+
text_draw_unsafe(0x2000 + 8 * 32 + ((32 - sizeof(GAME_TITLE) + 1) / 2), GAME_TITLE);
|
|
327
|
+
text_draw_unsafe(0x2000 + 13 * 32 + 10, "1P RACE - A");
|
|
328
|
+
text_draw_unsafe(0x2000 + 15 * 32 + 9, "2P VERSUS - B");
|
|
329
|
+
/* Persistent best line — hand-painted digits (queued text needs rendering
|
|
330
|
+
* on; we're PPU-off here). */
|
|
331
|
+
text_draw_unsafe(0x2000 + 20 * 32 + 10, "BEST");
|
|
332
|
+
v = best;
|
|
333
|
+
for (i = 0; i < 5; i++) { d[i] = (uint8_t)(v % 10); v /= 10; }
|
|
334
|
+
for (i = 0; i < 5; i++)
|
|
335
|
+
vram_unsafe_set((uint16_t)(0x2000 + 20 * 32 + 15 + i), (uint8_t)(0x40 + d[4 - i]));
|
|
336
|
+
road_scroll = 0;
|
|
337
|
+
ppu_scroll(0, 0);
|
|
338
|
+
oam_clear();
|
|
339
|
+
ppu_on_all();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/* ── GAME LOGIC (clay) — the result screen ── */
|
|
343
|
+
static void paint_over(void) {
|
|
344
|
+
ppu_off();
|
|
345
|
+
/* Same road backdrop as the title — a bare single-colour card looks like a
|
|
346
|
+
* render failure (the verify tool flags >92% one-colour frames). */
|
|
347
|
+
paint_road();
|
|
348
|
+
if (two_player) {
|
|
349
|
+
text_draw_unsafe(0x2000 + 10 * 32 + 12, winner ? "P2 WINS" : "P1 WINS");
|
|
350
|
+
text_draw_unsafe(0x2000 + 14 * 32 + 8, "RIVAL CRASHED OUT");
|
|
351
|
+
} else {
|
|
352
|
+
uint8_t i; uint16_t v; uint8_t d[5];
|
|
353
|
+
text_draw_unsafe(0x2000 + 9 * 32 + 12, "WRECKED");
|
|
354
|
+
text_draw_unsafe(0x2000 + 13 * 32 + 9, "DIST");
|
|
355
|
+
text_draw_unsafe(0x2000 + 15 * 32 + 9, "BEST");
|
|
356
|
+
v = dist;
|
|
357
|
+
for (i = 0; i < 5; i++) { d[i] = (uint8_t)(v % 10); v /= 10; }
|
|
358
|
+
for (i = 0; i < 5; i++)
|
|
359
|
+
vram_unsafe_set((uint16_t)(0x2000 + 13 * 32 + 14 + i), (uint8_t)(0x40 + d[4 - i]));
|
|
360
|
+
v = best;
|
|
361
|
+
for (i = 0; i < 5; i++) { d[i] = (uint8_t)(v % 10); v /= 10; }
|
|
362
|
+
for (i = 0; i < 5; i++)
|
|
363
|
+
vram_unsafe_set((uint16_t)(0x2000 + 15 * 32 + 14 + i), (uint8_t)(0x40 + d[4 - i]));
|
|
166
364
|
}
|
|
365
|
+
text_draw_unsafe(0x2000 + 20 * 32 + 9, "START - TITLE");
|
|
366
|
+
road_scroll = 0;
|
|
367
|
+
ppu_scroll(0, 0);
|
|
368
|
+
oam_clear();
|
|
369
|
+
ppu_on_all();
|
|
167
370
|
}
|
|
168
371
|
|
|
169
|
-
|
|
372
|
+
/* ── GAME LOGIC (clay) — start a run ── */
|
|
373
|
+
static void start_game(uint8_t versus) {
|
|
170
374
|
uint8_t i;
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
for (i = 0; i <
|
|
174
|
-
|
|
375
|
+
two_player = versus;
|
|
376
|
+
for (i = 0; i < MAX_TRAFFIC; i++) traffic_alive[i] = 0;
|
|
377
|
+
for (i = 0; i < 2; i++) {
|
|
378
|
+
crashes_left[i] = START_LIVES;
|
|
379
|
+
invuln[i] = 0;
|
|
380
|
+
prev_pad[i] = 0;
|
|
381
|
+
}
|
|
382
|
+
if (versus) {
|
|
383
|
+
car_active[0] = 1; car_active[1] = 1;
|
|
384
|
+
lane_min[0] = 0; lane_max[0] = 1; car_lane[0] = 0; /* P1: left half */
|
|
385
|
+
lane_min[1] = 2; lane_max[1] = 3; car_lane[1] = 3; /* P2: right half */
|
|
386
|
+
speed = SPEED_2P; /* shared road, fixed speed (see header) */
|
|
387
|
+
} else {
|
|
388
|
+
car_active[0] = 1; car_active[1] = 0;
|
|
389
|
+
lane_min[0] = 0; lane_max[0] = 3; car_lane[0] = 1; /* whole road */
|
|
390
|
+
speed = 1;
|
|
391
|
+
}
|
|
392
|
+
dist = 0; dist_frac = 0;
|
|
393
|
+
hud_cached = 0xFFFF;
|
|
175
394
|
spawn_timer = 0;
|
|
176
|
-
|
|
395
|
+
ppu_off();
|
|
396
|
+
paint_road();
|
|
397
|
+
road_scroll = 0;
|
|
398
|
+
prev_top_row = 0;
|
|
399
|
+
ppu_scroll(0, 0);
|
|
400
|
+
oam_clear();
|
|
401
|
+
ppu_on_all();
|
|
402
|
+
state = ST_PLAY;
|
|
177
403
|
}
|
|
178
404
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
405
|
+
static void game_over(void) {
|
|
406
|
+
if (!two_player && dist > best) {
|
|
407
|
+
best = dist;
|
|
408
|
+
/* ── HARDWARE IDIOM (load-bearing) — persists via battery PRG-RAM at
|
|
409
|
+
* $6000; works because the crt0's iNES header sets the BATTERY bit.
|
|
410
|
+
* See nes_runtime.c for the magic+checksum layout. ── */
|
|
411
|
+
hiscore_save(best);
|
|
412
|
+
}
|
|
413
|
+
state = ST_OVER;
|
|
414
|
+
paint_over();
|
|
189
415
|
}
|
|
190
416
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
417
|
+
/* ── GAME LOGIC (clay) — per-player input ───────────────────────────────────
|
|
418
|
+
* LEFT/RIGHT steer between lanes (edge-detected — held d-pad shouldn't
|
|
419
|
+
* machine-gun across the road). 1P only: A/UP accelerate, B/DOWN brake. */
|
|
420
|
+
static void update_player(uint8_t p) {
|
|
421
|
+
uint8_t pad = pad_poll(p);
|
|
422
|
+
uint8_t pressed = (uint8_t)(pad & ~prev_pad[p]);
|
|
423
|
+
prev_pad[p] = pad;
|
|
424
|
+
if (!car_active[p]) return;
|
|
425
|
+
if ((pressed & PAD_LEFT) && car_lane[p] > lane_min[p]) {
|
|
426
|
+
--car_lane[p];
|
|
427
|
+
sound_play_tone(0, 0x120, 5, 2); /* lane tick */
|
|
428
|
+
}
|
|
429
|
+
if ((pressed & PAD_RIGHT) && car_lane[p] < lane_max[p]) {
|
|
430
|
+
++car_lane[p];
|
|
431
|
+
sound_play_tone(0, 0x120, 5, 2);
|
|
432
|
+
}
|
|
433
|
+
if (!two_player) { /* speed is shared — only 1P gets it */
|
|
434
|
+
if ((pressed & (PAD_A | PAD_UP)) && speed < 4) {
|
|
435
|
+
++speed;
|
|
436
|
+
sound_play_tone(1, (uint16_t)(0x140 - speed * 0x30), 7, 4); /* engine */
|
|
437
|
+
}
|
|
438
|
+
if ((pressed & (PAD_B | PAD_DOWN)) && speed > 1) {
|
|
439
|
+
--speed;
|
|
440
|
+
sound_play_tone(1, 0x1C0, 4, 3); /* brake blip */
|
|
199
441
|
}
|
|
200
442
|
}
|
|
443
|
+
if (invuln[p] > 0) --invuln[p];
|
|
201
444
|
}
|
|
202
445
|
|
|
203
|
-
static void
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
(uint8_t)(T_DIGIT0 + digits[i]), 0);
|
|
446
|
+
static void crash(uint8_t p) {
|
|
447
|
+
sound_play_noise(10, 12, 14);
|
|
448
|
+
invuln[p] = 60; /* blink + no-collide grace */
|
|
449
|
+
if (!two_player) speed = 1; /* a wreck kills your momentum */
|
|
450
|
+
if (crashes_left[p] > 0) --crashes_left[p];
|
|
451
|
+
if (crashes_left[p] == 0) {
|
|
452
|
+
winner = (uint8_t)(1 - p); /* versus: the OTHER player wins */
|
|
453
|
+
game_over();
|
|
212
454
|
}
|
|
213
455
|
}
|
|
214
456
|
|
|
215
457
|
void main(void) {
|
|
216
|
-
uint8_t i;
|
|
217
|
-
uint8_t
|
|
458
|
+
uint8_t i, p, pad;
|
|
459
|
+
uint8_t top_row;
|
|
218
460
|
|
|
461
|
+
/* ── HARDWARE IDIOM (load-bearing — see TROUBLESHOOTING) ──
|
|
462
|
+
* Init order: PPU off → CHR upload → palette → nametable (raw writes) →
|
|
463
|
+
* OAM clear → rendering on. CHR/palette/nametable writes REQUIRE the PPU
|
|
464
|
+
* off (raw $2007 traffic during rendering corrupts the address latch
|
|
465
|
+
* mid-frame). The runtime's ppu_off/ppu_on_all pair owns the PPUCTRL/
|
|
466
|
+
* PPUMASK bits — don't poke those registers directly alongside it. */
|
|
219
467
|
ppu_off();
|
|
220
|
-
|
|
221
|
-
chr_ram_upload(
|
|
222
|
-
chr_ram_upload(
|
|
223
|
-
chr_ram_upload(
|
|
224
|
-
chr_ram_upload(
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
chr_ram_upload((uint16_t)(0x1000 +
|
|
229
|
-
chr_ram_upload((uint16_t)(0x1000 +
|
|
230
|
-
|
|
231
|
-
chr_ram_upload((uint16_t)(0x1000 + BG_T_ROAD * 16), bg_tile_road, 16);
|
|
232
|
-
|
|
468
|
+
chr_ram_upload(0x0000, tile_blank, 16);
|
|
469
|
+
chr_ram_upload(TILE_CAR * 16, tile_car, 16);
|
|
470
|
+
chr_ram_upload(TILE_TRAFFIC * 16, tile_traffic, 16);
|
|
471
|
+
chr_ram_upload(TILE_DIGIT0 * 16, tile_digits, sizeof(tile_digits));
|
|
472
|
+
chr_ram_upload((uint16_t)(0x1000 + BG_EDGE * 16), bg_edge, 16);
|
|
473
|
+
chr_ram_upload((uint16_t)(0x1000 + BG_DASH * 16), bg_dash, 16);
|
|
474
|
+
chr_ram_upload((uint16_t)(0x1000 + BG_GRASS * 16), bg_grass, 16);
|
|
475
|
+
chr_ram_upload((uint16_t)(0x1000 + BG_TUFT * 16), bg_tuft, 16);
|
|
476
|
+
chr_ram_upload((uint16_t)(0x1000 + BG_TREE * 16), bg_tree, 16);
|
|
477
|
+
chr_ram_upload((uint16_t)(0x1000 + BG_SPECK * 16), bg_speck, 16);
|
|
478
|
+
font_upload();
|
|
233
479
|
palette_load(palette);
|
|
234
|
-
draw_road(); /* paint the static road while the PPU is off */
|
|
235
|
-
oam_clear();
|
|
236
|
-
ppu_on_all();
|
|
237
480
|
sound_init();
|
|
238
481
|
|
|
239
|
-
|
|
240
|
-
|
|
482
|
+
best = hiscore_load(); /* battery SRAM — 0 on first boot */
|
|
483
|
+
state = ST_TITLE;
|
|
484
|
+
paint_title();
|
|
241
485
|
|
|
242
486
|
for (;;) {
|
|
487
|
+
if (state == ST_TITLE) {
|
|
488
|
+
/* ── GAME LOGIC (clay) — title: A = 1P race, B = 2P versus ── */
|
|
489
|
+
oam_clear();
|
|
490
|
+
ppu_wait_nmi();
|
|
491
|
+
sound_music_tick();
|
|
492
|
+
pad = pad_poll(0);
|
|
493
|
+
if ((pad & PAD_A) && !(prev_pad[0] & PAD_A)) { prev_pad[0] = pad; start_game(0); continue; }
|
|
494
|
+
if ((pad & PAD_B) && !(prev_pad[0] & PAD_B)) { prev_pad[0] = pad; start_game(1); continue; }
|
|
495
|
+
if ((pad & PAD_START) && !(prev_pad[0] & PAD_START)) { prev_pad[0] = pad; start_game(0); continue; }
|
|
496
|
+
prev_pad[0] = pad;
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (state == ST_OVER) {
|
|
501
|
+
/* Result card; START or A returns to the title. */
|
|
502
|
+
oam_clear();
|
|
503
|
+
ppu_wait_nmi();
|
|
504
|
+
sound_music_tick();
|
|
505
|
+
pad = pad_poll(0);
|
|
506
|
+
if ((pad & (PAD_START | PAD_A)) && !(prev_pad[0] & (PAD_START | PAD_A))) {
|
|
507
|
+
state = ST_TITLE;
|
|
508
|
+
paint_title();
|
|
509
|
+
}
|
|
510
|
+
prev_pad[0] = pad;
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/* ── ST_PLAY ─────────────────────────────────────────────────────── */
|
|
515
|
+
|
|
516
|
+
/* ── HARDWARE IDIOM (load-bearing — see TROUBLESHOOTING) ──
|
|
517
|
+
* Stage ALL sprites BEFORE ppu_wait_nmi(). The NMI DMAs shadow OAM →
|
|
518
|
+
* real OAM at the START of vblank, copying whatever shadow OAM holds AT
|
|
519
|
+
* THAT MOMENT. Stage-then-wait; flipping it shows stale/empty sprites.
|
|
520
|
+
* (No sprite-0 split here — the HUD is sprites — so order past that is
|
|
521
|
+
* free; we stage cars first purely so they win sprite-priority ties.) */
|
|
243
522
|
oam_clear();
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (obstacles[i].alive)
|
|
249
|
-
oam_spr(obstacles[i].x, obstacles[i].y, T_CAR_ENEMY, 1);
|
|
523
|
+
for (p = 0; p < 2; p++) {
|
|
524
|
+
if (!car_active[p]) continue;
|
|
525
|
+
if (invuln[p] & 2) continue; /* crash blink: skip odd pairs */
|
|
526
|
+
oam_spr(lane_x[car_lane[p]], CAR_Y, TILE_CAR, p ? PAL_P2 : PAL_P1);
|
|
250
527
|
}
|
|
251
|
-
|
|
528
|
+
for (i = 0; i < MAX_TRAFFIC; i++)
|
|
529
|
+
if (traffic_alive[i]) oam_spr(lane_x[traffic_lane[i]], traffic_y[i], TILE_TRAFFIC, PAL_TRAFFIC);
|
|
530
|
+
stage_hud();
|
|
252
531
|
|
|
253
532
|
ppu_wait_nmi();
|
|
254
|
-
|
|
255
533
|
sound_music_tick();
|
|
256
534
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
535
|
+
/* Scroll the road, then stream scenery into the row that just wrapped
|
|
536
|
+
* into the (overscan-hidden) top band. Both idioms documented above. */
|
|
537
|
+
scroll_road_down(speed);
|
|
538
|
+
top_row = (uint8_t)(road_scroll >> 3);
|
|
539
|
+
if (top_row != prev_top_row) {
|
|
540
|
+
prev_top_row = top_row;
|
|
541
|
+
stream_road_row(top_row);
|
|
264
542
|
}
|
|
265
543
|
|
|
266
|
-
/* ──
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
544
|
+
/* ── GAME LOGIC (clay) from here down ── */
|
|
545
|
+
update_player(0);
|
|
546
|
+
if (two_player) update_player(1);
|
|
547
|
+
if (state != ST_PLAY) continue; /* a crash may have ended the game */
|
|
548
|
+
|
|
549
|
+
/* Distance (1P stat): 1 unit per 16 scrolled pixels. A chime every 256
|
|
550
|
+
* units marks a checkpoint. */
|
|
551
|
+
if (!two_player) {
|
|
552
|
+
dist_frac = (uint8_t)(dist_frac + speed);
|
|
553
|
+
if (dist_frac >= 16) {
|
|
554
|
+
dist_frac -= 16;
|
|
555
|
+
if (dist < 65535u) ++dist;
|
|
556
|
+
if (dist != 0 && (dist & 0xFF) == 0)
|
|
557
|
+
sound_play_tone(0, 0x0D6, 8, 10); /* checkpoint chime (C6) */
|
|
558
|
+
}
|
|
270
559
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
560
|
+
|
|
561
|
+
/* Traffic flows down at road speed (it reads as slower cars you're
|
|
562
|
+
* overtaking); despawn past the bottom with a little pass tick. */
|
|
563
|
+
for (i = 0; i < MAX_TRAFFIC; i++) {
|
|
564
|
+
if (!traffic_alive[i]) continue;
|
|
565
|
+
if (traffic_y[i] >= (uint8_t)(224 - speed)) {
|
|
566
|
+
traffic_alive[i] = 0;
|
|
567
|
+
sound_play_tone(1, 0x0C0, 2, 2);
|
|
568
|
+
} else {
|
|
569
|
+
traffic_y[i] = (uint8_t)(traffic_y[i] + speed);
|
|
570
|
+
}
|
|
274
571
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
/* ── Obstacles slide down ─────────────────────────────────── */
|
|
279
|
-
for (i = 0; i < MAX_OBSTACLES; i++) {
|
|
280
|
-
if (!obstacles[i].alive) continue;
|
|
281
|
-
if (obstacles[i].y < 232) obstacles[i].y += 2;
|
|
282
|
-
else obstacles[i].alive = 0;
|
|
572
|
+
if (++spawn_timer >= SPAWN_PERIOD) {
|
|
573
|
+
spawn_timer = 0;
|
|
574
|
+
spawn_traffic();
|
|
283
575
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
for (i = 0; i <
|
|
288
|
-
if (!
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
576
|
+
|
|
577
|
+
/* Traffic ↔ cars. Crash grace: a just-wrecked car blinks and can't
|
|
578
|
+
* collide for 60 frames. */
|
|
579
|
+
for (i = 0; i < MAX_TRAFFIC; i++) {
|
|
580
|
+
if (!traffic_alive[i]) continue;
|
|
581
|
+
for (p = 0; p < 2; p++) {
|
|
582
|
+
if (!car_active[p] || invuln[p]) continue;
|
|
583
|
+
if (hits(lane_x[traffic_lane[i]], traffic_y[i], lane_x[car_lane[p]], CAR_Y)) {
|
|
584
|
+
traffic_alive[i] = 0;
|
|
585
|
+
crash(p);
|
|
586
|
+
if (state != ST_PLAY) break;
|
|
587
|
+
}
|
|
293
588
|
}
|
|
589
|
+
if (state != ST_PLAY) break;
|
|
294
590
|
}
|
|
295
|
-
|
|
296
|
-
if (score < 65500u) score++;
|
|
297
591
|
}
|
|
298
592
|
}
|