romdevtools 0.28.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/AGENTS.md +53 -43
  2. package/CHANGELOG.md +91 -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 +12 -12
  88. package/src/cores/wasm/bluemsx_libretro.js +1 -1
  89. package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
  90. package/src/cores/wasm/fceumm_libretro.js +1 -1
  91. package/src/cores/wasm/fceumm_libretro.wasm +0 -0
  92. package/src/cores/wasm/gambatte_libretro.js +1 -1
  93. package/src/cores/wasm/gambatte_libretro.wasm +0 -0
  94. package/src/cores/wasm/geargrafx_libretro.js +1 -1
  95. package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
  96. package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
  97. package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
  98. package/src/cores/wasm/handy_libretro.js +1 -1
  99. package/src/cores/wasm/handy_libretro.wasm +0 -0
  100. package/src/cores/wasm/mgba_libretro.js +1 -1
  101. package/src/cores/wasm/mgba_libretro.wasm +0 -0
  102. package/src/cores/wasm/prosystem_libretro.js +1 -1
  103. package/src/cores/wasm/prosystem_libretro.wasm +0 -0
  104. package/src/cores/wasm/snes9x_libretro.js +1 -1
  105. package/src/cores/wasm/snes9x_libretro.wasm +0 -0
  106. package/src/cores/wasm/stella2014_libretro.js +1 -1
  107. package/src/cores/wasm/stella2014_libretro.wasm +0 -0
  108. package/src/cores/wasm/vice_x64_libretro.js +1 -1
  109. package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
  110. package/src/host/LibretroHost.js +84 -8
  111. package/src/http/tool-registry.js +11 -11
  112. package/src/mcp/tools/cheats.js +2 -1
  113. package/src/mcp/tools/frame.js +3 -2
  114. package/src/mcp/tools/index.js +3 -3
  115. package/src/mcp/tools/input.js +5 -4
  116. package/src/mcp/tools/lifecycle.js +6 -4
  117. package/src/mcp/tools/memory.js +131 -24
  118. package/src/mcp/tools/platform-docs.js +1 -1
  119. package/src/mcp/tools/preview-tile.js +6 -2
  120. package/src/mcp/tools/project.js +1098 -130
  121. package/src/mcp/tools/record.js +6 -7
  122. package/src/mcp/tools/rom-id.js +5 -1
  123. package/src/mcp/tools/run-until.js +12 -4
  124. package/src/mcp/tools/snippets.js +6 -6
  125. package/src/mcp/tools/sprite-pipeline.js +14 -2
  126. package/src/mcp/tools/state.js +2 -1
  127. package/src/mcp/tools/tile-inspect.js +8 -1
  128. package/src/mcp/tools/toolchain.js +12 -1
  129. package/src/mcp/tools/watch-memory.js +53 -10
  130. package/src/observer/bus.js +73 -0
  131. package/src/observer/livestream.html +4 -2
  132. package/src/observer/tool-wrap.js +17 -14
  133. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +32 -3
  134. package/src/platforms/atari7800/MENTAL_MODEL.md +5 -5
  135. package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
  136. package/src/platforms/c64/MENTAL_MODEL.md +11 -4
  137. package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
  138. package/src/platforms/gb/MENTAL_MODEL.md +3 -3
  139. package/src/platforms/gb/TROUBLESHOOTING.md +61 -8
  140. package/src/platforms/gb/lib/c/README.md +10 -11
  141. package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
  142. package/src/platforms/gb/lib/c/patch-header.js +13 -3
  143. package/src/platforms/gba/MENTAL_MODEL.md +4 -4
  144. package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
  145. package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
  146. package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
  147. package/src/platforms/gbc/MENTAL_MODEL.md +4 -4
  148. package/src/platforms/gbc/TROUBLESHOOTING.md +4 -4
  149. package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
  150. package/src/platforms/gbc/lib/c/README.md +10 -11
  151. package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
  152. package/src/platforms/gbc/lib/c/patch-header.js +13 -3
  153. package/src/platforms/genesis/MENTAL_MODEL.md +3 -3
  154. package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
  155. package/src/platforms/gg/MENTAL_MODEL.md +4 -4
  156. package/src/platforms/gg/TROUBLESHOOTING.md +3 -3
  157. package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
  158. package/src/platforms/gg/lib/c/joypad_read.c +29 -0
  159. package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
  160. package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
  161. package/src/platforms/msx/MENTAL_MODEL.md +5 -5
  162. package/src/platforms/msx/TROUBLESHOOTING.md +2 -2
  163. package/src/platforms/msx/lib/c/msx_hw.h +1 -0
  164. package/src/platforms/msx/lib/c/msx_vdp.c +25 -0
  165. package/src/platforms/nes/MENTAL_MODEL.md +2 -2
  166. package/src/platforms/nes/lib/c/nes_runtime.c +149 -34
  167. package/src/platforms/nes/lib/c/nes_runtime.h +34 -1
  168. package/src/platforms/pce/MENTAL_MODEL.md +5 -5
  169. package/src/platforms/pce/TROUBLESHOOTING.md +1 -1
  170. package/src/platforms/pce/lib/c/pce_hw.h +11 -0
  171. package/src/platforms/pce/lib/c/pce_video.c +32 -0
  172. package/src/platforms/sms/MENTAL_MODEL.md +6 -6
  173. package/src/platforms/snes/MENTAL_MODEL.md +2 -2
  174. package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
  175. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
  176. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
  177. package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
  178. package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
  179. package/src/toolchains/index.js +27 -11
@@ -1,56 +1,66 @@
1
- /* ── shmup.c — Game Boy Advance Tonc vertical-shooter scaffold ──────
1
+ /* ── shmup.c — Game Boy Advance vertical shooter (complete example game) ─────
2
2
  *
3
- * Complete runnable vertical-shmup baseline:
4
- * - Player ship (1 sprite, d-pad moves, A fires)
5
- * - 6 bullet slots, 6 enemy slots (fixed-size object pools)
6
- * - Enemy wave spawner: one enemy from the top every ~28 frames
7
- * - Linear movement, AABB collision (8x8 vs 8x8)
8
- * - 16-bit score, rendered as text via TTE (Tonc Text Engine)
3
+ * A COMPLETE, working game — title screen, score + persistent hi-score
4
+ * (cartridge SRAM), music + SFX, waves of enemies, and the GBA's signature
5
+ * hardware feature shown BOTH ways it exists:
6
+ * - an AFFINE BACKGROUND: the playfield backdrop is a vortex on BG2 that
7
+ * rotates and pulses (Mode 1, REG_BG2PA..PD matrix + BG2X/Y reference)
8
+ * - an AFFINE SPRITE: the wave boss is a 32x32 OBJ that spins and
9
+ * scale-pulses as it attacks (OAM affine parameter slot 0, double-size)
9
10
  *
10
- * Sprite slot discipline (GBA has 128 OAM entries, more than enough):
11
- * slot 0 → player
12
- * slot 1..6 → bullets
13
- * slot 7..12 → enemies
14
- * total 13 << 128 no flicker
11
+ * THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
12
+ * very different one. The markers tell you what's what:
13
+ * HARDWARE IDIOM (load-bearing) — dodges a documented GBA footgun; reshape
14
+ * your gameplay around it (see TROUBLESHOOTING before changing).
15
+ * GAME LOGIC (clay) enemy patterns, scoring, tuning, art: reshape freely.
15
16
  *
16
- * Idiomatic Tonc frame heartbeat: VBlankIntrWait() + key_poll() at the
17
- * top of every frame, oam_copy() at the bottom. Update positions in a
18
- * shadow OAM buffer (obj_buffer[]); copy to hardware OAM in one DMA.
17
+ * What depends on what:
18
+ * gba_sfx.{h,c} PSG sound: sfx_tone/sfx_noise one-shots + the music loop
19
+ * (sfx_music_tick once per frame forget it and the game is silent).
20
+ * libtonc (the build links it) — VBlankIntrWait/key_poll/OAM/TTE.
21
+ *
22
+ * HANDHELD, SO SINGLE-PLAYER ONLY (honest note): 2P on GBA means a link
23
+ * cable between two units — a second emulator instance this environment
24
+ * can't provide. Title is press-start, no mode select.
25
+ *
26
+ * Frame budget: ARM7TDMI at 16.78MHz with this object count (1+6+6+boss)
27
+ * doesn't come close to a full frame; the affine math is a handful of
28
+ * multiplies per frame, not per pixel — the PPU does the per-pixel work.
19
29
  */
20
30
 
21
31
  #include <tonc.h>
22
32
  #include "gba_sfx.h"
23
33
 
34
+ /* The title screen renders this — examples({op:'fork'}) stamps your game's
35
+ * name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
36
+ #define GAME_TITLE "GYRE GUNNER"
37
+
38
+ /* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
39
+ * Object pools — fixed slots, no allocation. Sprite slot discipline (128 OAM
40
+ * entries total, we use 14):
41
+ * slot 0 → player
42
+ * slot 1..6 → bullets
43
+ * slot 7..12 → enemies
44
+ * slot 13 → boss (AFFINE — uses OAM affine parameter slot 0; see the
45
+ * affine-sprite idiom below for why slot CHOICE matters)
46
+ */
24
47
  #define MAX_BULLETS 6
25
48
  #define MAX_ENEMIES 6
49
+ #define SLOT_PLAYER 0
50
+ #define SLOT_BULLET 1
51
+ #define SLOT_ENEMY 7
52
+ #define SLOT_BOSS 13
26
53
 
27
- #define TILE_BLANK 0
28
54
  #define TILE_SHIP 1
29
55
  #define TILE_BULLET 2
30
56
  #define TILE_ENEMY 3
57
+ #define TILE_BOSS 16 /* 32x32 4bpp = 16 tiles, ids 16..31 */
31
58
 
32
- /* Draw a 5-digit score at pixel (x,8) WITHOUT tte_printf. The bundled libtonc's
33
- * tte_printf with a %d/%05d conversion is broken (it routes through a vsnprintf
34
- * path that isn't wired in this build — it garbles the output AND wedges the
35
- * game loop when called per-frame, GBA-1). We build the string ourselves and
36
- * use tte_write, which processes the #{P:x,y} position command but does NO
37
- * format conversion → safe every frame. */
38
- static void draw_score(int x, unsigned v) {
39
- char buf[24];
40
- int i, n = 0;
41
- /* "#{P:<x>,8}" position command, then 5 decimal digits. */
42
- buf[n++]='#'; buf[n++]='{'; buf[n++]='P'; buf[n++]=':';
43
- if (x >= 100) buf[n++] = '0' + (x/100)%10;
44
- if (x >= 10) buf[n++] = '0' + (x/10)%10;
45
- buf[n++] = '0' + x%10;
46
- buf[n++]=','; buf[n++]='8'; buf[n++]='}';
47
- for (i = 4; i >= 0; i--) { buf[n+i] = '0' + (v % 10); v /= 10; }
48
- n += 5; buf[n] = 0;
49
- tte_write(buf);
50
- }
59
+ #define START_LIVES 3
60
+ #define WAVE_KILLS 10 /* kills before the wave boss appears */
51
61
 
52
- /* 4bpp tiles (8 rows × 32 bits each = 32 bytes). Each nibble is a
53
- * palette index. Index 0 = transparent. */
62
+ /* 4bpp sprite tiles (8 rows × 32 bits each = 32 bytes). Each nibble is a
63
+ * palette index within the sprite's palbank. Index 0 = transparent. */
54
64
  static const u32 tile_ship[8] = {
55
65
  0x00011000, 0x00011000, 0x00111100, 0x00111100,
56
66
  0x01111110, 0x01111110, 0x11111111, 0x11000011,
@@ -64,202 +74,577 @@ static const u32 tile_enemy[8] = {
64
74
  0x33333333, 0x03333330, 0x30000003, 0x03000030,
65
75
  };
66
76
 
67
- /* ── Starfield backdrop tiles (4bpp) ─────────────────────────────────
68
- * Two space-coloured fill tiles (palette indices 4 and 5) laid in
69
- * vertical bands so the whole BG is filled — NOT a flat blank backdrop.
70
- * A handful of star pixels (index 6) are punched into each tile so the
71
- * field reads as space rather than a solid block. */
72
- static const u32 tile_star_a[8] = {
73
- 0x44464444, 0x44444444, 0x46444444, 0x44444644,
74
- 0x44444444, 0x64444444, 0x44444444, 0x44446444,
75
- };
76
- static const u32 tile_star_b[8] = {
77
- 0x55555555, 0x55655555, 0x55555555, 0x55555565,
78
- 0x65555555, 0x55555555, 0x55556555, 0x55555555,
79
- };
80
-
81
77
  typedef struct { s16 x, y; u16 alive; } Obj;
82
78
 
83
79
  static OBJ_ATTR obj_buffer[128];
80
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
81
+ * OAM AFFINE SLOT LAYOUT. There is no separate affine-matrix memory: the 32
82
+ * OBJ_AFFINE parameter sets live INTERLEAVED inside OAM itself, in the
83
+ * 16-bit "fill" field of every OBJ_ATTR (4 sprites × 8 bytes carry one
84
+ * 8-byte matrix between them — pa in sprite 4n, pb in 4n+1, pc in 4n+2,
85
+ * pd in 4n+3). Casting the shadow-OAM buffer to OBJ_AFFINE* is the whole
86
+ * trick: obj_aff_buffer[k] aliases the fill words of sprites 4k..4k+3, and
87
+ * one oam_copy() of the full buffer commits sprites AND matrices together.
88
+ * Consequences you must respect:
89
+ * - oam_init() already set all 32 matrices to identity (pa=pd=0x0100).
90
+ * - NEVER memset OBJ_ATTRs to 0 — that zeroes the interleaved matrices
91
+ * (pa=0 means "scale by infinity": every affine sprite vanishes).
92
+ * - Matrix slot k is INDEPENDENT of which sprite uses it (attr1 AFF_ID
93
+ * picks any of the 32) — but the bytes live under sprites 4k..4k+3.
94
+ * requires: obj_buffer staged with oam_init(), committed with oam_copy(). */
95
+ static OBJ_AFFINE *const obj_aff_buffer = (OBJ_AFFINE *)obj_buffer;
84
96
 
85
97
  static Obj player;
86
98
  static Obj bullets[MAX_BULLETS];
87
99
  static Obj enemies[MAX_ENEMIES];
88
- static u16 score;
100
+ static u16 score, hiscore;
101
+ static u8 lives;
102
+ static u8 wave; /* 1-based; bumps each boss defeat */
103
+ static u8 kills; /* kills this wave (boss gate) */
89
104
  static u16 spawn_timer;
105
+ static u16 frame; /* free-running frame counter (drives the vortex) */
90
106
 
91
- static int aabb_hit(const Obj *a, const Obj *b) {
92
- return (a->x < b->x + 8) && (a->x + 8 > b->x)
93
- && (a->y < b->y + 8) && (a->y + 8 > b->y);
107
+ /* Boss state the affine sprite showcase. */
108
+ static u8 boss_active;
109
+ static s16 boss_x, boss_y; /* CENTER of the boss, in screen pixels */
110
+ static u8 boss_hp;
111
+ static u16 boss_theta; /* rotation angle: full circle = 0x10000 */
112
+ static u16 boss_pulse; /* scale-pulse phase */
113
+
114
+ /* Game states — the shell every example shares: title → play → game over. */
115
+ #define ST_TITLE 0
116
+ #define ST_PLAY 1
117
+ #define ST_OVER 2
118
+ static u8 state;
119
+
120
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
121
+ * PERSISTENT SRAM at 0x0E000000. Two footguns, both fatal-but-silent:
122
+ * 1. The SRAM bus is 8 BITS WIDE. Byte reads/writes only — a u16/u32
123
+ * access doesn't fault, it just reads the same byte mirrored (and a
124
+ * wide write stores one byte), so your data "almost" round-trips and
125
+ * then the checksum never matches. Every access below is via vu8.
126
+ * 2. Emulators and flashcarts detect the SAVE TYPE by scanning the ROM
127
+ * image for a marker string. Without "SRAM_V" in the ROM, mGBA gives
128
+ * the cart NO save memory at all and writes to 0x0E000000 vanish.
129
+ * The aligned, (used)-attributed const below plants that marker —
130
+ * delete it and persistence dies even though this code is untouched.
131
+ * Layout: 'V' 'X' score-lo score-hi checksum (xor ^ 0xA5) — magic+checksum
132
+ * so a fresh (0xFF-filled) cart reads as "no record" instead of garbage.
133
+ * requires: nothing else — self-contained; safe to transplant whole. */
134
+ #define SRAM_BYTE ((volatile u8 *)0x0E000000)
135
+ __attribute__((used, aligned(4))) static const char sram_type_marker[] = "SRAM_V113";
136
+
137
+ static u16 hiscore_load(void) {
138
+ u8 lo, hi;
139
+ if (SRAM_BYTE[0] != 'V' || SRAM_BYTE[1] != 'X') return 0;
140
+ lo = SRAM_BYTE[2];
141
+ hi = SRAM_BYTE[3];
142
+ if (SRAM_BYTE[4] != (u8)(lo ^ hi ^ 0xA5)) return 0;
143
+ return (u16)(lo | (hi << 8));
144
+ }
145
+
146
+ static void hiscore_save(u16 v) {
147
+ SRAM_BYTE[0] = 'V';
148
+ SRAM_BYTE[1] = 'X';
149
+ SRAM_BYTE[2] = (u8)v;
150
+ SRAM_BYTE[3] = (u8)(v >> 8);
151
+ SRAM_BYTE[4] = (u8)((u8)v ^ (u8)(v >> 8) ^ 0xA5);
152
+ }
153
+
154
+ /* ── GAME LOGIC (clay) — TTE text helpers ────────────────────────────────────
155
+ * Draw right-aligned decimal digits at pixel (x,y) WITHOUT tte_printf. The
156
+ * bundled libtonc's tte_printf with a %d conversion is broken (it routes
157
+ * through a vsnprintf path that isn't wired in this build — it garbles
158
+ * output AND wedges the loop when called per-frame, GBA-1). We build the
159
+ * string ourselves and use tte_write, which processes the #{P:x,y} position
160
+ * command but does NO format conversion → safe every frame. */
161
+ static void draw_num(int x, int y, unsigned v, int digits) {
162
+ char buf[24];
163
+ int i, n = 0;
164
+ buf[n++] = '#'; buf[n++] = '{'; buf[n++] = 'P'; buf[n++] = ':';
165
+ if (x >= 100) buf[n++] = (char)('0' + (x / 100) % 10);
166
+ if (x >= 10) buf[n++] = (char)('0' + (x / 10) % 10);
167
+ buf[n++] = (char)('0' + x % 10);
168
+ buf[n++] = ',';
169
+ if (y >= 100) buf[n++] = (char)('0' + (y / 100) % 10);
170
+ if (y >= 10) buf[n++] = (char)('0' + (y / 10) % 10);
171
+ buf[n++] = (char)('0' + y % 10);
172
+ buf[n++] = '}';
173
+ for (i = digits - 1; i >= 0; i--) { buf[n + i] = (char)('0' + (v % 10)); v /= 10; }
174
+ n += digits; buf[n] = 0;
175
+ tte_write(buf);
176
+ }
177
+
178
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
179
+ * AFFINE BACKGROUND (BG2, Mode 1) — the GBA's "Mode 7": one background the
180
+ * PPU rotates/scales per frame for free. This block owns four matrix
181
+ * registers and one reference point:
182
+ *
183
+ * REG_BG2PA..PD — a 2x2 matrix in 8.8 FIXED POINT (256 == 1.0) that maps
184
+ * SCREEN pixels → TEXTURE pixels: tex = P · (screen - origin) + ref.
185
+ * Because it maps screen→texture (the INVERSE of "how is the image
186
+ * transformed"), a matrix that SAMPLES texture 2px per screen px makes
187
+ * the image look HALF size: bigger pa = smaller image. To zoom IN by z,
188
+ * write 1/z; to rotate the image one way, write the matrix of the other.
189
+ * REG_BG2X/Y — the texture point sampled at screen pixel (0,0), in 20.8
190
+ * fixed point. Without compensation the bg rotates around the screen's
191
+ * TOP-LEFT. To pivot around screen center (cx,cy)=(120,80) anchored at
192
+ * texture point (tx,ty): BG2X = (tx<<8) - (pa*cx + pb*cy) (same shape
193
+ * for Y with pc/pd) — i.e. "walk back from the anchor by half a screen
194
+ * through the matrix".
195
+ *
196
+ * The math, spelled out (libtonc's bg_aff_rotscale does the same):
197
+ * lu_sin/lu_cos take a u16 angle (full circle = 0x10000) and return 4.12
198
+ * fixed → >>4 converts to 8.8. For rotation θ and zoom z (8.8):
199
+ * inv = 65536/z (8.8 reciprocal: 1/z)
200
+ * pa = cos·inv>>8 pb = -sin·inv>>8
201
+ * pc = sin·inv>>8 pd = cos·inv>>8
202
+ *
203
+ * Footguns this block already dodges:
204
+ * - These 6 registers are WRITE-ONLY. You cannot read-modify-update;
205
+ * keep your angle/zoom in variables (boss_theta-style) and rewrite ALL
206
+ * of them every frame.
207
+ * - Affine BGs are ALWAYS 8bpp, and the map is 1 BYTE per tile (no flip
208
+ * bits, no palbank — plain tile index), unlike regular BGs' u16 entries.
209
+ * - VRAM IGNORES BYTE WRITES (a u8 store writes the byte TWICE into the
210
+ * 16-bit lane). Building tiles/map in a work-RAM staging buffer and
211
+ * tonccpy()ing them over is the idiom — tonccpy is VRAM-safe.
212
+ * - BG_WRAP makes the 256x256 texture tile forever; without it everything
213
+ * outside the map edge renders as tile 0.
214
+ * requires: DCNT_MODE1 (BG2 affine there), BG2CNT pointing CBB 1 / SBB 26,
215
+ * vortex_apply() called every frame, BG palette indices 224..228 (bank 14
216
+ * — bank 15 belongs to TTE; see the palette footgun at vortex_build). */
217
+ static void vortex_apply(u16 theta, u32 zoom_q8) {
218
+ s32 inv = (s32)(65536u / zoom_q8); /* 8.8 ── 1/zoom */
219
+ s32 cc = ((lu_cos(theta) >> 4) * inv) >> 8; /* 8.8 ── cosθ/zoom */
220
+ s32 ss = ((lu_sin(theta) >> 4) * inv) >> 8; /* 8.8 ── sinθ/zoom */
221
+ REG_BG2PA = (s16)cc; REG_BG2PB = (s16)-ss;
222
+ REG_BG2PC = (s16)ss; REG_BG2PD = (s16)cc;
223
+ /* Pivot: texture center (128,128) shows at screen center (120,80). */
224
+ REG_BG2X = (128 << 8) - (cc * 120 + (-ss) * 80);
225
+ REG_BG2Y = (128 << 8) - (ss * 120 + cc * 80);
226
+ }
227
+
228
+ /* ── GAME LOGIC (clay) — the vortex ART (the idiom above is the machinery;
229
+ * this is just what the texture looks like — replace at will).
230
+ * 8bpp tiles are 64 bytes, 1 byte per pixel, row-major. We stage 5 tiles +
231
+ * the 32x32 one-byte-per-entry map in work RAM, then tonccpy to VRAM
232
+ * (CBB 1 tiles, SBB 26 map) per the byte-write footgun above. The texture
233
+ * needs ANGULAR content (spiral arms) or rotation is invisible, and RADIAL
234
+ * content (rings) or the zoom pulse is invisible.
235
+ * PALETTE FOOTGUN: an 8bpp BG indexes the FULL 256-color BG palette, and
236
+ * tte_init_chr4c_default OWNS BANK 15 (indices 240-255: ink 241 = yellow,
237
+ * shadow 242 = orange). Park 8bpp art colors in bank 14 (224..) or your
238
+ * backdrop turns ink-yellow the moment TTE initialises. */
239
+ #define VC 224 /* vortex colors live at 224..228 — clear of TTE's bank 15 */
240
+ static void vortex_build(void) {
241
+ static u8 tiles[5][64];
242
+ static u8 vmap[1024];
243
+ int x, y, t;
244
+
245
+ pal_bg_mem[VC + 0] = RGB15(2, 2, 8); /* deep blue */
246
+ pal_bg_mem[VC + 1] = RGB15(4, 3, 12); /* indigo */
247
+ pal_bg_mem[VC + 2] = RGB15(8, 18, 26); /* cyan glow */
248
+ pal_bg_mem[VC + 3] = RGB15(13, 5, 22); /* violet */
249
+ pal_bg_mem[VC + 4] = RGB15(26, 28, 31); /* star white */
250
+
251
+ for (y = 0; y < 8; y++)
252
+ for (x = 0; x < 8; x++) {
253
+ tiles[0][y * 8 + x] = 0; /* void */
254
+ tiles[1][y * 8 + x] = (u8)(((x * 3 + y * 5) % 11) ? VC : VC + 1); /* band A */
255
+ tiles[2][y * 8 + x] = (u8)(((x + y * 3) % 9) ? VC + 1 : VC + 3); /* band B */
256
+ tiles[3][y * 8 + x] = (u8)(((x - 4) * (x - 4) + (y - 4) * (y - 4) < 9) ? VC + 2 : VC + 3); /* arm blob */
257
+ tiles[4][y * 8 + x] = (u8)((x == 4 || y == 4) && (x + y > 5 && x + y < 12) ? VC + 4 : VC); /* star */
258
+ }
259
+
260
+ /* Map: concentric rings of bands A/B (radial content for the pulse)... */
261
+ for (y = 0; y < 32; y++)
262
+ for (x = 0; x < 32; x++) {
263
+ int dx = 2 * (x - 16) + 1, dy = 2 * (y - 16) + 1; /* center-ish */
264
+ int r2 = dx * dx + dy * dy; /* 2..2048 */
265
+ u8 tile = (u8)(((r2 >> 7) & 1) ? 1 : 2);
266
+ if (((x * 7 + y * 13) % 29) == 0) tile = 4; /* stars */
267
+ vmap[y * 32 + x] = tile;
268
+ }
269
+ /* ...plus two trailing spiral arms (angular content for the rotation). */
270
+ for (t = 0; t < 56; t++) {
271
+ u16 th = (u16)(t * 1400); /* ~1.2 turns over the arm */
272
+ s32 rq8 = 512 + t * 60; /* radius 2.0→15 tiles, 8.8 */
273
+ s32 ax = 16 + ((rq8 * (lu_cos(th) >> 4)) >> 16);
274
+ s32 ay = 16 + ((rq8 * (lu_sin(th) >> 4)) >> 16);
275
+ if (ax >= 0 && ax < 32 && ay >= 0 && ay < 32) vmap[ay * 32 + ax] = 3;
276
+ ax = 16 + ((rq8 * (lu_cos((u16)(th + 0x8000)) >> 4)) >> 16);
277
+ ay = 16 + ((rq8 * (lu_sin((u16)(th + 0x8000)) >> 4)) >> 16);
278
+ if (ax >= 0 && ax < 32 && ay >= 0 && ay < 32) vmap[ay * 32 + ax] = 3;
279
+ }
280
+
281
+ tonccpy(&tile8_mem[1][0], tiles, sizeof(tiles)); /* tiles → charblock 1 */
282
+ tonccpy(se_mem[26], vmap, sizeof(vmap)); /* map → screenblock 26 */
283
+ REG_BG2CNT = BG_CBB(1) | BG_SBB(26) | BG_AFF_32x32 | BG_WRAP | BG_PRIO(3);
284
+ }
285
+
286
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
287
+ * AFFINE SPRITE (the boss) — same 8.8 screen→texture matrix as the affine
288
+ * BG, but stored in OAM affine slot 0 (see the slot-layout idiom at
289
+ * obj_aff_buffer). Three OBJ-specific footguns this block dodges:
290
+ * 1. attr0 mode bits: ATTR0_AFF (01) turns affine ON; ATTR0_AFF_DBL (11)
291
+ * is affine + DOUBLE-SIZE. Without double-size the sprite is clipped
292
+ * to its original WxH box — a rotated 32x32 has its corners CUT OFF
293
+ * (≈29% of the diagonal) and a zoomed-up one is cropped to 32x32.
294
+ * Double-size renders into a 64x64 window so rotation/zoom≤2x fits.
295
+ * 2. Double-size MOVES THE SPRITE: attr0/attr1 x/y are the top-left of
296
+ * the RENDER WINDOW, so the visual center sits at (x+32, y+32) for a
297
+ * 32x32 sprite — position it by center and subtract 32 (a plain
298
+ * sprite would subtract 16). Forks that toggle DBL must re-anchor.
299
+ * 3. ATTR0_HIDE does NOT hide an affine sprite — mode bits 01/11 reuse
300
+ * the hide bit. To hide the boss, drop attr0 back to a REGULAR hidden
301
+ * object (ATTR0_HIDE alone), as boss_stage() does below.
302
+ * requires: OAM affine slot 0 free (sprites 0..3's fill words — fine here,
303
+ * they're regular objects whose fill is untouched), obj_buffer committed
304
+ * by oam_copy() every frame, boss tiles at OBJ tile 16 (4bpp 32x32, 1D). */
305
+ static void boss_stage(void) {
306
+ OBJ_ATTR *o = &obj_buffer[SLOT_BOSS];
307
+ if (!boss_active) {
308
+ o->attr0 = ATTR0_HIDE; /* REGULAR mode + hide (footgun 3) */
309
+ return;
310
+ }
311
+ /* zoom pulse: 1.0 ± 0.45 from the sine LUT (4.12 → ±~115 in 8.8) */
312
+ u32 zoom = (u32)(256 + (lu_sin(boss_pulse) >> 5));
313
+ s32 inv = (s32)(65536u / zoom);
314
+ s32 cc = ((lu_cos(boss_theta) >> 4) * inv) >> 8;
315
+ s32 ss = ((lu_sin(boss_theta) >> 4) * inv) >> 8;
316
+ obj_aff_buffer[0].pa = (s16)cc; obj_aff_buffer[0].pb = (s16)-ss;
317
+ obj_aff_buffer[0].pc = (s16)ss; obj_aff_buffer[0].pd = (s16)cc;
318
+
319
+ o->attr0 = (u16)(ATTR0_AFF_DBL | ATTR0_SQUARE | ATTR0_4BPP
320
+ | ((boss_y - 32) & 0x00FF)); /* window top */
321
+ o->attr1 = (u16)(ATTR1_SIZE_32 | ATTR1_AFF_ID(0)
322
+ | ((boss_x - 32) & 0x01FF)); /* window left */
323
+ o->attr2 = (u16)(ATTR2_PALBANK(4) | TILE_BOSS);
324
+ }
325
+
326
+ /* ── GAME LOGIC (clay) — boss ART: a spiked disc with ONE cyan spike (the
327
+ * asymmetry makes the spin readable; a symmetric disc looks static).
328
+ * Drawn procedurally into a 32x32 4bpp staging buffer laid out exactly as
329
+ * OBJ VRAM wants it in 1D mapping: 16 consecutive 8x8 tiles, row-major
330
+ * within the sprite, 2 pixels per byte (low nibble = left pixel). */
331
+ static void boss_build_tiles(void) {
332
+ static u32 tiles[16][8];
333
+ int x, y;
334
+ for (y = 0; y < 32; y++)
335
+ for (x = 0; x < 32; x++) {
336
+ int dx = x - 16, dy = y - 16;
337
+ int r2 = dx * dx + dy * dy;
338
+ int c = 0;
339
+ if (r2 < 16) c = 3; /* core */
340
+ else if (r2 < 100) c = (r2 >= 49 && r2 < 81) ? 2 : 1; /* body+ring */
341
+ if (dy >= -2 && dy <= 2 && dx > 8 && dx < 16) c = 4; /* CYAN spike → */
342
+ if (dy >= -2 && dy <= 2 && dx < -8 && dx > -16) c = 2;
343
+ if (dx >= -2 && dx <= 2 && (dy > 8 ? dy < 16 : dy > -16 && dy < -8)) c = 2;
344
+ if (c) {
345
+ int t = (y / 8) * 4 + (x / 8);
346
+ tiles[t][y % 8] |= (u32)c << (4 * (x % 8));
347
+ }
348
+ }
349
+ tonccpy(&tile_mem[4][TILE_BOSS], tiles, sizeof(tiles));
350
+ pal_obj_bank[4][1] = RGB15(16, 6, 26); /* violet body */
351
+ pal_obj_bank[4][2] = RGB15(28, 10, 8); /* ember spikes */
352
+ pal_obj_bank[4][3] = RGB15(31, 30, 24); /* hot core */
353
+ pal_obj_bank[4][4] = RGB15(8, 30, 30); /* THE cyan spike (spin marker) */
354
+ }
355
+
356
+ /* ── GAME LOGIC (clay — reshape freely) ─────────────────────────────────── */
357
+ static u8 rng_state = 0xA5;
358
+ static u8 rand8(void) { /* Galois LFSR, period 255 */
359
+ u8 lsb = (u8)(rng_state & 1);
360
+ rng_state >>= 1;
361
+ if (lsb) rng_state ^= 0xB8;
362
+ return rng_state;
94
363
  }
95
364
 
96
365
  static void fire_bullet(void) {
97
- for (int i = 0; i < MAX_BULLETS; i++) {
366
+ int i;
367
+ for (i = 0; i < MAX_BULLETS; i++)
98
368
  if (!bullets[i].alive) {
99
369
  bullets[i].x = player.x;
100
370
  bullets[i].y = player.y - 8;
101
371
  bullets[i].alive = 1;
372
+ sfx_tone(1, 1900, 4); /* pew (ch1; music owns ch2) */
102
373
  return;
103
374
  }
104
- }
105
- }
106
-
107
- /* Galois LFSR (taps $B8), period 255 -- real per-spawn randomness.
108
- * The old code derived the spawn column from spawn_timer, but the caller
109
- * resets spawn_timer just before calling here, so it was CONSTANT and
110
- * every enemy spawned in the same left column/lane. */
111
- static u8 rng_state = 0xA5;
112
- static u8 rand8(void) {
113
- u8 lsb = (u8)(rng_state & 1);
114
- rng_state >>= 1;
115
- if (lsb) rng_state ^= 0xB8;
116
- return rng_state;
117
375
  }
118
376
 
119
377
  static void spawn_enemy(void) {
120
- for (int i = 0; i < MAX_ENEMIES; i++) {
378
+ int i;
379
+ for (i = 0; i < MAX_ENEMIES; i++)
121
380
  if (!enemies[i].alive) {
122
- /* cheap deterministic x scatter */
123
381
  enemies[i].x = rand8() % (240 - 16) + 8;
124
382
  enemies[i].y = -8;
125
383
  enemies[i].alive = 1;
126
384
  return;
127
385
  }
386
+ }
387
+
388
+ static int aabb_hit(const Obj *a, const Obj *b) {
389
+ return (a->x < b->x + 8) && (a->x + 8 > b->x)
390
+ && (a->y < b->y + 8) && (a->y + 8 > b->y);
391
+ }
392
+
393
+ /* ── GAME LOGIC (clay) — HUD / screens (TTE on BG1, priority 0) ── */
394
+ static void draw_hud_labels(void) {
395
+ tte_erase_screen();
396
+ tte_write("#{P:8,4}SC");
397
+ tte_write("#{P:96,4}HI");
398
+ tte_write("#{P:168,4}W");
399
+ tte_write("#{P:208,4}x");
400
+ }
401
+
402
+ static void draw_hud_numbers(void) {
403
+ tte_erase_rect(28, 4, 70, 12); draw_num(28, 4, score, 5);
404
+ tte_erase_rect(116, 4, 158, 12); draw_num(116, 4, hiscore, 5);
405
+ tte_erase_rect(178, 4, 196, 12); draw_num(178, 4, wave, 2);
406
+ tte_erase_rect(218, 4, 228, 12); draw_num(218, 4, lives, 1);
407
+ }
408
+
409
+ static void enter_title(void) {
410
+ state = ST_TITLE;
411
+ tte_erase_screen();
412
+ tte_write("#{P:60,40}" GAME_TITLE);
413
+ tte_write("#{P:76,80}PRESS START");
414
+ tte_write("#{P:88,100}HI");
415
+ draw_num(112, 100, hiscore, 5);
416
+ tte_write("#{P:48,128}DPAD MOVE - A FIRE");
417
+ }
418
+
419
+ static void enter_play(void) {
420
+ int i;
421
+ state = ST_PLAY;
422
+ player.x = 116; player.y = 130; player.alive = 1;
423
+ for (i = 0; i < MAX_BULLETS; i++) bullets[i].alive = 0;
424
+ for (i = 0; i < MAX_ENEMIES; i++) enemies[i].alive = 0;
425
+ score = 0; lives = START_LIVES; wave = 1; kills = 0;
426
+ spawn_timer = 0;
427
+ boss_active = 0;
428
+ draw_hud_labels();
429
+ draw_hud_numbers();
430
+ }
431
+
432
+ static void enter_over(void) {
433
+ state = ST_OVER;
434
+ if (score > hiscore) {
435
+ hiscore = score;
436
+ hiscore_save(hiscore); /* byte-wise SRAM write — see the idiom */
437
+ draw_hud_numbers();
128
438
  }
439
+ tte_write("#{P:84,64}GAME OVER");
440
+ tte_write("#{P:76,84}PRESS START");
441
+ }
442
+
443
+ static void boss_enter(void) {
444
+ boss_active = 1;
445
+ boss_x = 120; boss_y = -20; /* descends into view */
446
+ boss_hp = (u8)(6 + wave * 2);
447
+ if (boss_hp > 20) boss_hp = 20;
448
+ boss_theta = 0; boss_pulse = 0;
449
+ sfx_noise(30);
450
+ }
451
+
452
+ static void boss_defeat(void) {
453
+ boss_active = 0;
454
+ if (score < 65000u) score += 250;
455
+ wave++; kills = 0;
456
+ draw_hud_numbers();
457
+ sfx_noise(24);
458
+ }
459
+
460
+ static void lose_life(void) {
461
+ sfx_noise(12);
462
+ if (lives > 0) lives--;
463
+ draw_hud_numbers();
464
+ player.x = 116; player.y = 130;
465
+ if (lives == 0) enter_over();
466
+ }
467
+
468
+ /* ── GAME LOGIC (clay) — one ST_PLAY tick ── */
469
+ static void update_play(void) {
470
+ int i, j;
471
+
472
+ if (key_held(KEY_LEFT) && player.x > 8) player.x -= 2;
473
+ if (key_held(KEY_RIGHT) && player.x < 240 - 16) player.x += 2;
474
+ if (key_held(KEY_UP) && player.y > 20) player.y -= 2;
475
+ if (key_held(KEY_DOWN) && player.y < 160 - 16) player.y += 2;
476
+ if (key_hit(KEY_A)) fire_bullet();
477
+
478
+ for (i = 0; i < MAX_BULLETS; i++) {
479
+ if (!bullets[i].alive) continue;
480
+ bullets[i].y -= 4;
481
+ if (bullets[i].y < -8) bullets[i].alive = 0;
482
+ }
483
+ for (i = 0; i < MAX_ENEMIES; i++) {
484
+ if (!enemies[i].alive) continue;
485
+ enemies[i].y += 1 + (wave >> 2);
486
+ if (enemies[i].y > 160) enemies[i].alive = 0;
487
+ }
488
+
489
+ /* Spawner: steady waves; during a boss the boss is the spawner. */
490
+ if (!boss_active) {
491
+ u16 period = (u16)(28 > 12 + wave * 2 ? 28 - wave * 2 : 12);
492
+ if (++spawn_timer >= period && kills < WAVE_KILLS) { spawn_timer = 0; spawn_enemy(); }
493
+ if (kills >= WAVE_KILLS) {
494
+ u8 field_clear = 1;
495
+ for (i = 0; i < MAX_ENEMIES; i++) if (enemies[i].alive) field_clear = 0;
496
+ if (field_clear) boss_enter();
497
+ }
498
+ } else {
499
+ /* Boss attack pattern: spin faster than the backdrop, pulse scale,
500
+ * strafe a sine path while drifting down, shed minions. */
501
+ boss_theta = (u16)(boss_theta + 0x0140); /* ~1.8°/frame */
502
+ boss_pulse = (u16)(boss_pulse + 0x0120);
503
+ if (boss_y < 56) boss_y++; /* entrance dive */
504
+ else boss_x = (s16)(120 + ((76 * lu_sin((u16)(frame << 7))) >> 12));
505
+ if (++spawn_timer >= 90) { spawn_timer = 0; spawn_enemy(); }
506
+
507
+ /* Bullets vs boss: 28x28 box around the boss CENTER. Collision is
508
+ * the UNROTATED box on purpose — honest simplification; rotating
509
+ * hitboxes buys little for a round boss. */
510
+ for (i = 0; i < MAX_BULLETS; i++) {
511
+ if (!bullets[i].alive) continue;
512
+ if (bullets[i].x + 4 > boss_x - 14 && bullets[i].x + 4 < boss_x + 14 &&
513
+ bullets[i].y + 4 > boss_y - 14 && bullets[i].y + 4 < boss_y + 14) {
514
+ bullets[i].alive = 0;
515
+ sfx_tone(1, 900, 3);
516
+ if (--boss_hp == 0) { boss_defeat(); break; }
517
+ }
518
+ }
519
+ /* Boss vs player (same box vs the 8x8 ship). */
520
+ if (boss_active &&
521
+ player.x + 8 > boss_x - 14 && player.x < boss_x + 14 &&
522
+ player.y + 8 > boss_y - 14 && player.y < boss_y + 14) {
523
+ lose_life();
524
+ }
525
+ }
526
+
527
+ /* Bullets vs enemies. */
528
+ for (i = 0; i < MAX_BULLETS; i++) {
529
+ if (!bullets[i].alive) continue;
530
+ for (j = 0; j < MAX_ENEMIES; j++) {
531
+ if (!enemies[j].alive) continue;
532
+ if (aabb_hit(&bullets[i], &enemies[j])) {
533
+ bullets[i].alive = 0;
534
+ enemies[j].alive = 0;
535
+ if (score < 65000u) score += 10;
536
+ kills++;
537
+ sfx_noise(6);
538
+ draw_hud_numbers();
539
+ break;
540
+ }
541
+ }
542
+ }
543
+ /* Enemies vs player. */
544
+ for (j = 0; j < MAX_ENEMIES && state == ST_PLAY; j++) {
545
+ if (!enemies[j].alive) continue;
546
+ if (aabb_hit(&enemies[j], &player)) {
547
+ enemies[j].alive = 0;
548
+ lose_life();
549
+ }
550
+ }
551
+ }
552
+
553
+ /* ── GAME LOGIC (clay) — stage the regular sprites (boss has its own idiom
554
+ * block). Inactive slots park offscreen (y=200) instead of HIDE so the loop
555
+ * stays branch-light; either works for REGULAR sprites. ── */
556
+ static void stage_sprites(void) {
557
+ int i;
558
+ int px = (state == ST_PLAY) ? player.x : 250, py = (state == ST_PLAY) ? player.y : 200;
559
+ obj_set_attr(&obj_buffer[SLOT_PLAYER], ATTR0_SQUARE, ATTR1_SIZE_8,
560
+ ATTR2_PALBANK(0) | TILE_SHIP);
561
+ obj_set_pos(&obj_buffer[SLOT_PLAYER], px, py);
562
+ for (i = 0; i < MAX_BULLETS; i++) {
563
+ obj_set_attr(&obj_buffer[SLOT_BULLET + i], ATTR0_SQUARE, ATTR1_SIZE_8,
564
+ ATTR2_PALBANK(1) | TILE_BULLET);
565
+ obj_set_pos(&obj_buffer[SLOT_BULLET + i], bullets[i].x,
566
+ (state == ST_PLAY && bullets[i].alive) ? bullets[i].y : 200);
567
+ }
568
+ for (i = 0; i < MAX_ENEMIES; i++) {
569
+ obj_set_attr(&obj_buffer[SLOT_ENEMY + i], ATTR0_SQUARE, ATTR1_SIZE_8,
570
+ ATTR2_PALBANK(2) | TILE_ENEMY);
571
+ obj_set_pos(&obj_buffer[SLOT_ENEMY + i], enemies[i].x,
572
+ (state == ST_PLAY && enemies[i].alive) ? enemies[i].y : 200);
573
+ }
574
+ boss_stage();
129
575
  }
130
576
 
131
577
  int main(void) {
132
- /* ── Sprite tiles char base 4 is the OBJ tile area in BG mode 0 ─ */
578
+ /* ── HARDWARE IDIOM (load-bearing reshape gameplay around this; see TROUBLESHOOTING) ──
579
+ * Init order: tiles/palettes → oam_init → irq_init + II_VBLANK →
580
+ * TTE init → DISPCNT last. VBlankIntrWait() HANGS FOREVER without the
581
+ * vblank IRQ registered (the #1 "frozen on frame 1" cause), and
582
+ * enabling DISPCNT layers before their tiles/maps exist flashes
583
+ * garbage. TTE owns BG1 (CBB 2 / SBB 30) — keep other layers off
584
+ * those blocks. requires: nothing prior; this IS the boot. */
133
585
  tonccpy(&tile_mem[4][TILE_SHIP], tile_ship, sizeof(tile_ship));
134
586
  tonccpy(&tile_mem[4][TILE_BULLET], tile_bullet, sizeof(tile_bullet));
135
587
  tonccpy(&tile_mem[4][TILE_ENEMY], tile_enemy, sizeof(tile_enemy));
136
-
137
- /* ── Sprite palette ── one palette bank per object class ─────────
138
- * pal_obj_bank[N][i] = colour i of bank N. Index 0 = transparent. */
139
- pal_obj_bank[0][1] = CLR_WHITE; /* ship */
140
- pal_obj_bank[1][2] = CLR_YELLOW; /* bullets */
141
- pal_obj_bank[2][3] = CLR_RED; /* enemies */
142
-
143
- /* ── Starfield backdrop on BG0 ───────────────────────────────────
144
- * Fill the whole screen with a banded space backdrop + scattered
145
- * stars so the playfield doesn't read as a blank black screen. BG
146
- * palette indices 4/5 = the two space bands, 6 = star colour. Tile
147
- * data → char-block 0, map → screen-block 28 (clear of TTE on
148
- * char-block 2 / screen-block 30). BG0 sits at the lowest priority
149
- * so the ship/bullets/enemies draw in front of it. */
588
+ boss_build_tiles();
589
+ pal_obj_bank[0][1] = CLR_WHITE; /* ship */
590
+ pal_obj_bank[1][2] = CLR_YELLOW; /* bullets */
591
+ pal_obj_bank[2][3] = CLR_RED; /* enemies */
150
592
  pal_bg_mem[0] = CLR_BLACK;
151
- pal_bg_mem[4] = RGB15(1, 2, 7); /* dark space band */
152
- pal_bg_mem[5] = RGB15(2, 3, 10); /* lighter band */
153
- pal_bg_mem[6] = RGB15(28, 28, 31); /* stars */
154
- tonccpy(&tile_mem[0][4], tile_star_a, sizeof(tile_star_a));
155
- tonccpy(&tile_mem[0][5], tile_star_b, sizeof(tile_star_b));
156
- REG_BG0CNT = BG_CBB(0) | BG_SBB(28) | BG_REG_32x32 | BG_4BPP | BG_PRIO(3);
157
- {
158
- SCR_ENTRY *map = se_mem[28];
159
- for (int ty = 0; ty < 32; ty++)
160
- for (int tx = 0; tx < 32; tx++)
161
- map[ty * 32 + tx] = SE_BUILD(4 + ((ty >> 1) & 1), 0, 0, 0);
162
- }
163
593
 
164
- oam_init(obj_buffer, 128);
594
+ vortex_build(); /* affine BG2: tiles+map+BG2CNT */
595
+ oam_init(obj_buffer, 128); /* hides all 128, matrices = identity */
165
596
 
166
- /* IRQ setup — required for VBlankIntrWait() to function. */
167
597
  irq_init(NULL);
168
598
  irq_add(II_VBLANK, NULL);
169
599
 
170
- sfx_init();
171
-
172
- player.x = 116; player.y = 130; player.alive = 1;
173
- for (int i = 0; i < MAX_BULLETS; i++) bullets[i].alive = 0;
174
- for (int i = 0; i < MAX_ENEMIES; i++) enemies[i].alive = 0;
175
- score = 0;
176
- spawn_timer = 0;
600
+ sfx_init(); /* APU on; music loop ticks below */
177
601
 
178
- /* TTE on BG1 at screen-block 30, char-block 2 (BG0 holds the
179
- * starfield). Renders into VRAM tile map no libsysbase / iprintf
180
- * dependency. BG1 priority 0 → score/hint text in front. */
602
+ /* TTE text on BG1 (4bpp char block 2, screenblock 30), priority 0 so
603
+ * text draws over everything. Mode 1 = BG0/BG1 regular, BG2 AFFINE. */
181
604
  tte_init_chr4c_default(1, BG_CBB(2) | BG_SBB(30));
182
605
  REG_BG1CNT |= BG_PRIO(0);
183
- REG_DISPCNT = DCNT_MODE0 | DCNT_BG0 | DCNT_BG1 | DCNT_OBJ | DCNT_OBJ_1D;
184
- tte_write("#{P:8,8}SCORE 00000");
185
- tte_write("#{P:8,144}D-PAD MOVE A FIRE");
606
+ REG_DISPCNT = DCNT_MODE1 | DCNT_BG1 | DCNT_BG2 | DCNT_OBJ | DCNT_OBJ_1D;
186
607
 
187
- u16 prev = 0;
608
+ hiscore = hiscore_load(); /* cartridge SRAM — 0 on first boot */
609
+ enter_title();
188
610
 
189
611
  while (1) {
612
+ /* Idiomatic Tonc heartbeat: wait vblank, poll keys, update, then
613
+ * commit OAM + affine registers while still inside vblank (the
614
+ * whole update is far quicker than the 4.9ms vblank window). */
190
615
  VBlankIntrWait();
191
616
  key_poll();
617
+ sfx_music_tick(); /* forget this → silent game */
618
+ frame++;
192
619
 
193
- if (key_held(KEY_LEFT) && player.x > 8) player.x -= 2;
194
- if (key_held(KEY_RIGHT) && player.x < 240 - 16) player.x += 2;
195
- if (key_held(KEY_UP) && player.y > 24) player.y -= 2;
196
- if (key_held(KEY_DOWN) && player.y < 160 - 24) player.y += 2;
197
-
198
- u16 now = key_curr_state();
199
- if ((now & KEY_A) && !(prev & KEY_A)) {
200
- fire_bullet();
201
- sfx_tone(2, 1900, 4); /* pew */
202
- }
203
- prev = now;
204
-
205
- for (int i = 0; i < MAX_BULLETS; i++) {
206
- if (!bullets[i].alive) continue;
207
- bullets[i].y -= 4;
208
- if (bullets[i].y < -8) bullets[i].alive = 0;
209
- }
210
- for (int i = 0; i < MAX_ENEMIES; i++) {
211
- if (!enemies[i].alive) continue;
212
- enemies[i].y += 1;
213
- if (enemies[i].y > 160) enemies[i].alive = 0;
620
+ if (state == ST_TITLE) {
621
+ if (key_hit(KEY_START | KEY_A)) enter_play();
622
+ } else if (state == ST_OVER) {
623
+ if (key_hit(KEY_START)) enter_title();
624
+ } else {
625
+ update_play();
214
626
  }
215
- if (++spawn_timer >= 28) { spawn_timer = 0; spawn_enemy(); }
216
627
 
217
- /* Bullet × enemy collisions. */
218
- for (int i = 0; i < MAX_BULLETS; i++) {
219
- if (!bullets[i].alive) continue;
220
- for (int j = 0; j < MAX_ENEMIES; j++) {
221
- if (!enemies[j].alive) continue;
222
- if (aabb_hit(&bullets[i], &enemies[j])) {
223
- bullets[i].alive = 0;
224
- enemies[j].alive = 0;
225
- if (score < 65500u) score += 10;
226
- sfx_noise(6); /* explosion */
227
- break;
228
- }
628
+ /* The vortex breathes with the game: gentle on the title, driving
629
+ * during play, frantic while the boss is up. (Affine BG idiom —
630
+ * rewrite ALL the write-only registers every frame.) */
631
+ {
632
+ u16 vth; u32 vzoom;
633
+ if (state == ST_PLAY && boss_active) {
634
+ vth = (u16)(frame * 0x00C0);
635
+ vzoom = (u32)(256 + (lu_sin((u16)(frame * 0x0180)) >> 5));
636
+ } else if (state == ST_PLAY) {
637
+ vth = (u16)(frame * 0x0050);
638
+ vzoom = (u32)(256 + (lu_sin((u16)(frame * 0x0060)) >> 6));
639
+ } else {
640
+ vth = (u16)(frame * 0x0030);
641
+ vzoom = (u32)(256 + (lu_sin((u16)(frame * 0x0040)) >> 6));
229
642
  }
643
+ vortex_apply(vth, vzoom);
230
644
  }
231
645
 
232
- /* ── Stage shadow OAM ────────────────────────────────────────
233
- * Slot 0: player. Slots 1..6: bullets. Slots 7..12: enemies.
234
- * Hide inactive slots by parking them offscreen (y = 160). */
235
- obj_set_attr(&obj_buffer[0],
236
- ATTR0_SQUARE,
237
- ATTR1_SIZE_8,
238
- ATTR2_PALBANK(0) | TILE_SHIP);
239
- obj_set_pos(&obj_buffer[0], player.x, player.y);
240
-
241
- for (int i = 0; i < MAX_BULLETS; i++) {
242
- int by = bullets[i].alive ? bullets[i].y : 200;
243
- obj_set_attr(&obj_buffer[1 + i],
244
- ATTR0_SQUARE,
245
- ATTR1_SIZE_8,
246
- ATTR2_PALBANK(1) | TILE_BULLET);
247
- obj_set_pos(&obj_buffer[1 + i], bullets[i].x, by);
248
- }
249
- for (int i = 0; i < MAX_ENEMIES; i++) {
250
- int ey = enemies[i].alive ? enemies[i].y : 200;
251
- obj_set_attr(&obj_buffer[7 + i],
252
- ATTR0_SQUARE,
253
- ATTR1_SIZE_8,
254
- ATTR2_PALBANK(2) | TILE_ENEMY);
255
- obj_set_pos(&obj_buffer[7 + i], enemies[i].x, ey);
256
- }
257
-
258
- oam_copy(oam_mem, obj_buffer, 128);
259
-
260
- /* Score: 5 digits via draw_score (NOT tte_printf — see GBA-1). */
261
- tte_erase_rect(8 + 6*8, 8, 8 + 11*8, 16);
262
- draw_score(8 + 6*8, score);
646
+ stage_sprites();
647
+ oam_copy(oam_mem, obj_buffer, 128); /* sprites AND affine slot 0 */
263
648
  }
264
649
  return 0;
265
650
  }