romdevtools 0.27.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.
Files changed (199) hide show
  1. package/AGENTS.md +56 -44
  2. package/CHANGELOG.md +355 -0
  3. package/README.md +4 -4
  4. package/examples/README.md +7 -7
  5. package/examples/atari2600/templates/platformer.asm +1227 -325
  6. package/examples/atari2600/templates/puzzle.asm +1056 -0
  7. package/examples/atari2600/templates/racing.asm +909 -257
  8. package/examples/atari2600/templates/shmup.asm +1035 -218
  9. package/examples/atari2600/templates/sports.asm +1143 -229
  10. package/examples/atari7800/templates/hello_sprite.c +8 -4
  11. package/examples/atari7800/templates/platformer.c +991 -152
  12. package/examples/atari7800/templates/puzzle.c +1091 -145
  13. package/examples/atari7800/templates/racing.c +949 -118
  14. package/examples/atari7800/templates/shmup.c +812 -130
  15. package/examples/atari7800/templates/sports.c +820 -181
  16. package/examples/c64/templates/platformer.c +876 -157
  17. package/examples/c64/templates/puzzle.c +881 -143
  18. package/examples/c64/templates/racing.c +873 -88
  19. package/examples/c64/templates/shmup.c +762 -154
  20. package/examples/c64/templates/sports.c +755 -95
  21. package/examples/gb/templates/platformer.c +841 -175
  22. package/examples/gb/templates/puzzle.c +1094 -176
  23. package/examples/gb/templates/racing.c +761 -169
  24. package/examples/gb/templates/shmup.c +679 -169
  25. package/examples/gb/templates/sports.c +790 -153
  26. package/examples/gba/templates/platformer.c +624 -169
  27. package/examples/gba/templates/puzzle.c +535 -207
  28. package/examples/gba/templates/racing.c +513 -196
  29. package/examples/gba/templates/shmup.c +565 -168
  30. package/examples/gba/templates/sports.c +454 -162
  31. package/examples/gbc/templates/platformer.c +944 -176
  32. package/examples/gbc/templates/puzzle.c +1131 -177
  33. package/examples/gbc/templates/racing.c +891 -175
  34. package/examples/gbc/templates/shmup.c +827 -179
  35. package/examples/gbc/templates/sports.c +870 -156
  36. package/examples/genesis/templates/platformer.c +747 -129
  37. package/examples/genesis/templates/puzzle.c +702 -208
  38. package/examples/genesis/templates/racing.c +728 -193
  39. package/examples/genesis/templates/shmup.c +535 -142
  40. package/examples/genesis/templates/shmup_2p.c +13 -1
  41. package/examples/genesis/templates/sports.c +495 -158
  42. package/examples/gg/templates/platformer.c +883 -214
  43. package/examples/gg/templates/puzzle.c +906 -181
  44. package/examples/gg/templates/racing.c +919 -160
  45. package/examples/gg/templates/shmup.c +716 -177
  46. package/examples/gg/templates/sports.c +735 -128
  47. package/examples/lynx/templates/platformer.c +604 -50
  48. package/examples/lynx/templates/puzzle.c +533 -130
  49. package/examples/lynx/templates/racing.c +538 -102
  50. package/examples/lynx/templates/shmup.c +461 -122
  51. package/examples/lynx/templates/sports.c +496 -69
  52. package/examples/msx/platformer/main.c +648 -159
  53. package/examples/msx/puzzle/main.c +750 -185
  54. package/examples/msx/racing/main.c +669 -177
  55. package/examples/msx/shmup/main.c +460 -177
  56. package/examples/msx/sports/main.c +591 -124
  57. package/examples/nes/templates/platformer.c +586 -160
  58. package/examples/nes/templates/puzzle.c +603 -222
  59. package/examples/nes/templates/racing.c +505 -197
  60. package/examples/nes/templates/shmup.c +339 -144
  61. package/examples/nes/templates/sports.c +341 -182
  62. package/examples/pce/platformer/main.c +875 -204
  63. package/examples/pce/puzzle/main.c +797 -216
  64. package/examples/pce/racing/main.c +782 -206
  65. package/examples/pce/shmup/main.c +638 -211
  66. package/examples/pce/sports/main.c +585 -167
  67. package/examples/porting-across-platforms/README.md +1 -1
  68. package/examples/sms/templates/platformer.c +765 -176
  69. package/examples/sms/templates/puzzle.c +783 -177
  70. package/examples/sms/templates/racing.c +812 -133
  71. package/examples/sms/templates/shmup.c +601 -148
  72. package/examples/sms/templates/shmup_2p.c +17 -1
  73. package/examples/sms/templates/sports.c +633 -121
  74. package/examples/snes/templates/music_demo.c +7 -0
  75. package/examples/snes/templates/platformer-data.asm +123 -24
  76. package/examples/snes/templates/platformer-hdr.asm +57 -0
  77. package/examples/snes/templates/platformer.c +587 -149
  78. package/examples/snes/templates/puzzle-data.asm +116 -21
  79. package/examples/snes/templates/puzzle-hdr.asm +57 -0
  80. package/examples/snes/templates/puzzle.c +632 -185
  81. package/examples/snes/templates/racing-data.asm +390 -32
  82. package/examples/snes/templates/racing-hdr.asm +57 -0
  83. package/examples/snes/templates/racing.c +807 -177
  84. package/examples/snes/templates/shmup-data.asm +87 -29
  85. package/examples/snes/templates/shmup-hdr.asm +57 -0
  86. package/examples/snes/templates/shmup.c +459 -180
  87. package/examples/snes/templates/sports-data.asm +48 -2
  88. package/examples/snes/templates/sports-hdr.asm +57 -0
  89. package/examples/snes/templates/sports.c +414 -156
  90. package/package.json +12 -12
  91. package/src/cores/wasm/bluemsx_libretro.js +1 -1
  92. package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
  93. package/src/cores/wasm/fceumm_libretro.js +1 -1
  94. package/src/cores/wasm/fceumm_libretro.wasm +0 -0
  95. package/src/cores/wasm/gambatte_libretro.js +1 -1
  96. package/src/cores/wasm/gambatte_libretro.wasm +0 -0
  97. package/src/cores/wasm/geargrafx_libretro.js +1 -1
  98. package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
  99. package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
  100. package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
  101. package/src/cores/wasm/handy_libretro.js +1 -1
  102. package/src/cores/wasm/handy_libretro.wasm +0 -0
  103. package/src/cores/wasm/mgba_libretro.js +1 -1
  104. package/src/cores/wasm/mgba_libretro.wasm +0 -0
  105. package/src/cores/wasm/prosystem_libretro.js +1 -1
  106. package/src/cores/wasm/prosystem_libretro.wasm +0 -0
  107. package/src/cores/wasm/snes9x_libretro.js +1 -1
  108. package/src/cores/wasm/snes9x_libretro.wasm +0 -0
  109. package/src/cores/wasm/stella2014_libretro.js +1 -1
  110. package/src/cores/wasm/stella2014_libretro.wasm +0 -0
  111. package/src/cores/wasm/vice_x64_libretro.js +1 -1
  112. package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
  113. package/src/host/LibretroHost.js +304 -11
  114. package/src/http/tool-registry.js +11 -11
  115. package/src/mcp/server.js +6 -0
  116. package/src/mcp/tools/cheats.js +2 -1
  117. package/src/mcp/tools/disasm-rebuild.js +315 -65
  118. package/src/mcp/tools/disasm.js +149 -28
  119. package/src/mcp/tools/find-references.js +216 -51
  120. package/src/mcp/tools/frame.js +14 -6
  121. package/src/mcp/tools/index.js +18 -4
  122. package/src/mcp/tools/input.js +31 -7
  123. package/src/mcp/tools/lifecycle.js +6 -4
  124. package/src/mcp/tools/memory.js +208 -39
  125. package/src/mcp/tools/platform-docs.js +1 -1
  126. package/src/mcp/tools/playtest.js +56 -4
  127. package/src/mcp/tools/preview-tile.js +6 -2
  128. package/src/mcp/tools/project.js +1114 -120
  129. package/src/mcp/tools/rom-id.js +5 -1
  130. package/src/mcp/tools/run-until.js +4 -2
  131. package/src/mcp/tools/snippets.js +6 -6
  132. package/src/mcp/tools/sprite-pipeline.js +14 -2
  133. package/src/mcp/tools/state.js +2 -1
  134. package/src/mcp/tools/tile-inspect.js +8 -1
  135. package/src/mcp/tools/toolchain.js +55 -11
  136. package/src/mcp/tools/watch-memory.js +145 -27
  137. package/src/observer/bus.js +73 -0
  138. package/src/observer/livestream.html +4 -2
  139. package/src/observer/tool-wrap.js +17 -14
  140. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +64 -17
  141. package/src/platforms/atari2600/MENTAL_MODEL.md +5 -1
  142. package/src/platforms/atari2600/TROUBLESHOOTING.md +40 -0
  143. package/src/platforms/atari7800/MENTAL_MODEL.md +32 -11
  144. package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
  145. package/src/platforms/c64/MENTAL_MODEL.md +11 -4
  146. package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
  147. package/src/platforms/gb/MENTAL_MODEL.md +19 -4
  148. package/src/platforms/gb/TROUBLESHOOTING.md +101 -6
  149. package/src/platforms/gb/lib/c/README.md +10 -11
  150. package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
  151. package/src/platforms/gb/lib/c/patch-header.js +19 -6
  152. package/src/platforms/gba/MENTAL_MODEL.md +4 -4
  153. package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
  154. package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
  155. package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
  156. package/src/platforms/gbc/MENTAL_MODEL.md +16 -4
  157. package/src/platforms/gbc/TROUBLESHOOTING.md +24 -3
  158. package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
  159. package/src/platforms/gbc/lib/c/README.md +10 -11
  160. package/src/platforms/gbc/lib/c/font.h +43 -0
  161. package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
  162. package/src/platforms/gbc/lib/c/patch-header.js +19 -6
  163. package/src/platforms/genesis/MENTAL_MODEL.md +43 -9
  164. package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
  165. package/src/platforms/genesis/lib/c/genesis_sfx.c +37 -0
  166. package/src/platforms/genesis/lib/c/genesis_sfx.h +1 -0
  167. package/src/platforms/gg/MENTAL_MODEL.md +4 -4
  168. package/src/platforms/gg/TROUBLESHOOTING.md +14 -18
  169. package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
  170. package/src/platforms/gg/lib/c/gg_crt0.s +14 -2
  171. package/src/platforms/gg/lib/c/joypad_read.c +29 -0
  172. package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
  173. package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
  174. package/src/platforms/lynx/lib/c/lynx_sfx.c +38 -2
  175. package/src/platforms/lynx/lib/c/lynx_sfx.h +1 -0
  176. package/src/platforms/msx/MENTAL_MODEL.md +11 -5
  177. package/src/platforms/msx/TROUBLESHOOTING.md +21 -0
  178. package/src/platforms/msx/lib/c/msx_crt0.s +27 -0
  179. package/src/platforms/msx/lib/c/msx_hw.h +3 -0
  180. package/src/platforms/msx/lib/c/msx_vdp.c +70 -0
  181. package/src/platforms/nes/MENTAL_MODEL.md +12 -5
  182. package/src/platforms/nes/lib/c/nes_runtime.c +190 -34
  183. package/src/platforms/nes/lib/c/nes_runtime.h +35 -0
  184. package/src/platforms/pce/MENTAL_MODEL.md +14 -5
  185. package/src/platforms/pce/TROUBLESHOOTING.md +9 -0
  186. package/src/platforms/pce/lib/c/pce_hw.h +13 -1
  187. package/src/platforms/pce/lib/c/pce_sound.c +22 -0
  188. package/src/platforms/pce/lib/c/pce_video.c +32 -0
  189. package/src/platforms/sms/MENTAL_MODEL.md +11 -6
  190. package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
  191. package/src/platforms/sms/lib/c/sms_crt0.s +14 -2
  192. package/src/platforms/snes/MENTAL_MODEL.md +7 -2
  193. package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
  194. package/src/playtest/playtest.js +73 -3
  195. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
  196. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
  197. package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
  198. package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
  199. package/src/toolchains/index.js +64 -19
@@ -1,251 +1,568 @@
1
- /* ── racing.c — Game Boy Advance Tonc top-down racing scaffold ──────
1
+ /* ── racing.c — Game Boy Advance top-down road racer (complete game) ──────────
2
2
  *
3
- * Endless top-down lane racer. Player car at the bottom of the screen,
4
- * three lanes, obstacles spawn from the top and slide down. Left/Right
5
- * D-pad switches lanes. Survive as long as possible — score is the
6
- * frame count since the last collision.
3
+ * VERGE PILOT a COMPLETE, working game: press-start title, a 1P endless
4
+ * top-down road race, music + SFX, vivid 15-bit colour, and a persistent BEST
5
+ * DISTANCE in cartridge SRAM. The GBA signature on show is the console's
6
+ * "Mode-7" trick: the ROAD is an AFFINE BACKGROUND (BG2, Mode 1) that the PPU
7
+ * rotates and scales per frame for free — so the road SCROLLS toward you
8
+ * (recedes), SCALES with your speed (faster = the road rushes up bigger), and
9
+ * BANKS as you steer (the whole strip tilts into the turn). That receding,
10
+ * banking affine road is the natural GBA racer showcase — the handheld cousin
11
+ * of the SNES Mode-7 racer, done with GBA affine BG hardware.
7
12
  *
8
- * Game state:
9
- * - Player car (1 sprite) at fixed Y, X = lane_x[lane]
10
- * - 4 obstacle cars (object pool), spawning from a random lane
11
- * - Speed grows slightly with score
12
- * - On collision: 60-frame freeze then run resets
13
+ * THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
14
+ * very different one. The markers tell you what's what:
15
+ * HARDWARE IDIOM (load-bearing) dodges a documented GBA footgun; reshape
16
+ * your gameplay around it (see TROUBLESHOOTING before changing).
17
+ * GAME LOGIC (clay) road art, traffic, speeds, tuning: reshape freely.
18
+ *
19
+ * What depends on what:
20
+ * gba_sfx.{h,c} — PSG sound: sfx_tone/sfx_noise one-shots + the music loop
21
+ * (sfx_music_tick once per frame — forget it and the game is silent).
22
+ * libtonc (the build links it) — VBlankIntrWait/key_poll/TTE/lu_sin/lu_cos/
23
+ * tonccpy and the affine-BG matrix registers (REG_BG2PA..PD, BG2X/Y).
24
+ *
25
+ * HANDHELD, SO SINGLE-PLAYER ONLY (honest note): 2P versus on the GBA means a
26
+ * link cable between two units — a second emulator instance this environment
27
+ * can't provide. So VERGE PILOT is a 1P ENDLESS racer chasing your own best
28
+ * distance, not split-screen versus. (Contrast the NES/Genesis racing
29
+ * templates, which ARE 2P versus — two controllers on one machine.)
30
+ *
31
+ * THE AFFINE CHOICE (read before reshaping): a FULL per-scanline perspective
32
+ * floor (a true Mode-7 "ground plane" where each screen row samples the road
33
+ * at a different scale) needs an HBlank-IRQ table that rewrites the matrix 160
34
+ * times a frame — powerful but heavy, and a distraction in a starter. VERGE
35
+ * PILOT takes the HONEST, lighter showcase: ONE affine matrix per frame that
36
+ * (1) scrolls the road texture downward (it recedes toward the horizon as you
37
+ * drive), (2) scales it with your speed, and (3) rotates/banks it as you
38
+ * steer. The matrix is provably non-identity and the scale+bank are visible on
39
+ * screen — the affine hardware is genuinely doing the work. Want the full
40
+ * floor later? Drive road_apply() from an HBlank handler with a per-row scale.
13
41
  */
14
42
 
15
43
  #include <tonc.h>
16
44
  #include "gba_sfx.h"
17
45
 
18
- /* draw a 5-digit score WITHOUT tte_printf (broken in this libtonc — GBA-1). */
19
- static void draw_score(int x, unsigned v) {
46
+ /* The title screen renders this examples({op:'fork'}) stamps your game's
47
+ * name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
48
+ #define GAME_TITLE "VERGE PILOT"
49
+
50
+ /* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
51
+ * Road / car geometry + race tuning. The road is an affine BG2 strip; the
52
+ * player car and traffic are sprites that live in SCREEN space over it. */
53
+ #define SCREEN_W 240
54
+ #define SCREEN_H 160
55
+ #define ROAD_X0 56 /* left edge of the asphalt (screen px) */
56
+ #define ROAD_X1 184 /* right edge of the asphalt */
57
+ #define CAR_Y 128 /* player car's fixed screen Y (near bottom) */
58
+ #define CAR_SLOTS 5 /* discrete lanes the car steers between */
59
+ #define MAX_TRAFFIC 4 /* obstacle cars on the road at once */
60
+ #define SPEED_MIN 1 /* road px/frame */
61
+ #define SPEED_MAX 6
62
+ #define START_SPEED 2
63
+
64
+ /* Sprite slot discipline (128 OAM entries; we use 5):
65
+ * 0 → player car
66
+ * 1..4 → traffic */
67
+ #define SLOT_CAR 0
68
+ #define SLOT_TRAFFIC 1
69
+
70
+ #define TILE_CAR 1 /* OBJ tile 1 = player car (4bpp 8x8) */
71
+ #define TILE_RIVAL 2 /* OBJ tile 2 = oncoming/rival car */
72
+
73
+ /* 4bpp sprite tiles (8 rows × 32 bits; each nibble is a palette index within
74
+ * the sprite's palbank. Index 0 = transparent). */
75
+ static const u32 tile_car[8] = { /* your car, nose up, bright cockpit */
76
+ 0x00133100, 0x01333310, 0x13322331, 0x13333331,
77
+ 0x13333331, 0x13311331, 0x13000031, 0x01000010,
78
+ };
79
+ static const u32 tile_rival[8] = { /* traffic, tail up (you overtake it) */
80
+ 0x01000010, 0x13000031, 0x13311331, 0x13333331,
81
+ 0x13333331, 0x13322331, 0x01333310, 0x00133100,
82
+ };
83
+
84
+ /* ── GAME LOGIC (clay — reshape freely) — game state (plain BSS).
85
+ * NOTE for headless verification: unlike the Genesis template (whose work-RAM
86
+ * globals are readable by symbol name), the GBA libretro core exposes NO
87
+ * IWRAM/EWRAM region, so a headless agent reads game state from what's ON
88
+ * HARDWARE — OAM (the cars), the BG2 affine matrix registers (the road), TTE
89
+ * ink pixels (the screen/HUD), and save_ram (the record). Keep game globals
90
+ * static and surface anything the harness must read onto hardware. */
91
+ #define ST_TITLE 0
92
+ #define ST_PLAY 1
93
+ #define ST_OVER 2
94
+ static u8 state;
95
+
96
+ static s16 car_slot; /* 0..CAR_SLOTS-1, the player's lane */
97
+ static s16 car_x; /* eased screen-x of the car (smooth steer) */
98
+ static u8 speed; /* road px/frame, SPEED_MIN..SPEED_MAX */
99
+ static u8 lives; /* crashes left */
100
+ static u8 invuln; /* post-crash blink / no-collide frames */
101
+ static u16 dist; /* distance travelled (the score) */
102
+ static u16 dist_sub; /* subcounter: 16 scrolled px = +1 distance */
103
+ static u16 best; /* battery-backed best distance — SRAM idiom */
104
+ static u8 new_best; /* result screen shows NEW BEST */
105
+ static u16 road_scroll; /* BG2 texture Y offset (drives the recede) */
106
+ static s16 bank; /* current road bank angle bias (steer lean) */
107
+
108
+ /* Traffic pool — fixed slots, no allocation. Each car has a lane + a SCREEN y
109
+ * that grows downward (it approaches from the top of the road and slides past
110
+ * the player), and an `alive` flag. */
111
+ static u8 tr_alive[MAX_TRAFFIC];
112
+ static u8 tr_slot[MAX_TRAFFIC];
113
+ static s16 tr_y[MAX_TRAFFIC];
114
+ static u8 spawn_timer;
115
+
116
+ #define START_LIVES 3
117
+
118
+ /* ── GAME LOGIC (clay) — xorshift16 PRNG. The GBA is deterministic; without a
119
+ * noise source the traffic would spawn in a fixed lockstep pattern that an
120
+ * idle run could memorise. The PRNG scatters spawn lanes/timing so each run is
121
+ * fresh. ── */
122
+ static u16 rng = 0xC0A7;
123
+ static u8 random8(void) {
124
+ u16 r = rng;
125
+ r ^= r << 7;
126
+ r ^= r >> 9;
127
+ r ^= r << 8;
128
+ rng = r;
129
+ return (u8)r;
130
+ }
131
+
132
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
133
+ * PERSISTENT SRAM at 0x0E000000. Two footguns, both fatal-but-silent:
134
+ * 1. The SRAM bus is 8 BITS WIDE. Byte reads/writes only — a u16/u32
135
+ * access doesn't fault, it just reads the same byte mirrored (and a
136
+ * wide write stores one byte), so your data "almost" round-trips and
137
+ * then the checksum never matches. Every access below is via vu8.
138
+ * 2. Emulators and flashcarts detect the SAVE TYPE by scanning the ROM
139
+ * image for a marker string. Without "SRAM_V" in the ROM, mGBA gives
140
+ * the cart NO save memory at all and writes to 0x0E000000 vanish.
141
+ * The aligned, (used)-attributed const below plants that marker —
142
+ * delete it and persistence dies even though this code is untouched.
143
+ * Layout: 'V' 'X' best-lo best-hi checksum (xor ^ 0xA5) — magic+checksum so a
144
+ * fresh (0xFF-filled) cart reads as "no record" instead of garbage.
145
+ * PERSISTENCE CHOICE: an endless racer's natural chase-stat is the longest
146
+ * DISTANCE survived — exactly what a returning player tries to beat.
147
+ * requires: nothing else — self-contained; safe to transplant whole. */
148
+ #define SRAM_BYTE ((volatile u8 *)0x0E000000)
149
+ __attribute__((used, aligned(4))) static const char sram_type_marker[] = "SRAM_V113";
150
+
151
+ static u16 best_load(void) {
152
+ u8 lo, hi;
153
+ if (SRAM_BYTE[0] != 'V' || SRAM_BYTE[1] != 'X') return 0;
154
+ lo = SRAM_BYTE[2];
155
+ hi = SRAM_BYTE[3];
156
+ if (SRAM_BYTE[4] != (u8)(lo ^ hi ^ 0xA5)) return 0;
157
+ return (u16)(lo | (hi << 8));
158
+ }
159
+
160
+ static void best_save(u16 v) {
161
+ SRAM_BYTE[0] = 'V';
162
+ SRAM_BYTE[1] = 'X';
163
+ SRAM_BYTE[2] = (u8)v;
164
+ SRAM_BYTE[3] = (u8)(v >> 8);
165
+ SRAM_BYTE[4] = (u8)((u8)v ^ (u8)(v >> 8) ^ 0xA5);
166
+ }
167
+
168
+ /* ── GAME LOGIC (clay) — TTE text helpers ────────────────────────────────────
169
+ * Draw right-aligned decimal digits at pixel (x,y) WITHOUT tte_printf. The
170
+ * bundled libtonc's tte_printf with a %d conversion is broken (it routes
171
+ * through a vsnprintf path that isn't wired in this build — it garbles output
172
+ * AND wedges the loop when called per-frame, GBA-1). We build the string
173
+ * ourselves and use tte_write, which processes the #{P:x,y} position command
174
+ * but does NO format conversion → safe every frame. */
175
+ static void draw_num(int x, int y, unsigned v, int digits) {
20
176
  char buf[24];
21
177
  int i, n = 0;
22
- buf[n++]='#'; buf[n++]='{'; buf[n++]='P'; buf[n++]=':';
23
- if (x >= 100) buf[n++] = '0' + (x/100)%10;
24
- if (x >= 10) buf[n++] = '0' + (x/10)%10;
25
- buf[n++] = '0' + x%10;
26
- buf[n++]=','; buf[n++]='8'; buf[n++]='}';
27
- for (i = 4; i >= 0; i--) { buf[n+i] = '0' + (v % 10); v /= 10; }
28
- n += 5; buf[n] = 0;
178
+ buf[n++] = '#'; buf[n++] = '{'; buf[n++] = 'P'; buf[n++] = ':';
179
+ if (x >= 100) buf[n++] = (char)('0' + (x / 100) % 10);
180
+ if (x >= 10) buf[n++] = (char)('0' + (x / 10) % 10);
181
+ buf[n++] = (char)('0' + x % 10);
182
+ buf[n++] = ',';
183
+ if (y >= 100) buf[n++] = (char)('0' + (y / 100) % 10);
184
+ if (y >= 10) buf[n++] = (char)('0' + (y / 10) % 10);
185
+ buf[n++] = (char)('0' + y % 10);
186
+ buf[n++] = '}';
187
+ for (i = digits - 1; i >= 0; i--) { buf[n + i] = (char)('0' + (v % 10)); v /= 10; }
188
+ n += digits; buf[n] = 0;
29
189
  tte_write(buf);
30
190
  }
31
191
 
32
- #define LANE_LEFT_X 56
33
- #define LANE_MID_X 116
34
- #define LANE_RIGHT_X 176
35
- #define PLAYER_Y 140
36
- #define MAX_OBSTACLES 4
192
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
193
+ * AFFINE ROAD (BG2, Mode 1) — the GBA's "Mode 7": one background the PPU
194
+ * rotates/scales per frame for free. THIS is the racer's whole visual hook.
195
+ * Four matrix registers + one reference point do the work:
196
+ *
197
+ * REG_BG2PA..PD — a 2x2 matrix in 8.8 FIXED POINT (256 == 1.0) that maps
198
+ * SCREEN pixels → TEXTURE pixels: tex = P · (screen - origin) + ref.
199
+ * Because it maps screen→texture (the INVERSE of "how is the image
200
+ * transformed"), a matrix that SAMPLES 2 texture-px per screen-px makes
201
+ * the image look HALF size: bigger pa = smaller image. To zoom IN by z
202
+ * write 1/z; to rotate the image one way, write the matrix of the other.
203
+ * REG_BG2X/Y — the texture point sampled at screen pixel (0,0), 20.8 fixed.
204
+ * We ADVANCE BG2Y by the scroll each frame so the road texture slides
205
+ * downward → the road RECEDES toward the horizon as you drive. To pivot
206
+ * the rotation/scale around the screen centre (cx,cy)=(120,120, near the
207
+ * car) anchored at texture point (tx,ty): BG2X = (tx<<8) - (pa*cx+pb*cy)
208
+ * (same shape for Y with pc/pd) — "walk back from the anchor by half a
209
+ * screen through the matrix".
210
+ *
211
+ * The math (libtonc's bg_aff_rotscale does the same). lu_sin/lu_cos take a u16
212
+ * angle (full circle = 0x10000), return 4.12 fixed → >>4 to 8.8. For bank θ
213
+ * and zoom z (8.8):
214
+ * inv = 65536/z (8.8 reciprocal: 1/z)
215
+ * pa = cos·inv>>8 pb = -sin·inv>>8
216
+ * pc = sin·inv>>8 pd = cos·inv>>8
217
+ *
218
+ * Footguns this block already dodges:
219
+ * - These 6 registers are WRITE-ONLY. You cannot read-modify-update; keep
220
+ * your angle/zoom in variables (bank/road_scroll) and rewrite ALL of them
221
+ * every frame.
222
+ * - Affine BGs are ALWAYS 8bpp, and the map is 1 BYTE per tile (no flip
223
+ * bits, no palbank — plain tile index), unlike regular BGs' u16 entries.
224
+ * - VRAM IGNORES BYTE WRITES (a u8 store writes the byte TWICE into the
225
+ * 16-bit lane). Build tiles/map in a work-RAM buffer and tonccpy() it —
226
+ * tonccpy is VRAM-safe.
227
+ * - BG_WRAP makes the 256x256 texture tile forever; without it the road
228
+ * runs out and everything past the edge renders as tile 0.
229
+ * requires: DCNT_MODE1 (BG2 affine there), BG2CNT → CBB 1 / SBB 26,
230
+ * road_apply() called every frame, BG palette indices 224..230 (bank 14 —
231
+ * bank 15 belongs to TTE; see the palette footgun at road_build). */
232
+ static void road_apply(u16 theta, u32 zoom_q8, u16 scroll_y) {
233
+ s32 inv = (s32)(65536u / zoom_q8); /* 8.8 ── 1/zoom */
234
+ s32 cc = ((lu_cos(theta) >> 4) * inv) >> 8; /* 8.8 ── cosθ/zoom */
235
+ s32 ss = ((lu_sin(theta) >> 4) * inv) >> 8; /* 8.8 ── sinθ/zoom */
236
+ REG_BG2PA = (s16)cc; REG_BG2PB = (s16)-ss;
237
+ REG_BG2PC = (s16)ss; REG_BG2PD = (s16)cc;
238
+ /* Pivot around (120,120) — under the car — anchored at texture (128, ty).
239
+ * ty advances with scroll_y so the road slides toward the player. */
240
+ {
241
+ s32 ty = (s32)scroll_y;
242
+ REG_BG2X = (128 << 8) - (cc * 120 + (-ss) * 120);
243
+ REG_BG2Y = ((128 + ty) << 8) - (ss * 120 + cc * 120);
244
+ }
245
+ }
37
246
 
38
- #define TILE_CAR_P1 1
39
- #define TILE_CAR_EN 2
247
+ /* ── GAME LOGIC (clay) — the road ART (the idiom above is the machinery; this
248
+ * is just what the texture looks like — replace at will).
249
+ * 8bpp tiles are 64 bytes, 1 byte per pixel, row-major. We stage 6 tiles + the
250
+ * 32x32 one-byte-per-entry map in work RAM, then tonccpy to VRAM (CBB 1 tiles,
251
+ * SBB 26 map) per the byte-write footgun above. The texture needs VERTICAL
252
+ * content (lane dashes that run up the strip, a centre line) so the scroll
253
+ * reads as forward motion, plus LATERAL structure (shoulders, grass) so the
254
+ * bank-rotation reads as a tilt.
255
+ * PALETTE FOOTGUN: an 8bpp BG indexes the FULL 256-color BG palette, and
256
+ * tte_init_chr4c_default OWNS BANK 15 (240-255: ink 241 = yellow). Park 8bpp
257
+ * road colours in bank 14 (224..) or the road turns ink-yellow the moment TTE
258
+ * initialises. */
259
+ #define RC 224 /* road colours live at 224..230 — clear of TTE's bank 15 */
260
+ #define T_GRASS 0
261
+ #define T_SHOULDER 1
262
+ #define T_ASPHALT 2
263
+ #define T_DASH 3
264
+ #define T_CENTER 4
265
+ #define T_SPECK 5
266
+ static void road_build(void) {
267
+ static u8 tiles[6][64];
268
+ static u8 rmap[1024];
269
+ int x, y, tx, ty;
40
270
 
41
- static const u32 tile_car_p1[8] = {
42
- 0x01111110, 0x11111111, 0x12222221, 0x11111111,
43
- 0x11111111, 0x12222221, 0x11111111, 0x01100110,
44
- };
45
- static const u32 tile_car_enemy[8] = {
46
- 0x03333330, 0x33333333, 0x34444443, 0x33333333,
47
- 0x33333333, 0x34444443, 0x33333333, 0x03300330,
48
- };
271
+ pal_bg_mem[RC + 0] = RGB15(3, 14, 5); /* grass green (vivid) */
272
+ pal_bg_mem[RC + 1] = RGB15(6, 20, 8); /* grass highlight */
273
+ pal_bg_mem[RC + 2] = RGB15(7, 7, 9); /* asphalt dark */
274
+ pal_bg_mem[RC + 3] = RGB15(11, 11, 13); /* asphalt light (dither) */
275
+ pal_bg_mem[RC + 4] = RGB15(31, 31, 22); /* lane dash (warm white) */
276
+ pal_bg_mem[RC + 5] = RGB15(31, 18, 4); /* centre line (hot amber) */
277
+ pal_bg_mem[RC + 6] = RGB15(28, 30, 31); /* shoulder (near-white) */
49
278
 
50
- /* ── Road backdrop tiles (4bpp) ──────────────────────────────────────
51
- * Fill the whole BG0 with a road scene instead of a flat black screen:
52
- * tile 4 = grass roadside (palette index 5, with darker tufts 6)
53
- * tile 5 = asphalt road (palette index 7)
54
- * tile 6 = asphalt + a centred white lane dash (index 8)
55
- * tile 7 = road edge line (white index 8 down the left column) */
56
- static const u32 tile_grass[8] = {
57
- 0x55555555, 0x55655555, 0x55555555, 0x55556555,
58
- 0x55555555, 0x65555555, 0x55555555, 0x55555655,
59
- };
60
- static const u32 tile_road[8] = {
61
- 0x77777777, 0x77777777, 0x77777777, 0x77777777,
62
- 0x77777777, 0x77777777, 0x77777777, 0x77777777,
63
- };
64
- static const u32 tile_road_dash[8] = {
65
- 0x77877877, 0x77877877, 0x77877877, 0x77877877,
66
- 0x77777777, 0x77777777, 0x77777777, 0x77777777,
67
- };
68
- static const u32 tile_road_edge[8] = {
69
- 0x87777778, 0x87777778, 0x87777778, 0x87777778,
70
- 0x87777778, 0x87777778, 0x87777778, 0x87777778,
71
- };
279
+ for (y = 0; y < 8; y++)
280
+ for (x = 0; x < 8; x++) {
281
+ tiles[T_GRASS][y * 8 + x] = (u8)(((x + y) & 3) ? RC : RC + 1);
282
+ tiles[T_SHOULDER][y * 8 + x] = (u8)(RC + 6);
283
+ tiles[T_ASPHALT][y * 8 + x] = (u8)(((x * 3 + y * 5) % 7) ? RC + 2 : RC + 3);
284
+ /* dash: a fat vertical stripe, dashed every other tile-row band */
285
+ tiles[T_DASH][y * 8 + x] = (u8)((x >= 3 && x <= 4 && y < 5) ? RC + 4 : RC + 2);
286
+ tiles[T_CENTER][y * 8 + x] = (u8)((x >= 3 && x <= 4) ? RC + 5 : RC + 2);
287
+ tiles[T_SPECK][y * 8 + x] = (u8)(((x == 2 && y == 5) || (x == 6 && y == 2)) ? RC + 3 : RC + 2);
288
+ }
72
289
 
73
- typedef struct { s16 x, y; u16 alive; } Car;
290
+ /* Map: a 32x32 (256x256) road texture. A central asphalt band (cols 9..22)
291
+ * with grass shoulders outside, a hot centre line down the middle, and
292
+ * dashed lane lines either side of centre — VERTICAL content so the
293
+ * downward scroll reads as forward motion, lateral content so the bank
294
+ * rotation reads as a tilt. WRAP makes it an endless road. */
295
+ for (ty = 0; ty < 32; ty++)
296
+ for (tx = 0; tx < 32; tx++) {
297
+ u8 t;
298
+ if (tx < 9 || tx > 22) t = T_GRASS; /* grass shoulders */
299
+ else if (tx == 9 || tx == 22) t = T_SHOULDER; /* white shoulders */
300
+ else if (tx == 15 || tx == 16) t = T_CENTER; /* centre line */
301
+ else if (tx == 12 || tx == 19) t = (ty & 1) ? T_DASH : T_ASPHALT; /* dashes */
302
+ else t = ((tx * 5 + ty * 3) % 13 == 0) ? T_SPECK : T_ASPHALT;
303
+ rmap[ty * 32 + tx] = t;
304
+ }
74
305
 
75
- static OBJ_ATTR obj_buffer[128];
306
+ tonccpy(&tile8_mem[1][0], tiles, sizeof(tiles)); /* tiles → charblock 1 */
307
+ tonccpy(se_mem[26], rmap, sizeof(rmap)); /* map → screenblock 26 */
308
+ REG_BG2CNT = BG_CBB(1) | BG_SBB(26) | BG_AFF_32x32 | BG_WRAP | BG_PRIO(3);
309
+ }
310
+
311
+ /* Map a discrete lane slot to a target screen-x for the player car. The car
312
+ * eases toward this so steering feels like a turn, not a teleport. */
313
+ static s16 slot_x(s16 slot) {
314
+ return (s16)(ROAD_X0 + 8 + slot * ((ROAD_X1 - ROAD_X0 - 16) / (CAR_SLOTS - 1)));
315
+ }
76
316
 
77
- static Car player;
78
- static Car obstacles[MAX_OBSTACLES];
79
- static u16 score;
80
- static u16 spawn_timer;
81
- static u16 game_over_timer;
82
- static u16 prev_keys;
83
- static u8 player_lane;
317
+ /* ── GAME LOGIC (clay) — HUD / screens (TTE on BG1, priority 0) ── */
318
+ static void draw_hud_labels(void) {
319
+ tte_erase_screen();
320
+ tte_write("#{P:8,4}DIST");
321
+ tte_write("#{P:150,4}LIFE");
322
+ }
84
323
 
85
- static const s16 lane_x[3] = { LANE_LEFT_X, LANE_MID_X, LANE_RIGHT_X };
324
+ static void draw_hud_numbers(void) {
325
+ tte_erase_rect(48, 4, 100, 12); draw_num(48, 4, dist, 5);
326
+ tte_erase_rect(196, 4, 210, 12); draw_num(196, 4, lives, 1);
327
+ }
86
328
 
87
- static int aabb(const Car *a, const Car *b) {
88
- return a->x < b->x + 8 && a->x + 8 > b->x
89
- && a->y < b->y + 8 && a->y + 8 > b->y;
329
+ static void enter_title(void) {
330
+ state = ST_TITLE;
331
+ tte_erase_screen();
332
+ tte_write("#{P:64,40}" GAME_TITLE);
333
+ tte_write("#{P:76,72}PRESS START");
334
+ tte_write("#{P:84,92}BEST");
335
+ draw_num(128, 92, best, 5);
336
+ tte_write("#{P:20,116}LEFT RIGHT STEER - A B SPEED");
337
+ tte_write("#{P:44,128}1P ENDLESS - NO LINK 2P");
90
338
  }
91
339
 
92
- static void reset_run(void) {
93
- player_lane = 1;
94
- player.x = lane_x[1];
95
- player.y = PLAYER_Y;
96
- player.alive = 1;
97
- for (int i = 0; i < MAX_OBSTACLES; i++) obstacles[i].alive = 0;
98
- score = 0;
340
+ static void enter_play(void) {
341
+ int i;
342
+ state = ST_PLAY;
343
+ car_slot = CAR_SLOTS / 2;
344
+ car_x = slot_x(car_slot);
345
+ speed = START_SPEED;
346
+ lives = START_LIVES;
347
+ invuln = 0;
348
+ dist = 0; dist_sub = 0;
349
+ new_best = 0;
350
+ road_scroll = 0;
351
+ bank = 0;
352
+ for (i = 0; i < MAX_TRAFFIC; i++) tr_alive[i] = 0;
99
353
  spawn_timer = 0;
100
- game_over_timer = 0;
354
+ /* Stir the PRNG with time-on-title so each run differs. */
355
+ rng ^= (u16)REG_VCOUNT ^ ((u16)REG_VCOUNT << 7);
356
+ if (rng == 0) rng = 0xC0A7;
357
+ draw_hud_labels();
358
+ draw_hud_numbers();
101
359
  }
102
360
 
103
- static void spawn_obstacle(void) {
104
- for (int i = 0; i < MAX_OBSTACLES; i++) {
105
- if (!obstacles[i].alive) {
106
- obstacles[i].x = lane_x[(spawn_timer * 13) % 3];
107
- obstacles[i].y = -8;
108
- obstacles[i].alive = 1;
361
+ static void enter_over(void) {
362
+ state = ST_OVER;
363
+ if (dist > best) {
364
+ best = dist;
365
+ new_best = 1;
366
+ best_save(best); /* byte-wise SRAM write — see SRAM idiom */
367
+ }
368
+ tte_write("#{P:84,56}WRECKED");
369
+ tte_write("#{P:84,72}DIST");
370
+ draw_num(140, 72, dist, 5);
371
+ if (new_best) tte_write("#{P:72,88}NEW BEST");
372
+ tte_write("#{P:76,108}PRESS START");
373
+ sfx_tone(1, 1100, 12);
374
+ sfx_tone(2, 900, 14);
375
+ }
376
+
377
+ /* ── GAME LOGIC (clay) — spawn traffic into a free slot ── */
378
+ static void spawn_traffic(void) {
379
+ int i;
380
+ for (i = 0; i < MAX_TRAFFIC; i++)
381
+ if (!tr_alive[i]) {
382
+ tr_alive[i] = 1;
383
+ tr_slot[i] = random8() % CAR_SLOTS;
384
+ tr_y[i] = -8; /* enters from the top of the road */
109
385
  return;
110
386
  }
111
- }
112
387
  }
113
388
 
114
- int main(void) {
115
- /* Sprite palettes. Bank 0 for player (white+grey), bank 1 for enemy
116
- * (red+dark grey). Index 0 is always transparent. */
117
- pal_obj_bank[0][1] = CLR_WHITE;
118
- pal_obj_bank[0][2] = RGB15(20, 20, 20);
119
- pal_obj_bank[1][3] = CLR_RED;
120
- pal_obj_bank[1][4] = RGB15(12, 12, 12);
121
-
122
- /* Sprite tiles — OBJ char base 4. */
123
- tonccpy(&tile_mem[4][TILE_CAR_P1], tile_car_p1, sizeof(tile_car_p1));
124
- tonccpy(&tile_mem[4][TILE_CAR_EN], tile_car_enemy, sizeof(tile_car_enemy));
125
-
126
- /* ── Road backdrop on BG0 ────────────────────────────────────────
127
- * Paint a full-screen road scene (grass shoulders + asphalt + lane
128
- * dashes) so the track doesn't read as a blank black screen. BG
129
- * palette: 5 grass / 6 grass-tuft / 7 asphalt / 8 white lines. Tile
130
- * data → char-block 0, map → screen-block 28 (clear of TTE on
131
- * char-block 2 / screen-block 30). BG0 is lowest priority so the
132
- * cars draw in front. The 240-px screen is 30 tiles wide; the road
133
- * occupies columns 5..24 with grass on either shoulder. */
134
- pal_bg_mem[0] = CLR_BLACK;
135
- pal_bg_mem[5] = RGB15(4, 16, 5); /* grass */
136
- pal_bg_mem[6] = RGB15(2, 11, 3); /* darker grass tuft*/
137
- pal_bg_mem[7] = RGB15(7, 7, 8); /* asphalt */
138
- pal_bg_mem[8] = CLR_WHITE; /* lane / edge lines*/
139
- tonccpy(&tile_mem[0][4], tile_grass, sizeof(tile_grass));
140
- tonccpy(&tile_mem[0][5], tile_road, sizeof(tile_road));
141
- tonccpy(&tile_mem[0][6], tile_road_dash, sizeof(tile_road_dash));
142
- tonccpy(&tile_mem[0][7], tile_road_edge, sizeof(tile_road_edge));
143
- REG_BG0CNT = BG_CBB(0) | BG_SBB(28) | BG_REG_32x32 | BG_4BPP | BG_PRIO(3);
389
+ /* AABB, both boxes ~12 px (cars are 8px sprites; a slightly fat box makes the
390
+ * crash feel fair at speed). */
391
+ static u8 hits(s16 ax, s16 ay, s16 bx, s16 by) {
392
+ s16 dx = (s16)(ax > bx ? ax - bx : bx - ax);
393
+ s16 dy = (s16)(ay > by ? ay - by : by - ay);
394
+ return (u8)(dx < 12 && dy < 12);
395
+ }
396
+
397
+ static void crash(void) {
398
+ sfx_noise(14);
399
+ invuln = 60;
400
+ if (speed > SPEED_MIN) speed--; /* a wreck kills your momentum */
401
+ if (lives > 0) lives--;
402
+ draw_hud_numbers();
403
+ if (lives == 0) enter_over();
404
+ }
405
+
406
+ /* ── GAME LOGIC (clay) — one ST_PLAY tick. The road is the affine BG2; the car
407
+ * and traffic are sprites over it. Edge cases handled: the car eases toward its
408
+ * lane (no teleport); a just-crashed car blinks and can't collide for 60
409
+ * frames; traffic flows down at road speed and despawns past the bottom. ── */
410
+ static void update_play(void) {
411
+ int i;
412
+ s16 target_x;
413
+
414
+ random8(); /* tick the noise source every frame */
415
+
416
+ /* Steer: LEFT/RIGHT move between discrete lanes (edge-triggered so a held
417
+ * d-pad doesn't machine-gun across the road). The road BANKS toward the
418
+ * turn bank eases back to 0 when you're straight. */
419
+ if (key_hit(KEY_LEFT) && car_slot > 0) { car_slot--; sfx_tone(1, 1400, 3); }
420
+ if (key_hit(KEY_RIGHT) && car_slot < CAR_SLOTS - 1) { car_slot++; sfx_tone(1, 1400, 3); }
421
+ target_x = slot_x(car_slot);
422
+ if (car_x < target_x) { car_x += 3; if (car_x > target_x) car_x = target_x; }
423
+ if (car_x > target_x) { car_x -= 3; if (car_x < target_x) car_x = target_x; }
424
+ /* bank = how far the car is from screen centre → tilt the whole road */
144
425
  {
145
- SCR_ENTRY *map = se_mem[28];
146
- for (int ty = 0; ty < 32; ty++) {
147
- for (int tx = 0; tx < 32; tx++) {
148
- int tile;
149
- if (tx < 5 || tx > 24) {
150
- tile = 4; /* grass shoulder */
151
- } else if (tx == 5 || tx == 24) {
152
- tile = 7; /* white road edge */
153
- } else if ((tx == 11 || tx == 18) && (ty & 1)) {
154
- tile = 6; /* dashed lane divider */
155
- } else {
156
- tile = 5; /* plain asphalt */
157
- }
158
- map[ty * 32 + tx] = SE_BUILD(tile, 0, 0, 0);
426
+ s16 want = (s16)((car_x - 120) * 6); /* ±~0x0180-ish lean */
427
+ if (bank < want) bank += 24;
428
+ if (bank > want) bank -= 24;
429
+ }
430
+
431
+ /* Throttle: A/UP faster, B/DOWN slower. */
432
+ if (key_hit(KEY_A | KEY_UP) && speed < SPEED_MAX) { speed++; sfx_tone(2, (u16)(900 + speed * 90), 4); }
433
+ if (key_hit(KEY_B | KEY_DOWN) && speed > SPEED_MIN) { speed--; sfx_tone(2, 1500, 3); }
434
+
435
+ /* Recede: advance the road texture downward by `speed`. */
436
+ road_scroll = (u16)(road_scroll + speed);
437
+
438
+ /* Distance (the score): 1 unit per 16 scrolled px. A chime every 256. */
439
+ dist_sub = (u16)(dist_sub + speed);
440
+ if (dist_sub >= 16) {
441
+ dist_sub -= 16;
442
+ if (dist < 65000u) dist++;
443
+ if (dist != 0 && (dist & 0xFF) == 0) sfx_tone(1, 1800, 8); /* checkpoint */
444
+ draw_hud_numbers();
445
+ }
446
+
447
+ /* Traffic flows down at road speed (reads as cars you overtake). */
448
+ for (i = 0; i < MAX_TRAFFIC; i++) {
449
+ if (!tr_alive[i]) continue;
450
+ tr_y[i] = (s16)(tr_y[i] + speed + 1); /* a touch faster than scroll */
451
+ if (tr_y[i] > SCREEN_H) { tr_alive[i] = 0; sfx_tone(2, 700, 2); }
452
+ }
453
+ if (++spawn_timer >= (u8)(28 - speed * 2)) { /* denser at higher speed */
454
+ spawn_timer = 0;
455
+ spawn_traffic();
456
+ }
457
+
458
+ /* Crash check: traffic ↔ player. Grace window after a crash. */
459
+ if (invuln > 0) invuln--;
460
+ if (!invuln) {
461
+ for (i = 0; i < MAX_TRAFFIC; i++) {
462
+ if (!tr_alive[i]) continue;
463
+ if (hits((s16)(slot_x(tr_slot[i]) + 4), (s16)(tr_y[i] + 4), (s16)(car_x + 4), CAR_Y + 4)) {
464
+ tr_alive[i] = 0;
465
+ crash();
466
+ if (state != ST_PLAY) return;
159
467
  }
160
468
  }
161
469
  }
470
+ }
471
+
472
+ /* ── GAME LOGIC (clay) — stage the sprites: player car + traffic. Off-screen /
473
+ * inactive slots park at y=200. ── */
474
+ static OBJ_ATTR obj_buffer[128];
475
+ static void stage_sprites(void) {
476
+ int i;
477
+ int playing = (state == ST_PLAY);
478
+ /* player car (blinks during the post-crash grace) */
479
+ obj_set_attr(&obj_buffer[SLOT_CAR], ATTR0_SQUARE, ATTR1_SIZE_8,
480
+ (u16)(ATTR2_PALBANK(0) | TILE_CAR));
481
+ {
482
+ int hide = !playing || (invuln && (invuln & 2));
483
+ obj_set_pos(&obj_buffer[SLOT_CAR], hide ? 250 : car_x, hide ? 200 : CAR_Y);
484
+ }
485
+ for (i = 0; i < MAX_TRAFFIC; i++) {
486
+ int on = playing && tr_alive[i];
487
+ obj_set_attr(&obj_buffer[SLOT_TRAFFIC + i], ATTR0_SQUARE, ATTR1_SIZE_8,
488
+ (u16)(ATTR2_PALBANK(1) | TILE_RIVAL));
489
+ obj_set_pos(&obj_buffer[SLOT_TRAFFIC + i],
490
+ on ? slot_x(tr_slot[i]) : 250, on ? tr_y[i] : 200);
491
+ }
492
+ }
493
+
494
+ int main(void) {
495
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
496
+ * Init order: tiles/palettes → oam_init → irq_init + II_VBLANK → TTE init
497
+ * → DISPCNT last. VBlankIntrWait() HANGS FOREVER without the vblank IRQ
498
+ * registered (the #1 "frozen on frame 1" cause), and enabling DISPCNT
499
+ * layers before their tiles/maps exist flashes garbage. Mode 1 = BG0/BG1
500
+ * regular, BG2 AFFINE. TTE owns BG1 (CBB 2 / SBB 30) — keep other layers
501
+ * off those blocks.
502
+ * requires: nothing prior; this IS the boot. */
162
503
 
163
- oam_init(obj_buffer, 128);
504
+ /* Sprite tiles → OBJ char base (tile_mem[4]). */
505
+ tonccpy(&tile_mem[4][TILE_CAR], tile_car, sizeof(tile_car));
506
+ tonccpy(&tile_mem[4][TILE_RIVAL], tile_rival, sizeof(tile_rival));
507
+
508
+ /* OBJ palettes: bank 0 = your car (cyan body, hot cockpit), bank 1 = rival
509
+ * (red). The GBA's 15-bit RGB gives saturated colours the GB/NES can only
510
+ * hint at. */
511
+ pal_obj_bank[0][1] = RGB15(6, 20, 31); /* your car body cyan */
512
+ pal_obj_bank[0][2] = RGB15(31, 31, 24); /* cockpit glint near-white */
513
+ pal_obj_bank[0][3] = RGB15(4, 10, 22); /* car outline deep blue */
514
+ pal_obj_bank[1][1] = RGB15(31, 7, 7); /* rival body red */
515
+ pal_obj_bank[1][2] = RGB15(31, 24, 10); /* rival windshield amber */
516
+ pal_obj_bank[1][3] = RGB15(16, 2, 2); /* rival outline maroon */
517
+
518
+ road_build(); /* affine BG2: tiles + map + BG2CNT */
519
+
520
+ oam_init(obj_buffer, 128); /* hides all 128, matrices = identity */
164
521
 
165
- /* IRQ setup — required for VBlankIntrWait() to function. */
166
522
  irq_init(NULL);
167
523
  irq_add(II_VBLANK, NULL);
168
524
 
169
- sfx_init();
525
+ sfx_init(); /* APU on; music loop ticks below */
170
526
 
171
- /* TTE for score + hint on BG1 (BG0 holds the road). char-block 2 /
172
- * screen-block 30; priority 0 text in front of everything. */
527
+ /* TTE text on BG1 (4bpp char block 2, screenblock 30), priority 0 so text
528
+ * draws over everything. Mode 1 = BG0/BG1 regular, BG2 affine. */
173
529
  tte_init_chr4c_default(1, BG_CBB(2) | BG_SBB(30));
174
530
  REG_BG1CNT |= BG_PRIO(0);
175
- REG_DISPCNT = DCNT_MODE0 | DCNT_BG0 | DCNT_BG1 | DCNT_OBJ | DCNT_OBJ_1D;
176
- tte_write("#{P:160,8}SCORE 00000");
177
- tte_write("#{P:64,150}L/R MOVES LANE");
531
+ REG_DISPCNT = DCNT_MODE1 | DCNT_BG1 | DCNT_BG2 | DCNT_OBJ | DCNT_OBJ_1D;
178
532
 
179
- reset_run();
180
- prev_keys = 0;
533
+ best = best_load(); /* cartridge SRAM — 0 on first boot */
534
+ enter_title();
181
535
 
182
536
  while (1) {
537
+ /* Idiomatic Tonc heartbeat: wait vblank, poll keys, update, then commit
538
+ * OAM + the affine matrix while still inside vblank (the update is far
539
+ * quicker than the 4.9ms vblank window). */
183
540
  VBlankIntrWait();
184
541
  key_poll();
542
+ sfx_music_tick(); /* forget this → silent game */
185
543
 
186
- if (game_over_timer > 0) {
187
- game_over_timer--;
188
- if (game_over_timer == 60) {
189
- tte_write("#{P:72,80}CRASH! TRY AGAIN");
190
- }
191
- if (game_over_timer == 0) {
192
- tte_erase_rect(72, 80, 240, 96);
193
- reset_run();
194
- }
544
+ if (state == ST_TITLE) {
545
+ if (key_hit(KEY_START | KEY_A)) enter_play();
546
+ } else if (state == ST_OVER) {
547
+ if (key_hit(KEY_START)) enter_title();
195
548
  } else {
196
- u16 now = key_curr_state();
197
- if ((now & KEY_LEFT) && !(prev_keys & KEY_LEFT) && player_lane > 0) {
198
- player_lane--;
199
- sfx_tone(2, 1300, 2); /* lane switch */
200
- }
201
- if ((now & KEY_RIGHT) && !(prev_keys & KEY_RIGHT) && player_lane < 2) {
202
- player_lane++;
203
- sfx_tone(2, 1300, 2);
204
- }
205
- player.x = lane_x[player_lane];
206
- prev_keys = now;
207
-
208
- /* Obstacle speed grows slowly with score. */
209
- s16 step = 2 + (s16)(score / 500);
210
- if (step > 4) step = 4;
211
-
212
- for (int i = 0; i < MAX_OBSTACLES; i++) {
213
- if (!obstacles[i].alive) continue;
214
- obstacles[i].y += step;
215
- if (obstacles[i].y > 160) obstacles[i].alive = 0;
216
- }
217
-
218
- if (++spawn_timer >= 32) { spawn_timer = 0; spawn_obstacle(); }
219
-
220
- for (int i = 0; i < MAX_OBSTACLES; i++) {
221
- if (obstacles[i].alive && aabb(&player, &obstacles[i])) {
222
- game_over_timer = 60;
223
- sfx_noise(30); /* crash */
224
- break;
225
- }
226
- }
227
-
228
- if (score < 65500u) score++;
549
+ update_play();
229
550
  }
230
551
 
231
- /* Sprite slots: 0 = player, 1..4 = obstacles. */
232
- obj_set_attr(&obj_buffer[0],
233
- ATTR0_SQUARE,
234
- ATTR1_SIZE_8,
235
- ATTR2_PALBANK(0) | TILE_CAR_P1);
236
- obj_set_pos(&obj_buffer[0], player.x, player.y);
237
- for (int i = 0; i < MAX_OBSTACLES; i++) {
238
- s16 ey = obstacles[i].alive ? obstacles[i].y : 200;
239
- obj_set_attr(&obj_buffer[1 + i],
240
- ATTR0_SQUARE,
241
- ATTR1_SIZE_8,
242
- ATTR2_PALBANK(1) | TILE_CAR_EN);
243
- obj_set_pos(&obj_buffer[1 + i], obstacles[i].x, ey);
552
+ /* Apply the affine road every frame. On the title/result we still spin
553
+ * a slow, gently-banking road so the screen is never a blank card and
554
+ * the affine hardware is visibly alive. In play, zoom pulses with
555
+ * speed: faster = the road scales UP and rushes toward you. */
556
+ if (state == ST_PLAY) {
557
+ u32 zoom = (u32)(220 + speed * 14); /* 0.86..1.19 (8.8) */
558
+ road_apply((u16)bank, zoom, road_scroll);
559
+ } else {
560
+ road_scroll = (u16)(road_scroll + 1);
561
+ road_apply((u16)(road_scroll << 6), 256, road_scroll);
244
562
  }
245
- oam_copy(oam_mem, obj_buffer, 1 + MAX_OBSTACLES);
246
563
 
247
- tte_erase_rect(160 + 6*8, 8, 160 + 11*8, 16);
248
- draw_score(160 + 6*8, score);
564
+ stage_sprites();
565
+ oam_copy(oam_mem, obj_buffer, 128);
249
566
  }
250
567
  return 0;
251
568
  }