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.
Files changed (154) hide show
  1. package/AGENTS.md +51 -41
  2. package/CHANGELOG.md +46 -0
  3. package/README.md +3 -3
  4. package/examples/README.md +7 -7
  5. package/examples/atari2600/templates/platformer.asm +1225 -332
  6. package/examples/atari2600/templates/puzzle.asm +1056 -0
  7. package/examples/atari2600/templates/racing.asm +906 -275
  8. package/examples/atari2600/templates/shmup.asm +1031 -239
  9. package/examples/atari2600/templates/sports.asm +1135 -253
  10. package/examples/atari7800/templates/platformer.c +991 -156
  11. package/examples/atari7800/templates/puzzle.c +1091 -148
  12. package/examples/atari7800/templates/racing.c +952 -124
  13. package/examples/atari7800/templates/shmup.c +812 -134
  14. package/examples/atari7800/templates/sports.c +820 -184
  15. package/examples/c64/templates/platformer.c +879 -164
  16. package/examples/c64/templates/puzzle.c +855 -178
  17. package/examples/c64/templates/racing.c +873 -97
  18. package/examples/c64/templates/shmup.c +757 -161
  19. package/examples/c64/templates/sports.c +755 -100
  20. package/examples/gb/templates/platformer.c +841 -179
  21. package/examples/gb/templates/puzzle.c +986 -246
  22. package/examples/gb/templates/racing.c +754 -174
  23. package/examples/gb/templates/shmup.c +673 -175
  24. package/examples/gb/templates/sports.c +790 -159
  25. package/examples/gba/templates/platformer.c +626 -165
  26. package/examples/gba/templates/puzzle.c +519 -269
  27. package/examples/gba/templates/racing.c +511 -206
  28. package/examples/gba/templates/shmup.c +564 -179
  29. package/examples/gba/templates/sports.c +454 -174
  30. package/examples/gbc/templates/platformer.c +944 -180
  31. package/examples/gbc/templates/puzzle.c +363 -109
  32. package/examples/gbc/templates/racing.c +884 -180
  33. package/examples/gbc/templates/shmup.c +821 -185
  34. package/examples/gbc/templates/sports.c +870 -162
  35. package/examples/genesis/templates/platformer.c +747 -129
  36. package/examples/genesis/templates/puzzle.c +694 -261
  37. package/examples/genesis/templates/racing.c +726 -203
  38. package/examples/genesis/templates/shmup.c +535 -142
  39. package/examples/genesis/templates/sports.c +495 -158
  40. package/examples/gg/templates/platformer.c +880 -215
  41. package/examples/gg/templates/puzzle.c +875 -216
  42. package/examples/gg/templates/racing.c +915 -172
  43. package/examples/gg/templates/shmup.c +714 -191
  44. package/examples/gg/templates/sports.c +732 -129
  45. package/examples/lynx/templates/platformer.c +604 -69
  46. package/examples/lynx/templates/puzzle.c +498 -158
  47. package/examples/lynx/templates/racing.c +538 -102
  48. package/examples/lynx/templates/shmup.c +458 -131
  49. package/examples/lynx/templates/sports.c +496 -72
  50. package/examples/msx/platformer/main.c +649 -162
  51. package/examples/msx/puzzle/main.c +742 -240
  52. package/examples/msx/racing/main.c +669 -178
  53. package/examples/msx/shmup/main.c +460 -178
  54. package/examples/msx/sports/main.c +592 -126
  55. package/examples/nes/templates/platformer.c +589 -171
  56. package/examples/nes/templates/puzzle.c +563 -242
  57. package/examples/nes/templates/racing.c +502 -208
  58. package/examples/nes/templates/shmup.c +339 -145
  59. package/examples/nes/templates/sports.c +341 -183
  60. package/examples/pce/platformer/main.c +874 -205
  61. package/examples/pce/puzzle/main.c +802 -287
  62. package/examples/pce/racing/main.c +783 -208
  63. package/examples/pce/shmup/main.c +638 -212
  64. package/examples/pce/sports/main.c +586 -169
  65. package/examples/porting-across-platforms/README.md +1 -1
  66. package/examples/sms/templates/platformer.c +762 -177
  67. package/examples/sms/templates/puzzle.c +752 -212
  68. package/examples/sms/templates/racing.c +808 -145
  69. package/examples/sms/templates/shmup.c +599 -162
  70. package/examples/sms/templates/sports.c +630 -122
  71. package/examples/snes/templates/music_demo.c +7 -0
  72. package/examples/snes/templates/platformer-data.asm +123 -24
  73. package/examples/snes/templates/platformer-hdr.asm +57 -0
  74. package/examples/snes/templates/platformer.c +586 -165
  75. package/examples/snes/templates/puzzle-data.asm +116 -21
  76. package/examples/snes/templates/puzzle-hdr.asm +57 -0
  77. package/examples/snes/templates/puzzle.c +614 -235
  78. package/examples/snes/templates/racing-data.asm +390 -32
  79. package/examples/snes/templates/racing-hdr.asm +57 -0
  80. package/examples/snes/templates/racing.c +807 -196
  81. package/examples/snes/templates/shmup-data.asm +87 -29
  82. package/examples/snes/templates/shmup-hdr.asm +57 -0
  83. package/examples/snes/templates/shmup.c +459 -198
  84. package/examples/snes/templates/sports-data.asm +48 -2
  85. package/examples/snes/templates/sports-hdr.asm +57 -0
  86. package/examples/snes/templates/sports.c +414 -163
  87. package/package.json +1 -1
  88. package/src/host/LibretroHost.js +59 -1
  89. package/src/http/tool-registry.js +11 -11
  90. package/src/mcp/tools/cheats.js +2 -1
  91. package/src/mcp/tools/frame.js +3 -2
  92. package/src/mcp/tools/index.js +3 -3
  93. package/src/mcp/tools/input.js +5 -4
  94. package/src/mcp/tools/lifecycle.js +6 -4
  95. package/src/mcp/tools/platform-docs.js +1 -1
  96. package/src/mcp/tools/preview-tile.js +6 -2
  97. package/src/mcp/tools/project.js +1098 -130
  98. package/src/mcp/tools/rom-id.js +5 -1
  99. package/src/mcp/tools/run-until.js +4 -2
  100. package/src/mcp/tools/snippets.js +6 -6
  101. package/src/mcp/tools/sprite-pipeline.js +14 -2
  102. package/src/mcp/tools/state.js +2 -1
  103. package/src/mcp/tools/tile-inspect.js +8 -1
  104. package/src/mcp/tools/toolchain.js +12 -1
  105. package/src/mcp/tools/watch-memory.js +4 -3
  106. package/src/observer/bus.js +73 -0
  107. package/src/observer/livestream.html +4 -2
  108. package/src/observer/tool-wrap.js +17 -14
  109. package/src/platforms/atari7800/MENTAL_MODEL.md +5 -5
  110. package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
  111. package/src/platforms/c64/MENTAL_MODEL.md +11 -4
  112. package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
  113. package/src/platforms/gb/MENTAL_MODEL.md +3 -3
  114. package/src/platforms/gb/TROUBLESHOOTING.md +61 -8
  115. package/src/platforms/gb/lib/c/README.md +10 -11
  116. package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
  117. package/src/platforms/gb/lib/c/patch-header.js +13 -3
  118. package/src/platforms/gba/MENTAL_MODEL.md +4 -4
  119. package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
  120. package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
  121. package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
  122. package/src/platforms/gbc/MENTAL_MODEL.md +4 -4
  123. package/src/platforms/gbc/TROUBLESHOOTING.md +4 -4
  124. package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
  125. package/src/platforms/gbc/lib/c/README.md +10 -11
  126. package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
  127. package/src/platforms/gbc/lib/c/patch-header.js +13 -3
  128. package/src/platforms/genesis/MENTAL_MODEL.md +3 -3
  129. package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
  130. package/src/platforms/gg/MENTAL_MODEL.md +4 -4
  131. package/src/platforms/gg/TROUBLESHOOTING.md +3 -3
  132. package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
  133. package/src/platforms/gg/lib/c/joypad_read.c +29 -0
  134. package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
  135. package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
  136. package/src/platforms/msx/MENTAL_MODEL.md +5 -5
  137. package/src/platforms/msx/TROUBLESHOOTING.md +2 -2
  138. package/src/platforms/msx/lib/c/msx_hw.h +1 -0
  139. package/src/platforms/msx/lib/c/msx_vdp.c +25 -0
  140. package/src/platforms/nes/MENTAL_MODEL.md +2 -2
  141. package/src/platforms/nes/lib/c/nes_runtime.c +149 -34
  142. package/src/platforms/nes/lib/c/nes_runtime.h +34 -1
  143. package/src/platforms/pce/MENTAL_MODEL.md +5 -5
  144. package/src/platforms/pce/TROUBLESHOOTING.md +1 -1
  145. package/src/platforms/pce/lib/c/pce_hw.h +11 -0
  146. package/src/platforms/pce/lib/c/pce_video.c +32 -0
  147. package/src/platforms/sms/MENTAL_MODEL.md +6 -6
  148. package/src/platforms/snes/MENTAL_MODEL.md +2 -2
  149. package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
  150. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
  151. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
  152. package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
  153. package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
  154. package/src/toolchains/index.js +27 -11
@@ -1,211 +1,672 @@
1
- /* ── platformer.c — Game Boy Advance Tonc SIDE-SCROLLING platformer ──
1
+ /* ── platformer.c — Game Boy Advance side-scrolling platformer (complete game) ─
2
2
  *
3
- * A horizontally scrolling platformer: the world is 512 px wide, BG0
4
- * uses a 64x32 map (so the whole world fits with no streaming), the
5
- * camera follows the player via REG_BG0HOFS, and the HUD lives on BG1
6
- * which we leave un-scrolled. Gravity, jump, land-on-top collision
7
- * against a static platform list in WORLD coords.
3
+ * GEAR GROTTO a COMPLETE, working game: title screen, score + persistent
4
+ * hi-score (cartridge SRAM), music + SFX, gravity/jump physics over a
5
+ * scrolling tile level, coins + distance scoring, and the GBA's signature
6
+ * affine hardware shown where it actually fits a platformer:
7
+ * - a spinning GEAR HAZARD: a 32x32 OBJ that continuously rotates (and
8
+ * scale-pulses) via an OAM affine matrix (8.8 fixed point, affine slot 0,
9
+ * double-size flag). Touch it and you lose a life. The classic
10
+ * spinning-saw/gear obstacle, done the hardware-honest way.
8
11
  *
9
- * Physics is fixed-point: x/y in 1/16-pixel subpixel units. The player
10
- * sprite draws at SCREEN x = (worldX>>4) - camX.
12
+ * The level is a regular (text/tiled) Mode-0 BG: a 64x32 map = a 512-px run
13
+ * of pits, platforms, ground and coins, scrolled by REG_BG0HOFS as the camera
14
+ * follows the player. The 64x32 map WRAPS in hardware at 512 px, so writing
15
+ * (cam & 511) to REG_BG0HOFS and looking up world columns with (& 63) makes
16
+ * the run LOOP SEAMLESSLY — an endless runner with no streaming. The HUD
17
+ * lives on a SECOND regular BG (TTE) that we never scroll.
11
18
  *
12
- * For a world WIDER than the 64x32 map (512 px) you stream the column
13
- * entering view each 8-px camera step into the map's screen-blocks —
14
- * see the GBA MENTAL_MODEL.md "Horizontal scrolling" section.
19
+ * THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
20
+ * very different one. The markers tell you what's what:
21
+ * HARDWARE IDIOM (load-bearing) dodges a documented GBA footgun; reshape
22
+ * your gameplay around it (see TROUBLESHOOTING before changing).
23
+ * GAME LOGIC (clay) — level layout, physics tuning, scoring, art: reshape
24
+ * freely.
25
+ *
26
+ * What depends on what:
27
+ * gba_sfx.{h,c} — PSG sound: sfx_tone/sfx_noise one-shots + the music loop
28
+ * (sfx_music_tick once per frame — forget it and the game is silent).
29
+ * libtonc (the build links it) — VBlankIntrWait/key_poll/OAM/TTE.
30
+ *
31
+ * HANDHELD, SO SINGLE-PLAYER ONLY (honest note): 2P on GBA means a link
32
+ * cable between two units — a second emulator instance this environment
33
+ * can't provide. Title is press-start, no mode select.
34
+ *
35
+ * Frame budget: ARM7TDMI at 16.78MHz with this object count (player + 3 coins
36
+ * + the gear) doesn't come close to a full frame; the affine math is a handful
37
+ * of multiplies per frame, not per pixel — the PPU does the per-pixel work.
15
38
  */
16
39
 
17
40
  #include <tonc.h>
18
41
  #include "gba_sfx.h"
19
42
 
20
- #define TILE_BLANK 0
21
- #define TILE_PLATFORM 1
22
- #define TILE_PLAYER 1 /* sprite tile 1 (sprites have their own char base) */
43
+ /* The title screen renders this — examples({op:'fork'}) stamps your game's
44
+ * name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
45
+ #define GAME_TITLE "GEAR GROTTO"
23
46
 
24
- static const u32 tile_platform[8] = {
25
- 0x11111111, 0x12222221, 0x12222221, 0x12222221,
26
- 0x12222221, 0x12222221, 0x12222221, 0x11111111,
27
- };
47
+ /* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
48
+ * Object pools — fixed slots, no allocation. Sprite slot discipline (128 OAM
49
+ * entries total, we use 5):
50
+ * slot 0 → player
51
+ * slot 1..3 → coins
52
+ * slot 4 → gear hazard (AFFINE — uses OAM affine parameter slot 0; see
53
+ * the affine-sprite idiom below for why slot CHOICE matters)
54
+ */
55
+ #define SLOT_PLAYER 0
56
+ #define SLOT_COIN 1
57
+ #define NUM_COINS 3
58
+ #define SLOT_GEAR 4
59
+
60
+ #define TILE_PLAYER 1 /* sprite tile 1, 8x8 4bpp */
61
+ #define TILE_COIN 2
62
+ #define TILE_GEAR 16 /* 32x32 4bpp = 16 tiles, ids 16..31 */
63
+
64
+ #define START_LIVES 3
65
+
66
+ /* World geometry — a 512-px level in a BG0 64x32 map (whole world, no stream).
67
+ * Physics runs in WORLD pixels; sprites draw at SCREEN = world - camera. */
68
+ #define WORLD_W 512
69
+ #define SCREEN_W 240
70
+ #define SCREEN_H 160
71
+
72
+ /* 4bpp sprite tiles (8 rows × 32 bits each = 32 bytes). Each nibble is a
73
+ * palette index within the sprite's palbank. Index 0 = transparent. */
28
74
  static const u32 tile_player[8] = {
29
- 0x00033000, 0x00333300, 0x03333330, 0x03333330,
30
- 0x03333330, 0x03333330, 0x00333300, 0x00033000,
75
+ 0x00033000, 0x00333300, 0x03311330, 0x03333330,
76
+ 0x03333330, 0x03333330, 0x03300330, 0x03000030,
77
+ };
78
+ static const u32 tile_coin[8] = {
79
+ 0x00022000, 0x00222200, 0x02244220, 0x02422420,
80
+ 0x02422420, 0x02244220, 0x00222200, 0x00022000,
31
81
  };
32
82
 
33
- typedef struct { s16 x, y, w, h; } Rect;
83
+ typedef struct { s32 x; s16 y; u8 alive; } Coin;
84
+
85
+ static OBJ_ATTR obj_buffer[128];
86
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
87
+ * OAM AFFINE SLOT LAYOUT. There is no separate affine-matrix memory: the 32
88
+ * OBJ_AFFINE parameter sets live INTERLEAVED inside OAM itself, in the
89
+ * 16-bit "fill" field of every OBJ_ATTR (4 sprites × 8 bytes carry one
90
+ * 8-byte matrix between them — pa in sprite 4n, pb in 4n+1, pc in 4n+2,
91
+ * pd in 4n+3). Casting the shadow-OAM buffer to OBJ_AFFINE* is the whole
92
+ * trick: obj_aff_buffer[k] aliases the fill words of sprites 4k..4k+3, and
93
+ * one oam_copy() of the full buffer commits sprites AND matrices together.
94
+ * Consequences you must respect:
95
+ * - oam_init() already set all 32 matrices to identity (pa=pd=0x0100).
96
+ * - NEVER memset OBJ_ATTRs to 0 — that zeroes the interleaved matrices
97
+ * (pa=0 means "scale by infinity": every affine sprite vanishes).
98
+ * - Matrix slot k is INDEPENDENT of which sprite uses it (attr1 AFF_ID
99
+ * picks any of the 32) — but the bytes live under sprites 4k..4k+3.
100
+ * requires: obj_buffer staged with oam_init(), committed with oam_copy(). */
101
+ static OBJ_AFFINE *const obj_aff_buffer = (OBJ_AFFINE *)obj_buffer;
102
+
103
+ /* ── GAME LOGIC (clay) — player physics state ────────────────────────────────
104
+ * Position is fixed-point: 1 px = 16 subpixel units (Q.4). Gravity adds well
105
+ * under 1 px/frame near the jump apex, so sub-pixel Y is mandatory — integer
106
+ * Y would stutter the arc. X stays integer (walking is whole-pixel). */
107
+ #define SUBPX 16 /* subpixels per pixel (Q.4) */
108
+ #define GRAVITY 12 /* +12/16 px per frame per frame */
109
+ #define JUMP_VEL (-200) /* launch vy (Q.4) → ~6-tile apex */
110
+ #define MAX_FALL 80 /* terminal velocity 5 px/frame — keep < 6: *
111
+ * the landing window is 6 px, a faster fall *
112
+ * would tunnel through a platform top */
113
+ #define MOVE_SPEED 2 /* px/frame walk + scroll speed */
114
+ #define SCROLL_WALL 96 /* px: past this the world scrolls, not you */
34
115
 
35
- #define WORLD_W 512
36
- #define SCREEN_W 240
116
+ static s32 px; /* player WORLD x, whole px (left edge) — *
117
+ * s32: the endless camera grows without bound*/
118
+ static s32 py_q4; /* player WORLD y, Q.4 (top edge) */
119
+ static s16 vy_q4; /* vertical velocity, Q.4 */
120
+ static u8 on_ground;
121
+ static s32 cam_x; /* camera world-x (BG0 scroll, ever-growing) */
122
+ static u16 dist_sub; /* sub-counter: 64 px walked = +1 point */
37
123
 
38
- /* Static platforms in WORLD PIXEL coords, spread across the 512-px world. */
39
- static const Rect platforms[] = {
40
- { 0, 144, 512, 16 }, /* floor spans the world */
41
- { 24, 112, 56, 8 },
42
- { 140, 100, 64, 8 },
43
- { 250, 84, 48, 8 },
44
- { 360, 68, 56, 8 },
45
- { 440, 120, 56, 8 },
46
- { 180, 52, 48, 8 },
124
+ static Coin coins[NUM_COINS];
125
+ static u16 score, hiscore;
126
+ static u8 lives;
127
+ static u16 frame; /* free-running frame counter (drives gear) */
128
+
129
+ /* Gear hazard state — the affine sprite showcase. WORLD-anchored; it drifts
130
+ * left with the camera like the level does. */
131
+ static s32 gear_wx, gear_wy; /* CENTER of the gear, WORLD pixels */
132
+ static u16 gear_theta; /* rotation angle: full circle = 0x10000 */
133
+ static u16 gear_pulse; /* scale-pulse phase */
134
+
135
+ /* Game states — the shell every example shares: title → play → game over. */
136
+ #define ST_TITLE 0
137
+ #define ST_PLAY 1
138
+ #define ST_OVER 2
139
+ static u8 state;
140
+
141
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
142
+ * PERSISTENT SRAM at 0x0E000000. Two footguns, both fatal-but-silent:
143
+ * 1. The SRAM bus is 8 BITS WIDE. Byte reads/writes only — a u16/u32
144
+ * access doesn't fault, it just reads the same byte mirrored (and a
145
+ * wide write stores one byte), so your data "almost" round-trips and
146
+ * then the checksum never matches. Every access below is via vu8.
147
+ * 2. Emulators and flashcarts detect the SAVE TYPE by scanning the ROM
148
+ * image for a marker string. Without "SRAM_V" in the ROM, mGBA gives
149
+ * the cart NO save memory at all and writes to 0x0E000000 vanish.
150
+ * The aligned, (used)-attributed const below plants that marker —
151
+ * delete it and persistence dies even though this code is untouched.
152
+ * Layout: 'V' 'X' score-lo score-hi checksum (xor ^ 0xA5) — magic+checksum
153
+ * so a fresh (0xFF-filled) cart reads as "no record" instead of garbage.
154
+ * requires: nothing else — self-contained; safe to transplant whole. */
155
+ #define SRAM_BYTE ((volatile u8 *)0x0E000000)
156
+ __attribute__((used, aligned(4))) static const char sram_type_marker[] = "SRAM_V113";
157
+
158
+ static u16 hiscore_load(void) {
159
+ u8 lo, hi;
160
+ if (SRAM_BYTE[0] != 'V' || SRAM_BYTE[1] != 'X') return 0;
161
+ lo = SRAM_BYTE[2];
162
+ hi = SRAM_BYTE[3];
163
+ if (SRAM_BYTE[4] != (u8)(lo ^ hi ^ 0xA5)) return 0;
164
+ return (u16)(lo | (hi << 8));
165
+ }
166
+
167
+ static void hiscore_save(u16 v) {
168
+ SRAM_BYTE[0] = 'V';
169
+ SRAM_BYTE[1] = 'X';
170
+ SRAM_BYTE[2] = (u8)v;
171
+ SRAM_BYTE[3] = (u8)(v >> 8);
172
+ SRAM_BYTE[4] = (u8)((u8)v ^ (u8)(v >> 8) ^ 0xA5);
173
+ }
174
+
175
+ /* ── GAME LOGIC (clay) — TTE text helpers ────────────────────────────────────
176
+ * Draw right-aligned decimal digits at pixel (x,y) WITHOUT tte_printf. The
177
+ * bundled libtonc's tte_printf with a %d conversion is broken (it routes
178
+ * through a vsnprintf path that isn't wired in this build — it garbles
179
+ * output AND wedges the loop when called per-frame, GBA-1). We build the
180
+ * string ourselves and use tte_write, which processes the #{P:x,y} position
181
+ * command but does NO format conversion → safe every frame. */
182
+ static void draw_num(int x, int y, unsigned v, int digits) {
183
+ char buf[24];
184
+ int i, n = 0;
185
+ buf[n++] = '#'; buf[n++] = '{'; buf[n++] = 'P'; buf[n++] = ':';
186
+ if (x >= 100) buf[n++] = (char)('0' + (x / 100) % 10);
187
+ if (x >= 10) buf[n++] = (char)('0' + (x / 10) % 10);
188
+ buf[n++] = (char)('0' + x % 10);
189
+ buf[n++] = ',';
190
+ if (y >= 100) buf[n++] = (char)('0' + (y / 100) % 10);
191
+ if (y >= 10) buf[n++] = (char)('0' + (y / 10) % 10);
192
+ buf[n++] = (char)('0' + y % 10);
193
+ buf[n++] = '}';
194
+ for (i = digits - 1; i >= 0; i--) { buf[n + i] = (char)('0' + (v % 10)); v /= 10; }
195
+ n += digits; buf[n] = 0;
196
+ tte_write(buf);
197
+ }
198
+
199
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
200
+ * AFFINE SPRITE (the gear hazard) — the 8.8 screen→texture matrix stored in
201
+ * OAM affine slot 0 (see the slot-layout idiom at obj_aff_buffer). Three
202
+ * OBJ-specific footguns this block dodges:
203
+ * 1. attr0 mode bits: ATTR0_AFF (01) turns affine ON; ATTR0_AFF_DBL (11)
204
+ * is affine + DOUBLE-SIZE. Without double-size the sprite is clipped
205
+ * to its original WxH box — a rotated 32x32 has its corners CUT OFF
206
+ * (≈29% of the diagonal) and a zoomed-up one is cropped to 32x32.
207
+ * Double-size renders into a 64x64 window so rotation/zoom≤2x fits.
208
+ * 2. Double-size MOVES THE SPRITE: attr0/attr1 x/y are the top-left of
209
+ * the RENDER WINDOW, so the visual center sits at (x+32, y+32) for a
210
+ * 32x32 sprite — position it by center and subtract 32 (a plain
211
+ * sprite would subtract 16). Forks that toggle DBL must re-anchor.
212
+ * 3. ATTR0_HIDE does NOT hide an affine sprite — mode bits 01/11 reuse
213
+ * the hide bit. To hide the gear, drop attr0 back to a REGULAR hidden
214
+ * object (ATTR0_HIDE alone).
215
+ * The MATRIX (8.8 fixed point, 256 == 1.0) maps SCREEN pixels → TEXTURE
216
+ * pixels (the INVERSE of "how the image is transformed"), so to SHOW the
217
+ * texture rotated by θ and zoomed by z you write the matrix of -θ and 1/z:
218
+ * inv = 65536/z (8.8 reciprocal: 1/z)
219
+ * pa = cos·inv>>8 pb = -sin·inv>>8
220
+ * pc = sin·inv>>8 pd = cos·inv>>8
221
+ * lu_sin/lu_cos take a u16 angle (full circle = 0x10000) and return 4.12
222
+ * fixed → >>4 converts to 8.8.
223
+ * requires: OAM affine slot 0 free (sprites 0..3's fill words — fine here,
224
+ * they're regular objects whose fill is untouched), obj_buffer committed
225
+ * by oam_copy() every frame, gear tiles at OBJ tile 16 (4bpp 32x32, 1D). */
226
+ static void gear_stage(s16 screen_cx, s16 screen_cy) {
227
+ OBJ_ATTR *o = &obj_buffer[SLOT_GEAR];
228
+ /* zoom pulse: 1.0 ± 0.20 from the sine LUT (4.12 → ±~50 in 8.8). A
229
+ * pure rotation is readable, but the gentle breathing makes the affine
230
+ * scaling visible too — both halves of the idiom on screen at once. */
231
+ u32 zoom = (u32)(256 + (lu_sin(gear_pulse) >> 7));
232
+ s32 inv = (s32)(65536u / zoom);
233
+ s32 cc = ((lu_cos(gear_theta) >> 4) * inv) >> 8;
234
+ s32 ss = ((lu_sin(gear_theta) >> 4) * inv) >> 8;
235
+ obj_aff_buffer[0].pa = (s16)cc; obj_aff_buffer[0].pb = (s16)-ss;
236
+ obj_aff_buffer[0].pc = (s16)ss; obj_aff_buffer[0].pd = (s16)cc;
237
+
238
+ o->attr0 = (u16)(ATTR0_AFF_DBL | ATTR0_SQUARE | ATTR0_4BPP
239
+ | ((screen_cy - 32) & 0x00FF)); /* window top */
240
+ o->attr1 = (u16)(ATTR1_SIZE_32 | ATTR1_AFF_ID(0)
241
+ | ((screen_cx - 32) & 0x01FF)); /* window left */
242
+ o->attr2 = (u16)(ATTR2_PALBANK(3) | TILE_GEAR);
243
+ }
244
+
245
+ /* ── GAME LOGIC (clay) — gear ART: a toothed disc with ONE bright tooth (the
246
+ * asymmetry makes the spin readable; a symmetric disc looks static).
247
+ * Drawn procedurally into a 32x32 4bpp staging buffer laid out exactly as
248
+ * OBJ VRAM wants it in 1D mapping: 16 consecutive 8x8 tiles, row-major
249
+ * within the sprite, 2 pixels per byte (low nibble = left pixel). */
250
+ static void gear_build_tiles(void) {
251
+ static u32 tiles[16][8];
252
+ int x, y;
253
+ for (y = 0; y < 32; y++)
254
+ for (x = 0; x < 32; x++) {
255
+ int dx = x - 16, dy = y - 16;
256
+ int r2 = dx * dx + dy * dy;
257
+ int c = 0;
258
+ if (r2 < 25) c = 3; /* hub bore */
259
+ else if (r2 < 64) c = 1; /* hub */
260
+ else if (r2 < 144) c = 2; /* gear body*/
261
+ /* eight square teeth around the rim (one per 45°) */
262
+ if (r2 >= 144 && r2 < 225) {
263
+ if ((dx >= -2 && dx <= 2) || (dy >= -2 && dy <= 2)
264
+ || (dx - dy >= -3 && dx - dy <= 3)
265
+ || (dx + dy >= -3 && dx + dy <= 3)) c = 2;
266
+ }
267
+ if (dx >= -2 && dx <= 2 && dy > 8 && dy < 16) c = 4; /* BRIGHT tooth ↓ (spin marker) */
268
+ if (c) {
269
+ int t = (y / 8) * 4 + (x / 8);
270
+ tiles[t][y % 8] |= (u32)c << (4 * (x % 8));
271
+ }
272
+ }
273
+ tonccpy(&tile_mem[4][TILE_GEAR], tiles, sizeof(tiles));
274
+ pal_obj_bank[3][1] = RGB15(20, 20, 22); /* hub steel */
275
+ pal_obj_bank[3][2] = RGB15(13, 13, 16); /* gear body */
276
+ pal_obj_bank[3][3] = RGB15(28, 26, 10); /* bore brass */
277
+ pal_obj_bank[3][4] = RGB15(31, 12, 6); /* THE hot tooth (spin marker) */
278
+ }
279
+
280
+ /* ── GAME LOGIC (clay — reshape freely) ─────────────────────────────────── */
281
+ static u8 rng_state = 0xA5;
282
+ static u8 rand8(void) { /* Galois LFSR, period 255 */
283
+ u8 lsb = (u8)(rng_state & 1);
284
+ rng_state >>= 1;
285
+ if (lsb) rng_state ^= 0xB8;
286
+ return rng_state;
287
+ }
288
+
289
+ /* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
290
+ * The level — a 64-column world (512 px). For each column:
291
+ * ground_row[c] — map row of the ground's grass top, NO_GROUND = a pit.
292
+ * plat_row[c] — row of a one-way floating platform, 0 = none.
293
+ * Rows are map rows (y = row*8). Map is 32 rows tall (256 px); the playfield
294
+ * sits in rows 3..19, ground around row 18 (y=144). Identical layout repeats
295
+ * are fine — this is a fixed hand-authored run, not a procedural loop. */
296
+ #define NO_GROUND 0xFF
297
+ #define GROUND_ROW 18 /* y = 144 — the main floor */
298
+ #define MAP_COLS 64
299
+ static const u8 ground_row[MAP_COLS] = {
300
+ 18,18,18,18,18,18,18,18, /* long start runway */
301
+ 18,18,18,18,18,18,18,18, /* (lets it scroll */
302
+ 18,18,18,18,NO_GROUND,NO_GROUND,18,18, /* pit 1 (cols 20-21)*/
303
+ 18,18,18,18,18,18,18,18,
304
+ 18,18,NO_GROUND,NO_GROUND,NO_GROUND,18,18,18, /* pit 2 wide (34-36)*/
305
+ 18,18,18,18,18,18,18,18,
306
+ 18,18,18,NO_GROUND,NO_GROUND,18,18,18, /* pit 3 (51-52) */
307
+ 18,18,18,18,18,18,18,18, /* finish runway */
308
+ };
309
+ static const u8 plat_row[MAP_COLS] = {
310
+ 0,0,0,0,0,0,12,12, /* warm-up slab */
311
+ 12,0,0,0,0,0,0,0,
312
+ 0,0,11,11,11,0,0,0, /* slab over pit 1 */
313
+ 0,0,0,10,10,10,0,0, /* high slab */
314
+ 0,11,11,11,11,0,0,0, /* slab over pit 2 */
315
+ 0,0,0,0,9,9,0,0, /* high slab */
316
+ 0,0,12,12,12,0,0,0, /* slab over pit 3 */
317
+ 0,0,0,0,13,13,13,0, /* finish slab */
47
318
  };
48
- /* (int) cast so `for (int i = 0; i < N_PLATFORMS; ...)` doesn't compare a
49
- * signed counter against an unsigned size_t (-Wsign-compare). */
50
- #define N_PLATFORMS ((int)(sizeof(platforms) / sizeof(platforms[0])))
51
319
 
52
- static OBJ_ATTR obj_buffer[128];
320
+ #define BG_BLANK 0
321
+ #define BG_GRASS 1 /* ground surface + floating slabs */
322
+ #define BG_DIRT 2 /* ground body */
323
+ #define BG_BRICK 3 /* backdrop accent */
324
+
325
+ /* ── GAME LOGIC (clay) — BG tile art (regular Mode-0 4bpp BG tiles).
326
+ * Each 8x8 4bpp tile is 8 u32 rows; each nibble a palette index in the BG
327
+ * palbank we use (bank 0 — regular BGs carry a 4-bit palbank per map entry,
328
+ * unlike affine BGs). Index 0 transparent. */
329
+ static const u32 bg_tile_grass[8] = {
330
+ 0x11111111, 0x11111111, 0x21212121, 0x22222222,
331
+ 0x22222222, 0x22222222, 0x22222222, 0x22222222,
332
+ };
333
+ static const u32 bg_tile_dirt[8] = {
334
+ 0x22222222, 0x22322222, 0x22222222, 0x22222232,
335
+ 0x22222222, 0x23222222, 0x22222222, 0x22222223,
336
+ };
337
+ static const u32 bg_tile_brick[8] = {
338
+ 0x33333333, 0x30303030, 0x33333333, 0x03030303,
339
+ 0x33333333, 0x30303030, 0x33333333, 0x03030303,
340
+ };
341
+
342
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
343
+ * SCROLLING TILE BG (BG0, Mode 0) — a REGULAR text BG, the bread-and-butter
344
+ * GBA background. A 64x32 map (BG_REG_64x32) is exactly the 512-px world, so
345
+ * the whole level fits and we never stream: the camera just writes
346
+ * REG_BG0HOFS each frame. Footguns this block dodges:
347
+ * - A 64-wide regular map is TWO 32x32 screenblocks SIDE BY SIDE (SBB n =
348
+ * left 32 cols, SBB n+1 = right 32 cols). A flat (col,row) write must
349
+ * route col<32 to the left block and col>=32 to the right — MAP_SET
350
+ * below does that. (libtonc's se_mem[] indexes one screenblock.)
351
+ * - Each map entry is a u16: tile id (10 bits) + hflip/vflip + a 4-bit
352
+ * PALBANK. SE_BUILD(tile, palbank, hflip, vflip) packs it. (Regular BGs
353
+ * carry a palbank per tile; affine BGs do NOT — that's the other idiom.)
354
+ * - VRAM ignores byte writes (a u8 store duplicates the byte into both
355
+ * halves of the 16-bit lane). We only ever write whole u16 SE entries
356
+ * and tonccpy() tile data, both VRAM-safe.
357
+ * requires: DCNT_MODE0 + DCNT_BG0, BG0CNT pointing CBB 0 / SBB 28 (so 28+29
358
+ * hold the 64-wide map), REG_BG0HOFS written every frame, BG1 (TTE) kept
359
+ * clear of SBB 28/29. */
360
+ static SCR_ENTRY *const sbbL = se_mem[28]; /* left 32 cols */
361
+ static SCR_ENTRY *const sbbR = se_mem[29]; /* right 32 cols */
362
+ #define MAP_SET(tx, ty, se) do { \
363
+ if ((tx) < 32) sbbL[(ty) * 32 + (tx)] = (se); \
364
+ else sbbR[(ty) * 32 + ((tx) - 32)] = (se); \
365
+ } while (0)
366
+
367
+ static void build_level(void) {
368
+ int tx, ty;
369
+ u8 g, pr;
370
+
371
+ pal_bg_mem[0] = RGB15(2, 3, 8); /* cave backdrop (BG backdrop) */
372
+ pal_bg_mem[1] = RGB15(10, 24, 8); /* grass green */
373
+ pal_bg_mem[2] = RGB15(14, 9, 4); /* dirt brown */
374
+ pal_bg_mem[3] = RGB15(6, 7, 13); /* brick slate */
53
375
 
54
- static int on_platform(s16 px, s16 py) {
55
- /* Returns 1 when an 8x8 player at (px, py) is standing on the
56
- * top edge of any platform. */
57
- for (int i = 0; i < N_PLATFORMS; i++) {
58
- const Rect *p = &platforms[i];
59
- if (py + 8 == p->y
60
- && px + 8 > p->x
61
- && px < p->x + p->w) {
62
- return 1;
376
+ tonccpy(&tile_mem[0][BG_GRASS], bg_tile_grass, sizeof(bg_tile_grass));
377
+ tonccpy(&tile_mem[0][BG_DIRT], bg_tile_dirt, sizeof(bg_tile_dirt));
378
+ tonccpy(&tile_mem[0][BG_BRICK], bg_tile_brick, sizeof(bg_tile_brick));
379
+
380
+ for (ty = 0; ty < 32; ty++)
381
+ for (tx = 0; tx < MAP_COLS; tx++) {
382
+ u16 se = SE_BUILD(BG_BLANK, 0, 0, 0);
383
+ g = ground_row[tx];
384
+ pr = plat_row[tx];
385
+ if (pr && ty == pr) se = SE_BUILD(BG_GRASS, 0, 0, 0); /* slab */
386
+ else if (g != NO_GROUND && ty == g) se = SE_BUILD(BG_GRASS, 0, 0, 0);
387
+ else if (g != NO_GROUND && ty > g) se = SE_BUILD(BG_DIRT, 0, 0, 0);
388
+ else if (ty < 8 && ((tx * 5 + ty * 7) & 7) == 0)
389
+ se = SE_BUILD(BG_BRICK, 0, 0, 0); /* backdrop */
390
+ MAP_SET(tx, ty, se);
63
391
  }
392
+ REG_BG0CNT = BG_CBB(0) | BG_SBB(28) | BG_REG_64x32 | BG_4BPP | BG_PRIO(2);
393
+ }
394
+
395
+ /* ── GAME LOGIC (clay) — landing probe against the column map ──────────────
396
+ * One-way platforms: catch the player only while FALLING through a narrow
397
+ * window at a surface top. Window is feet in [top-1 .. top+5] so a 5px/frame
398
+ * terminal fall can't step over it (tunnelling). Columns are WORLD columns. */
399
+ static s16 land_top(int wcol, s16 feet) {
400
+ u8 r;
401
+ s16 top;
402
+ wcol &= (MAP_COLS - 1); /* wrap: the 64-col map loops endlessly */
403
+ r = plat_row[wcol];
404
+ if (r) {
405
+ top = (s16)(r << 3);
406
+ if (feet + 1 >= top && feet <= top + 5) return top;
64
407
  }
65
- return 0;
408
+ r = ground_row[wcol];
409
+ if (r != NO_GROUND) {
410
+ top = (s16)(r << 3);
411
+ if (feet + 1 >= top && feet <= top + 5) return top;
412
+ }
413
+ return -1;
66
414
  }
67
415
 
68
- int main(void) {
69
- /* ── BG palette (for the platform tile) ──────────────────────────
70
- * pal_bg_mem[i] is the BG palette. */
71
- pal_bg_mem[0] = CLR_BLACK;
72
- pal_bg_mem[1] = CLR_GRAY; /* platform edge */
73
- pal_bg_mem[2] = RGB15(8, 8, 12);/* platform fill */
416
+ /* ── GAME LOGIC (clay) — coins (world-anchored sprite objects) ── */
417
+ static const s16 coin_heights[4] = { 88, 72, 104, 56 };
418
+ static void place_coin(u8 i, s32 wx) {
419
+ coins[i].x = wx;
420
+ coins[i].y = coin_heights[rand8() & 3];
421
+ coins[i].alive = 1;
422
+ }
74
423
 
75
- /* ── Sprite palette ──────────────────────────────────────────────
76
- * Sprite palette 0, colour 3 = red player. */
77
- pal_obj_mem[3] = CLR_RED;
424
+ /* Box overlap in WORLD coords. s32 so it stays correct as the endless camera
425
+ * grows past 16 bits overlapping objects always have a small difference. */
426
+ static int aabb(s32 ax, s32 ay, s32 bx, s32 by, s32 r) {
427
+ s32 dx = ax - bx, dy = ay - by;
428
+ if (dx < 0) dx = -dx;
429
+ if (dy < 0) dy = -dy;
430
+ return dx < r && dy < r;
431
+ }
78
432
 
79
- /* ── BG tiles char base 0 */
80
- tonccpy(&tile_mem[0][TILE_PLATFORM], tile_platform, sizeof(tile_platform));
433
+ /* ── GAME LOGIC (clay) HUD / screens (TTE on BG1, priority 0) ── */
434
+ static void draw_hud_labels(void) {
435
+ tte_erase_screen();
436
+ tte_write("#{P:8,4}SC");
437
+ tte_write("#{P:96,4}HI");
438
+ tte_write("#{P:200,4}x");
439
+ }
81
440
 
82
- /* ── Sprite tiles ─ char base 4 ─ */
83
- tonccpy(&tile_mem[4][TILE_PLAYER], tile_player, sizeof(tile_player));
441
+ static void draw_hud_numbers(void) {
442
+ tte_erase_rect(28, 4, 70, 12); draw_num(28, 4, score, 5);
443
+ tte_erase_rect(116, 4, 158, 12); draw_num(116, 4, hiscore, 5);
444
+ tte_erase_rect(210, 4, 220, 12); draw_num(210, 4, lives, 1);
445
+ }
84
446
 
85
- /* ── BG0 control ── char-block 0, 64x32 map (= 512x256 px, fits the
86
- * whole world). A 64x32 map occupies TWO screen-blocks (28 and 29);
87
- * BG1's HUD sits at SBB 30, clear of it. ── */
88
- REG_BG0CNT = BG_CBB(0) | BG_SBB(28) | BG_REG_64x32 | BG_4BPP;
89
-
90
- /* Draw platforms into the 64x32 map. A 64-wide map is laid out as two
91
- * 32x32 screen-blocks side by side: the left 32 cols in SBB 28, the
92
- * right 32 cols in SBB 29. se_index() handles that mapping for us, but
93
- * we can address it as a flat 64-wide grid via the helper below. */
94
- SCR_ENTRY *sbbL = se_mem[28];
95
- SCR_ENTRY *sbbR = se_mem[29];
96
- #define MAP_SET(tx, ty, se) do { \
97
- if ((tx) < 32) sbbL[(ty) * 32 + (tx)] = (se); \
98
- else sbbR[(ty) * 32 + ((tx) - 32)] = (se); \
99
- } while (0)
100
- for (int ty = 0; ty < 32; ty++)
101
- for (int tx = 0; tx < 64; tx++)
102
- MAP_SET(tx, ty, SE_BUILD(TILE_BLANK, 0, 0, 0));
103
- for (int i = 0; i < N_PLATFORMS; i++) {
104
- const Rect *p = &platforms[i];
105
- int tx0 = p->x >> 3, ty0 = p->y >> 3;
106
- int tw = (p->w + 7) >> 3, th = (p->h + 7) >> 3;
107
- for (int dy = 0; dy < th; dy++)
108
- for (int dx = 0; dx < tw; dx++) {
109
- int tx = tx0 + dx, ty = ty0 + dy;
110
- if (tx < 64 && ty < 32) MAP_SET(tx, ty, SE_BUILD(TILE_PLATFORM, 0, 0, 0));
447
+ static void enter_title(void) {
448
+ state = ST_TITLE;
449
+ tte_erase_screen();
450
+ tte_write("#{P:60,40}" GAME_TITLE);
451
+ tte_write("#{P:76,80}PRESS START");
452
+ tte_write("#{P:88,100}HI");
453
+ draw_num(112, 100, hiscore, 5);
454
+ tte_write("#{P:40,128}DPAD MOVE - A JUMP");
455
+ }
456
+
457
+ static void enter_play(void) {
458
+ int i;
459
+ state = ST_PLAY;
460
+ px = 24; py_q4 = (s32)(112 << 4); vy_q4 = 0; on_ground = 1;
461
+ cam_x = 0; dist_sub = 0;
462
+ score = 0; lives = START_LIVES; frame = 0;
463
+ for (i = 0; i < NUM_COINS; i++)
464
+ place_coin((u8)i, (s16)(80 + i * 130));
465
+ gear_wx = 384; gear_wy = 116; /* hovers over the mid-level run */
466
+ gear_theta = 0; gear_pulse = 0;
467
+ draw_hud_labels();
468
+ draw_hud_numbers();
469
+ }
470
+
471
+ static void enter_over(void) {
472
+ state = ST_OVER;
473
+ if (score > hiscore) {
474
+ hiscore = score;
475
+ hiscore_save(hiscore); /* byte-wise SRAM write — see the idiom */
476
+ draw_hud_numbers();
477
+ }
478
+ tte_write("#{P:84,64}GAME OVER");
479
+ tte_write("#{P:76,84}PRESS START");
480
+ }
481
+
482
+ static void lose_life(void) {
483
+ sfx_noise(14);
484
+ if (lives > 0) lives--;
485
+ draw_hud_numbers();
486
+ /* respawn at the player's fixed screen lane, on the ground (the camera is
487
+ * one-way and never resets — the run keeps moving forward). */
488
+ px = (s16)(cam_x + SCROLL_WALL);
489
+ py_q4 = (s32)((GROUND_ROW * 8 - 8) << 4);
490
+ vy_q4 = 0; on_ground = 1;
491
+ /* shove the gear far ahead so we don't respawn straight onto it */
492
+ gear_wx = (s16)(cam_x + SCREEN_W + 120);
493
+ if (lives == 0) enter_over();
494
+ }
495
+
496
+ /* ── GAME LOGIC (clay) — one ST_PLAY tick ── */
497
+ static void update_play(void) {
498
+ int i;
499
+ s16 ipy, feet, npy, top;
500
+
501
+ /* Horizontal: the player walks up to SCROLL_WALL (screen x), then holds
502
+ * that lane while the WORLD scrolls under them — a one-way endless runner.
503
+ * cam_x grows without bound; only the BG register and column lookups wrap
504
+ * (mod 512 / mod 64), so the 64x32 map loops seamlessly. */
505
+ if (key_held(KEY_LEFT) && px > cam_x + 8) px -= MOVE_SPEED;
506
+ if (key_held(KEY_RIGHT)) {
507
+ s16 screen_x = (s16)(px - cam_x);
508
+ px += MOVE_SPEED;
509
+ if (screen_x >= SCROLL_WALL) {
510
+ cam_x += MOVE_SPEED; /* world scrolls (camera leads) */
511
+ dist_sub += MOVE_SPEED;
512
+ if (dist_sub >= 64) { dist_sub -= 64; if (score < 65000u) { score++; draw_hud_numbers(); } }
513
+ }
514
+ }
515
+
516
+ /* Jump. */
517
+ if (key_hit(KEY_A) && on_ground) {
518
+ vy_q4 = JUMP_VEL;
519
+ on_ground = 0;
520
+ sfx_tone(1, 1500, 6); /* boing */
521
+ }
522
+
523
+ /* Gravity + sub-pixel Y. */
524
+ if (vy_q4 < MAX_FALL) vy_q4 += GRAVITY;
525
+ ipy = (s16)(py_q4 >> 4);
526
+ npy = (s16)((py_q4 + vy_q4) >> 4);
527
+
528
+ /* Fell into a pit (below the playfield) → lose a life. */
529
+ if (npy > SCREEN_H + 8) { lose_life(); return; }
530
+
531
+ /* Landing: probe the world columns under the player's feet, while falling.
532
+ * land_top wraps the column (& 63), so the loop's pits/slabs keep coming. */
533
+ if (vy_q4 >= 0) {
534
+ feet = (s16)(npy + 8);
535
+ top = land_top(px >> 3, feet);
536
+ if (top < 0) top = land_top((px + 7) >> 3, feet);
537
+ if (top >= 0 && ipy + 8 <= top + 6) {
538
+ py_q4 = (s32)((top - 8) << 4);
539
+ if (!on_ground) sfx_tone(2, 800, 3); /* landing thud */
540
+ vy_q4 = 0; on_ground = 1;
541
+ } else {
542
+ py_q4 += vy_q4;
543
+ on_ground = 0;
544
+ }
545
+ } else {
546
+ py_q4 += vy_q4; /* rising */
547
+ }
548
+
549
+ /* Gear hazard: spin + pulse, world-anchored (drifts with the scroll). The
550
+ * collision is the UNROTATED box around the gear center — honest
551
+ * simplification; a rotating hitbox buys little for a round gear. Once it
552
+ * slides off the left, recycle it ahead at a fresh height. */
553
+ gear_theta = (u16)(gear_theta + 0x0300); /* ~4.2°/frame */
554
+ gear_pulse = (u16)(gear_pulse + 0x0180);
555
+ if (gear_wx < cam_x - 40) {
556
+ gear_wx = (s16)(cam_x + SCREEN_W + 40 + (rand8() & 63));
557
+ gear_wy = (s16)(96 + (rand8() & 31));
558
+ }
559
+ {
560
+ s32 plx = px, ply = (py_q4 >> 4);
561
+ if (aabb(plx + 4, ply + 4, gear_wx, gear_wy, 16)) {
562
+ lose_life();
563
+ if (state != ST_PLAY) return;
564
+ }
565
+ }
566
+
567
+ /* Coins: collect on overlap, recycle ahead of the camera. */
568
+ {
569
+ s32 plx = px, ply = (py_q4 >> 4);
570
+ for (i = 0; i < NUM_COINS; i++) {
571
+ if (!coins[i].alive) continue;
572
+ if (aabb(plx + 4, ply + 4, coins[i].x + 4, coins[i].y + 4, 8)) {
573
+ coins[i].alive = 0;
574
+ if (score < 65000u) score += 10;
575
+ draw_hud_numbers();
576
+ sfx_tone(1, 1900, 4); /* coin ping */
111
577
  }
578
+ /* recycle a coin once it's well behind the camera */
579
+ if (coins[i].x < cam_x - 16)
580
+ place_coin((u8)i, (s16)(cam_x + SCREEN_W + (rand8() & 63)));
581
+ }
582
+ }
583
+ }
584
+
585
+ /* ── GAME LOGIC (clay) — stage the regular sprites (the gear has its own
586
+ * idiom block). Off-screen objects park at y=200; either works for REGULAR
587
+ * sprites. The gear is staged in SCREEN space = world - camera. ── */
588
+ static void stage_sprites(void) {
589
+ int i;
590
+ int playing = (state == ST_PLAY);
591
+ s16 sx = (s16)(px - cam_x), sy = (s16)(py_q4 >> 4);
592
+
593
+ obj_set_attr(&obj_buffer[SLOT_PLAYER], ATTR0_SQUARE, ATTR1_SIZE_8,
594
+ ATTR2_PALBANK(0) | TILE_PLAYER);
595
+ obj_set_pos(&obj_buffer[SLOT_PLAYER], playing ? sx : 250, playing ? sy : 200);
596
+
597
+ for (i = 0; i < NUM_COINS; i++) {
598
+ obj_set_attr(&obj_buffer[SLOT_COIN + i], ATTR0_SQUARE, ATTR1_SIZE_8,
599
+ ATTR2_PALBANK(1) | TILE_COIN);
600
+ obj_set_pos(&obj_buffer[SLOT_COIN + i],
601
+ (playing && coins[i].alive) ? (s16)(coins[i].x - cam_x) : 250,
602
+ (playing && coins[i].alive) ? coins[i].y : 200);
603
+ }
604
+
605
+ if (playing) {
606
+ gear_stage((s16)(gear_wx - cam_x), gear_wy);
607
+ } else {
608
+ obj_buffer[SLOT_GEAR].attr0 = ATTR0_HIDE; /* REGULAR + hide (footgun 3) */
112
609
  }
610
+ }
611
+
612
+ int main(void) {
613
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
614
+ * Init order: tiles/palettes → oam_init → irq_init + II_VBLANK →
615
+ * TTE init → DISPCNT last. VBlankIntrWait() HANGS FOREVER without the
616
+ * vblank IRQ registered (the #1 "frozen on frame 1" cause), and
617
+ * enabling DISPCNT layers before their tiles/maps exist flashes garbage.
618
+ * TTE owns BG1 (CBB 2 / SBB 30) — keep other layers off those blocks.
619
+ * requires: nothing prior; this IS the boot. */
620
+ tonccpy(&tile_mem[4][TILE_PLAYER], tile_player, sizeof(tile_player));
621
+ tonccpy(&tile_mem[4][TILE_COIN], tile_coin, sizeof(tile_coin));
622
+ gear_build_tiles();
623
+ pal_obj_bank[0][1] = RGB15(31, 31, 31); /* player eyes white */
624
+ pal_obj_bank[0][3] = RGB15(28, 8, 8); /* player red */
625
+ pal_obj_bank[1][2] = RGB15(28, 24, 6); /* coin gold */
626
+ pal_obj_bank[1][4] = RGB15(31, 31, 18); /* coin shine */
113
627
 
114
- oam_init(obj_buffer, 128);
628
+ build_level(); /* regular BG0: tiles + 64x32 map */
629
+ oam_init(obj_buffer, 128); /* hides all 128, matrices = identity */
115
630
 
116
- /* IRQ setup — required for VBlankIntrWait() to function. */
117
631
  irq_init(NULL);
118
632
  irq_add(II_VBLANK, NULL);
119
633
 
120
- sfx_init();
634
+ sfx_init(); /* APU on; music loop ticks below */
121
635
 
122
- /* ── TTE for hint text on a SECOND BG ────────────────────────────
123
- * BG1 at screen-block 30, char-block 2 (TTE chr4c packs glyphs
124
- * starting around tile 0 of its char-block). */
636
+ /* TTE text on BG1 (4bpp char block 2, screenblock 30), priority 0 so
637
+ * text draws over everything. Mode 0 = all four BGs regular/tiled. */
125
638
  tte_init_chr4c_default(1, BG_CBB(2) | BG_SBB(30));
126
- tte_write("#{P:64,8}D-PAD MOVE A JUMP");
127
-
639
+ REG_BG1CNT |= BG_PRIO(0);
128
640
  REG_DISPCNT = DCNT_MODE0 | DCNT_BG0 | DCNT_BG1 | DCNT_OBJ | DCNT_OBJ_1D;
129
641
 
130
- /* Subpixel state1 pixel = 16 subpixels. WORLD coords. */
131
- s32 px = 32 << 4, py = 60 << 4;
132
- s32 vx = 0, vy = 0;
133
- s32 camX = 0;
134
-
135
- const s32 GRAVITY = 12;
136
- const s32 MOVE_SPEED = 24;
137
- const s32 JUMP_VEL = -200;
138
- const s32 MAX_FALL = 320;
139
-
140
- u16 prev = 0;
642
+ hiscore = hiscore_load(); /* cartridge SRAM0 on first boot */
643
+ enter_title();
141
644
 
142
645
  while (1) {
646
+ /* Idiomatic Tonc heartbeat: wait vblank, poll keys, update, then
647
+ * commit OAM + affine slot while still inside vblank (the whole
648
+ * update is far quicker than the 4.9ms vblank window). */
143
649
  VBlankIntrWait();
144
650
  key_poll();
651
+ sfx_music_tick(); /* forget this → silent game */
652
+ frame++;
145
653
 
146
- vx = 0;
147
- if (key_held(KEY_LEFT)) vx = -MOVE_SPEED;
148
- if (key_held(KEY_RIGHT)) vx = MOVE_SPEED;
654
+ if (state == ST_TITLE) {
655
+ if (key_hit(KEY_START | KEY_A)) enter_play();
656
+ } else if (state == ST_OVER) {
657
+ if (key_hit(KEY_START)) enter_title();
658
+ } else {
659
+ update_play();
660
+ }
149
661
 
150
- s16 ipx = px >> 4;
151
- s16 ipy = py >> 4;
152
- int grounded = on_platform(ipx, ipy);
662
+ /* The 64x32 map is exactly 512 px wide and WRAPS in hardware, so
663
+ * masking cam_x to 9 bits makes the level loop seamlessly under an
664
+ * ever-growing camera. */
665
+ if (state == ST_PLAY) REG_BG0HOFS = (u16)(cam_x & 511);
666
+ else REG_BG0HOFS = 0;
153
667
 
154
- u16 now = key_curr_state();
155
- if ((now & KEY_A) && !(prev & KEY_A) && grounded) {
156
- vy = JUMP_VEL;
157
- sfx_tone(1, 1500, 6); /* boing */
158
- }
159
- prev = now;
160
-
161
- vy += GRAVITY;
162
- if (vy > MAX_FALL) vy = MAX_FALL;
163
- if (grounded && vy > 0) vy = 0;
164
-
165
- /* Horizontal — clamp to the world (it scrolls now, no wrap). */
166
- px += vx;
167
- if (px < 0) px = 0;
168
- if (px > ((WORLD_W - 8) << 4)) px = (WORLD_W - 8) << 4;
169
-
170
- /* Vertical with platform-stop. */
171
- s32 np = py + vy;
172
- s16 npy = np >> 4;
173
- /* THE fall-through-the-floor fix: this used to be additionally
174
- * gated on blocked_below(), which only matches when a platform
175
- * top is within ONE pixel of the feet — but falls reach 20 px/
176
- * frame, so the (correct) crossing test below almost never got
177
- * to run and the player tunnelled through every platform. The
178
- * crossing test alone is the right check. */
179
- if (vy > 0) {
180
- for (int i = 0; i < N_PLATFORMS; i++) {
181
- const Rect *p = &platforms[i];
182
- if (ipy + 8 <= p->y && npy + 8 >= p->y
183
- && ipx + 8 > p->x && ipx < p->x + p->w) {
184
- py = (p->y - 8) << 4;
185
- vy = 0;
186
- sfx_tone(2, 800, 3); /* thud */
187
- goto done_y;
188
- }
189
- }
190
- }
191
- py = np;
192
- if (py > (160 << 4)) { py = 0; vy = 0; }
193
- done_y:
194
-
195
- /* Camera follows player, centered, clamped to the world. Write the
196
- * BG0 horizontal scroll offset. BG1 (HUD) is left un-scrolled. */
197
- camX = (px >> 4) - (SCREEN_W / 2 - 4);
198
- if (camX < 0) camX = 0;
199
- if (camX > WORLD_W - SCREEN_W) camX = WORLD_W - SCREEN_W;
200
- REG_BG0HOFS = camX;
201
-
202
- /* Player sprite drawn in SCREEN space = world - camera. */
203
- obj_set_attr(&obj_buffer[0],
204
- ATTR0_SQUARE,
205
- ATTR1_SIZE_8,
206
- ATTR2_PALBANK(0) | TILE_PLAYER);
207
- obj_set_pos(&obj_buffer[0], (px >> 4) - camX, py >> 4);
208
- oam_copy(oam_mem, obj_buffer, 1);
668
+ stage_sprites();
669
+ oam_copy(oam_mem, obj_buffer, 128); /* sprites AND affine slot 0 */
209
670
  }
210
671
  return 0;
211
672
  }