romdevtools 0.27.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/AGENTS.md +56 -44
  2. package/CHANGELOG.md +355 -0
  3. package/README.md +4 -4
  4. package/examples/README.md +7 -7
  5. package/examples/atari2600/templates/platformer.asm +1227 -325
  6. package/examples/atari2600/templates/puzzle.asm +1056 -0
  7. package/examples/atari2600/templates/racing.asm +909 -257
  8. package/examples/atari2600/templates/shmup.asm +1035 -218
  9. package/examples/atari2600/templates/sports.asm +1143 -229
  10. package/examples/atari7800/templates/hello_sprite.c +8 -4
  11. package/examples/atari7800/templates/platformer.c +991 -152
  12. package/examples/atari7800/templates/puzzle.c +1091 -145
  13. package/examples/atari7800/templates/racing.c +949 -118
  14. package/examples/atari7800/templates/shmup.c +812 -130
  15. package/examples/atari7800/templates/sports.c +820 -181
  16. package/examples/c64/templates/platformer.c +876 -157
  17. package/examples/c64/templates/puzzle.c +881 -143
  18. package/examples/c64/templates/racing.c +873 -88
  19. package/examples/c64/templates/shmup.c +762 -154
  20. package/examples/c64/templates/sports.c +755 -95
  21. package/examples/gb/templates/platformer.c +841 -175
  22. package/examples/gb/templates/puzzle.c +1094 -176
  23. package/examples/gb/templates/racing.c +761 -169
  24. package/examples/gb/templates/shmup.c +679 -169
  25. package/examples/gb/templates/sports.c +790 -153
  26. package/examples/gba/templates/platformer.c +624 -169
  27. package/examples/gba/templates/puzzle.c +535 -207
  28. package/examples/gba/templates/racing.c +513 -196
  29. package/examples/gba/templates/shmup.c +565 -168
  30. package/examples/gba/templates/sports.c +454 -162
  31. package/examples/gbc/templates/platformer.c +944 -176
  32. package/examples/gbc/templates/puzzle.c +1131 -177
  33. package/examples/gbc/templates/racing.c +891 -175
  34. package/examples/gbc/templates/shmup.c +827 -179
  35. package/examples/gbc/templates/sports.c +870 -156
  36. package/examples/genesis/templates/platformer.c +747 -129
  37. package/examples/genesis/templates/puzzle.c +702 -208
  38. package/examples/genesis/templates/racing.c +728 -193
  39. package/examples/genesis/templates/shmup.c +535 -142
  40. package/examples/genesis/templates/shmup_2p.c +13 -1
  41. package/examples/genesis/templates/sports.c +495 -158
  42. package/examples/gg/templates/platformer.c +883 -214
  43. package/examples/gg/templates/puzzle.c +906 -181
  44. package/examples/gg/templates/racing.c +919 -160
  45. package/examples/gg/templates/shmup.c +716 -177
  46. package/examples/gg/templates/sports.c +735 -128
  47. package/examples/lynx/templates/platformer.c +604 -50
  48. package/examples/lynx/templates/puzzle.c +533 -130
  49. package/examples/lynx/templates/racing.c +538 -102
  50. package/examples/lynx/templates/shmup.c +461 -122
  51. package/examples/lynx/templates/sports.c +496 -69
  52. package/examples/msx/platformer/main.c +648 -159
  53. package/examples/msx/puzzle/main.c +750 -185
  54. package/examples/msx/racing/main.c +669 -177
  55. package/examples/msx/shmup/main.c +460 -177
  56. package/examples/msx/sports/main.c +591 -124
  57. package/examples/nes/templates/platformer.c +586 -160
  58. package/examples/nes/templates/puzzle.c +603 -222
  59. package/examples/nes/templates/racing.c +505 -197
  60. package/examples/nes/templates/shmup.c +339 -144
  61. package/examples/nes/templates/sports.c +341 -182
  62. package/examples/pce/platformer/main.c +875 -204
  63. package/examples/pce/puzzle/main.c +797 -216
  64. package/examples/pce/racing/main.c +782 -206
  65. package/examples/pce/shmup/main.c +638 -211
  66. package/examples/pce/sports/main.c +585 -167
  67. package/examples/porting-across-platforms/README.md +1 -1
  68. package/examples/sms/templates/platformer.c +765 -176
  69. package/examples/sms/templates/puzzle.c +783 -177
  70. package/examples/sms/templates/racing.c +812 -133
  71. package/examples/sms/templates/shmup.c +601 -148
  72. package/examples/sms/templates/shmup_2p.c +17 -1
  73. package/examples/sms/templates/sports.c +633 -121
  74. package/examples/snes/templates/music_demo.c +7 -0
  75. package/examples/snes/templates/platformer-data.asm +123 -24
  76. package/examples/snes/templates/platformer-hdr.asm +57 -0
  77. package/examples/snes/templates/platformer.c +587 -149
  78. package/examples/snes/templates/puzzle-data.asm +116 -21
  79. package/examples/snes/templates/puzzle-hdr.asm +57 -0
  80. package/examples/snes/templates/puzzle.c +632 -185
  81. package/examples/snes/templates/racing-data.asm +390 -32
  82. package/examples/snes/templates/racing-hdr.asm +57 -0
  83. package/examples/snes/templates/racing.c +807 -177
  84. package/examples/snes/templates/shmup-data.asm +87 -29
  85. package/examples/snes/templates/shmup-hdr.asm +57 -0
  86. package/examples/snes/templates/shmup.c +459 -180
  87. package/examples/snes/templates/sports-data.asm +48 -2
  88. package/examples/snes/templates/sports-hdr.asm +57 -0
  89. package/examples/snes/templates/sports.c +414 -156
  90. package/package.json +12 -12
  91. package/src/cores/wasm/bluemsx_libretro.js +1 -1
  92. package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
  93. package/src/cores/wasm/fceumm_libretro.js +1 -1
  94. package/src/cores/wasm/fceumm_libretro.wasm +0 -0
  95. package/src/cores/wasm/gambatte_libretro.js +1 -1
  96. package/src/cores/wasm/gambatte_libretro.wasm +0 -0
  97. package/src/cores/wasm/geargrafx_libretro.js +1 -1
  98. package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
  99. package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
  100. package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
  101. package/src/cores/wasm/handy_libretro.js +1 -1
  102. package/src/cores/wasm/handy_libretro.wasm +0 -0
  103. package/src/cores/wasm/mgba_libretro.js +1 -1
  104. package/src/cores/wasm/mgba_libretro.wasm +0 -0
  105. package/src/cores/wasm/prosystem_libretro.js +1 -1
  106. package/src/cores/wasm/prosystem_libretro.wasm +0 -0
  107. package/src/cores/wasm/snes9x_libretro.js +1 -1
  108. package/src/cores/wasm/snes9x_libretro.wasm +0 -0
  109. package/src/cores/wasm/stella2014_libretro.js +1 -1
  110. package/src/cores/wasm/stella2014_libretro.wasm +0 -0
  111. package/src/cores/wasm/vice_x64_libretro.js +1 -1
  112. package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
  113. package/src/host/LibretroHost.js +304 -11
  114. package/src/http/tool-registry.js +11 -11
  115. package/src/mcp/server.js +6 -0
  116. package/src/mcp/tools/cheats.js +2 -1
  117. package/src/mcp/tools/disasm-rebuild.js +315 -65
  118. package/src/mcp/tools/disasm.js +149 -28
  119. package/src/mcp/tools/find-references.js +216 -51
  120. package/src/mcp/tools/frame.js +14 -6
  121. package/src/mcp/tools/index.js +18 -4
  122. package/src/mcp/tools/input.js +31 -7
  123. package/src/mcp/tools/lifecycle.js +6 -4
  124. package/src/mcp/tools/memory.js +208 -39
  125. package/src/mcp/tools/platform-docs.js +1 -1
  126. package/src/mcp/tools/playtest.js +56 -4
  127. package/src/mcp/tools/preview-tile.js +6 -2
  128. package/src/mcp/tools/project.js +1114 -120
  129. package/src/mcp/tools/rom-id.js +5 -1
  130. package/src/mcp/tools/run-until.js +4 -2
  131. package/src/mcp/tools/snippets.js +6 -6
  132. package/src/mcp/tools/sprite-pipeline.js +14 -2
  133. package/src/mcp/tools/state.js +2 -1
  134. package/src/mcp/tools/tile-inspect.js +8 -1
  135. package/src/mcp/tools/toolchain.js +55 -11
  136. package/src/mcp/tools/watch-memory.js +145 -27
  137. package/src/observer/bus.js +73 -0
  138. package/src/observer/livestream.html +4 -2
  139. package/src/observer/tool-wrap.js +17 -14
  140. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +64 -17
  141. package/src/platforms/atari2600/MENTAL_MODEL.md +5 -1
  142. package/src/platforms/atari2600/TROUBLESHOOTING.md +40 -0
  143. package/src/platforms/atari7800/MENTAL_MODEL.md +32 -11
  144. package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
  145. package/src/platforms/c64/MENTAL_MODEL.md +11 -4
  146. package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
  147. package/src/platforms/gb/MENTAL_MODEL.md +19 -4
  148. package/src/platforms/gb/TROUBLESHOOTING.md +101 -6
  149. package/src/platforms/gb/lib/c/README.md +10 -11
  150. package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
  151. package/src/platforms/gb/lib/c/patch-header.js +19 -6
  152. package/src/platforms/gba/MENTAL_MODEL.md +4 -4
  153. package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
  154. package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
  155. package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
  156. package/src/platforms/gbc/MENTAL_MODEL.md +16 -4
  157. package/src/platforms/gbc/TROUBLESHOOTING.md +24 -3
  158. package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
  159. package/src/platforms/gbc/lib/c/README.md +10 -11
  160. package/src/platforms/gbc/lib/c/font.h +43 -0
  161. package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
  162. package/src/platforms/gbc/lib/c/patch-header.js +19 -6
  163. package/src/platforms/genesis/MENTAL_MODEL.md +43 -9
  164. package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
  165. package/src/platforms/genesis/lib/c/genesis_sfx.c +37 -0
  166. package/src/platforms/genesis/lib/c/genesis_sfx.h +1 -0
  167. package/src/platforms/gg/MENTAL_MODEL.md +4 -4
  168. package/src/platforms/gg/TROUBLESHOOTING.md +14 -18
  169. package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
  170. package/src/platforms/gg/lib/c/gg_crt0.s +14 -2
  171. package/src/platforms/gg/lib/c/joypad_read.c +29 -0
  172. package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
  173. package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
  174. package/src/platforms/lynx/lib/c/lynx_sfx.c +38 -2
  175. package/src/platforms/lynx/lib/c/lynx_sfx.h +1 -0
  176. package/src/platforms/msx/MENTAL_MODEL.md +11 -5
  177. package/src/platforms/msx/TROUBLESHOOTING.md +21 -0
  178. package/src/platforms/msx/lib/c/msx_crt0.s +27 -0
  179. package/src/platforms/msx/lib/c/msx_hw.h +3 -0
  180. package/src/platforms/msx/lib/c/msx_vdp.c +70 -0
  181. package/src/platforms/nes/MENTAL_MODEL.md +12 -5
  182. package/src/platforms/nes/lib/c/nes_runtime.c +190 -34
  183. package/src/platforms/nes/lib/c/nes_runtime.h +35 -0
  184. package/src/platforms/pce/MENTAL_MODEL.md +14 -5
  185. package/src/platforms/pce/TROUBLESHOOTING.md +9 -0
  186. package/src/platforms/pce/lib/c/pce_hw.h +13 -1
  187. package/src/platforms/pce/lib/c/pce_sound.c +22 -0
  188. package/src/platforms/pce/lib/c/pce_video.c +32 -0
  189. package/src/platforms/sms/MENTAL_MODEL.md +11 -6
  190. package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
  191. package/src/platforms/sms/lib/c/sms_crt0.s +14 -2
  192. package/src/platforms/snes/MENTAL_MODEL.md +7 -2
  193. package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
  194. package/src/playtest/playtest.js +73 -3
  195. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
  196. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
  197. package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
  198. package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
  199. package/src/toolchains/index.js +64 -19
@@ -1,27 +1,36 @@
1
- /* ── shmup.c — SNES PVSnesLib vertical-shooter scaffold ─────────────
1
+ /* ── shmup.c — SNES vertical shooter (complete example game) ──────────────────
2
2
  *
3
- * Complete runnable vertical-shmup baseline. Layout mirrors the NES
4
- * and Genesis shmup scaffolds:
5
- * - Player ship (8×8 OAM 0), d-pad moves, B fires
6
- * - 6 bullet slots (OAM 1..6), 6 enemy slots (OAM 7..12)
7
- * - Wave spawner: one enemy from the top every ~28 frames
8
- * - Linear movement, AABB collision (8×8 vs 8×8)
3
+ * A COMPLETE, working game title screen, 1P and 2P SIMULTANEOUS co-op,
4
+ * shared lives, score + persistent hi-score (battery SRAM), SPC music + SFX,
5
+ * and a scrolling Mode 1 starfield under a rock-steady text HUD.
9
6
  *
10
- * SNES screen is 256×224 (NTSC). All movement / spawn coords use
11
- * that range. PVSnesLib's oamSet places OAM entries; oamUpdate
12
- * flushes them via DMA on the NMI.
7
+ * THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
8
+ * very different one. The markers tell you what's what:
9
+ * HARDWARE IDIOM (load-bearing) dodges a documented SNES footgun; reshape
10
+ * your gameplay around it (see TROUBLESHOOTING before changing).
11
+ * GAME LOGIC (clay) — enemy patterns, scoring, tuning, art: reshape freely.
13
12
  *
14
- * Sibling data.asm provides the font + sprite-tile + sprite-palette
15
- * blobs. We use tile index 0 (ship), 1 (bullet), 2 (enemy).
13
+ * What depends on what:
14
+ * data.asm font + sprite tiles + starfield tiles (rodata), and
15
+ * sram_read16/write16 (battery SRAM needs 24-bit addressing that tcc
16
+ * C pointers don't emit). Load-bearing.
17
+ * hdr.asm — THIS PROJECT OVERRIDES the stock header to declare battery
18
+ * SRAM (CARTRIDGETYPE $02 + SRAMSIZE $01). Delete that file and saves
19
+ * silently stop existing — the build still succeeds.
20
+ * snes_sfx.{h,c} + snes_sfx_data.asm + apu_blob.bin — the SPC700 sound
21
+ * driver (music + 2 one-shot samples). #include'd, not separately built.
16
22
  *
17
- * TWO PVSnesLib footguns this scaffold handles for you:
18
- * 1. oamSet's FIRST arg is a BYTE OFFSET into OAM, not a slot number.
19
- * Each sprite is 4 bytes, so sprite slot N lives at offset N*4.
20
- * Passing a plain slot number interleaves/corrupts entries. We use
21
- * the SPR(slot) macro belowalways address sprites through it.
22
- * 2. sfx_init() must run AFTER setScreenOn() and its return must be
23
- * checked. If it stalls before the screen is on, you get a black
24
- * screen forever. See the main() ordering below.
23
+ * Why the HUD never shears (read this if you come from the NES): the SNES
24
+ * Mode 1 gives you THREE independent background layers, each with its own
25
+ * scroll registers. The starfield lives on BG1 and scrolls; the text HUD
26
+ * lives on BG0 and simply never gets a scroll write. No sprite-0 splits, no
27
+ * mid-frame raster trickslayer separation IS the SNES way. (When one
28
+ * layer must be two things a fixed strip over a moving field on the SAME
29
+ * BG that's when you reach for HDMA; see the Mode 7 racing example.)
30
+ *
31
+ * VRAM BUDGET (word addresses):
32
+ * $0000- OBJ tiles, $2000- BG1 starfield tiles, $3000- BG0 console font,
33
+ * $4000- BG1 map, $6800- BG0 text map.
25
34
  */
26
35
 
27
36
  #include <snes.h>
@@ -29,194 +38,464 @@
29
38
  * inline rather than linked separately. */
30
39
  #include "snes_sfx.c"
31
40
 
32
- extern char tilfont, palfont;
33
- extern char tilsprite, palsprite;
34
- extern char tilbg, palbg; /* wallpaper tile + palette (data.asm) */
41
+ /* The title screen renders this — examples({op:'fork'}) stamps your game's
42
+ * name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
43
+ #define GAME_TITLE "SOLAR BULWARK"
44
+
45
+ extern char tilfont, palfont; /* HUD font + text palette (data.asm) */
46
+ extern char tilsprite, palsprite; /* ship/bullet/enemy tiles + OBJ pal */
47
+ extern char tilbg, palbg; /* 4 starfield tiles + BG palette */
35
48
 
36
49
  /* consoleVblank() copies the dirty text tilemap to VRAM during VBlank.
37
50
  * No public prototype in console.h, so declare it; call once per frame. */
38
51
  extern void consoleVblank(void);
39
52
 
40
- /* OAM is addressed by BYTE OFFSET; sprite slot N = offset N*4. */
53
+ /* data.asm exports battery SRAM accessors (long addressing to $70:0000). */
54
+ extern u16 sram_read16(u16 offset);
55
+ extern void sram_write16(u16 offset, u16 value);
56
+
57
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
58
+ * oamSet's FIRST arg is a BYTE OFFSET into OAM, not a slot number. Each
59
+ * sprite is 4 bytes, so sprite slot N lives at offset N*4. Passing a plain
60
+ * slot number interleaves/corrupts entries — always go through SPR(). */
41
61
  #define SPR(slot) ((slot) << 2)
42
62
 
43
- /* BG1 wallpaper map: a full 32×32 screen of the 4-colour tile so the
44
- * playfield reads as a real backdrop, not flat blank. Filled at runtime. */
45
- static u16 bg_map[32 * 32];
63
+ /* ── GAME LOGIC (clay reshape freely) ──────────────────────────────────────
64
+ * Object pools fixed slots, no allocation. OAM slot layout:
65
+ * 0..1 = ships (P1, P2), 2..9 = bullets, 10..15 = enemies. */
66
+ #define MAX_BULLETS 8
67
+ #define MAX_ENEMIES 6
68
+ #define OAM_SHIP 0
69
+ #define OAM_BULLET 2
70
+ #define OAM_ENEMY 10
71
+ #define OAM_COUNT 16
72
+ #define TILE_SHIP 0 /* tile indexes into tilsprite (data.asm) */
73
+ #define TILE_BULLET 1
74
+ #define TILE_ENEMY 2
75
+ #define START_LIVES 3
76
+ #define HUD_Y 24 /* playfield starts below the text HUD row */
77
+ #define FIELD_BOT 208
78
+ #define INV_FRAMES 90 /* post-hit invulnerability (blink) */
46
79
 
47
- #define MAX_BULLETS 6
48
- #define MAX_ENEMIES 6
80
+ /* SRAM layout: [0]=magic "SB", [2]=hi-score, [4]=hiscore ^ 0xA5C3.
81
+ * Magic is written LAST in hiscore_save so a torn write never validates. */
82
+ #define SRAM_MAGIC 0x4253u
83
+
84
+ /* Game states — the shell every example shares: title → play → game over. */
85
+ #define ST_TITLE 0
86
+ #define ST_PLAY 1
87
+ #define ST_OVER 2
49
88
 
50
89
  typedef struct { s16 x, y; u8 alive; } Obj;
51
90
 
52
- static Obj player;
91
+ static u8 state;
92
+ static u8 two_player; /* mode chosen on the title screen */
93
+ static u8 sound_ok;
94
+ static Obj ships[2];
95
+ static u8 ship_inv[2]; /* invulnerability frames after a hit */
96
+ static u8 fire_cd[2];
53
97
  static Obj bullets[MAX_BULLETS];
54
98
  static Obj enemies[MAX_ENEMIES];
55
- static u16 score;
99
+ static u8 lives; /* SHARED pool in co-op (arcade style) */
100
+ static u16 score, hiscore;
101
+ static u8 hud_dirty;
56
102
  static u16 spawn_timer;
103
+ static u16 frame_ct;
104
+ static u16 star_v; /* BG1 vertical scroll (starfield drift) */
105
+ static u16 prev_pad0;
106
+ static char nbuf[8]; /* 5-digit number formatter output */
107
+
108
+ /* BG1 starfield map: 32×32 entries, composed once at boot then scrolled in
109
+ * hardware forever. Static (not a local): >255 bytes of locals overflows
110
+ * tcc's 8-bit stack-relative addressing. */
111
+ static u16 bg_map[32 * 32];
112
+
113
+ /* Headless-test telemetry — written once per frame; a test harness finds it
114
+ * by scanning WRAM for the "SB"+0xB7 signature, then plays the game from
115
+ * real state instead of parsing pixels. Costs ~30 byte-writes; delete freely. */
116
+ static u8 telem[32];
117
+
118
+ /* ── GAME LOGIC (clay) — Galois LFSR (taps $B8), period 255 ────────────────── */
119
+ static u8 rng_state = 0xA5;
120
+ static u8 rand8(void) {
121
+ u8 lsb = (u8)(rng_state & 1);
122
+ rng_state >>= 1;
123
+ if (lsb) rng_state ^= 0xB8;
124
+ return rng_state;
125
+ }
126
+
127
+ /* ── GAME LOGIC (clay) — SRAM hi-score (see sram_* in data.asm) ────────────── */
128
+ static u16 hiscore_load(void) {
129
+ u16 v;
130
+ if (sram_read16(0) != SRAM_MAGIC) return 0;
131
+ v = sram_read16(2);
132
+ if (sram_read16(4) != (u16)(v ^ 0xA5C3u)) return 0;
133
+ return v;
134
+ }
135
+
136
+ static void hiscore_save(u16 v) {
137
+ sram_write16(2, v);
138
+ sram_write16(4, (u16)(v ^ 0xA5C3u));
139
+ sram_write16(0, SRAM_MAGIC); /* magic LAST — torn write = no record */
140
+ }
141
+
142
+ /* ── GAME LOGIC (clay) — text helpers ──────────────────────────────────────── */
143
+ static void fmt5(u16 v) { /* u16 → "00000" into nbuf */
144
+ s8 i;
145
+ for (i = 4; i >= 0; i--) { nbuf[i] = (char)('0' + v % 10); v /= 10; }
146
+ nbuf[5] = 0;
147
+ }
57
148
 
58
- static u8 aabb(Obj* a, Obj* b) {
59
- return a->x < b->x + 8 && a->x + 8 > b->x
60
- && a->y < b->y + 8 && a->y + 8 > b->y;
61
- }
62
-
63
- static void render_score(void) {
64
- char buf[8];
65
- u16 v;
66
- s8 i;
67
- buf[0]='0'; buf[1]='0'; buf[2]='0'; buf[3]='0'; buf[4]='0'; buf[5]=0;
68
- v = score;
69
- for (i = 4; i >= 0; i--) { buf[i] = '0' + (v % 10); v /= 10; }
70
- consoleDrawText(20, 1, buf);
71
- }
72
-
73
- static void fire(void) {
74
- u16 i;
75
- for (i = 0; i < MAX_BULLETS; i++) {
76
- if (!bullets[i].alive) {
77
- bullets[i].x = player.x;
78
- bullets[i].y = player.y - 8;
79
- bullets[i].alive = 1;
80
- return;
81
- }
149
+ static void clear_rows(u16 a, u16 b) {
150
+ u16 y;
151
+ for (y = a; y <= b; y++)
152
+ consoleDrawText(0, y, " ");
153
+ }
154
+
155
+ static void draw_hud(void) {
156
+ fmt5(score); consoleDrawText(3, 1, nbuf);
157
+ fmt5(hiscore); consoleDrawText(13, 1, nbuf);
158
+ nbuf[0] = (char)('0' + lives); nbuf[1] = 0;
159
+ consoleDrawText(23, 1, nbuf);
160
+ hud_dirty = 0;
161
+ }
162
+
163
+ /* ── GAME LOGIC (clay) — firing + spawning ─────────────────────────────────── */
164
+ static void fire_bullet(u8 p) {
165
+ u8 i;
166
+ for (i = 0; i < MAX_BULLETS; i++) {
167
+ if (!bullets[i].alive) {
168
+ bullets[i].x = ships[p].x;
169
+ bullets[i].y = ships[p].y - 8;
170
+ bullets[i].alive = 1;
171
+ if (sound_ok) sfx_play(1); /* pew (voice 0 one-shot) */
172
+ return;
82
173
  }
174
+ }
83
175
  }
84
176
 
85
- /* Write all sprites into the OAM low table. Slot layout: 0=player,
86
- * 1..6=bullets, 7..12=enemies. SPR(slot) = slot*4 (oamSet's id is a BYTE
87
- * offset). Inactive objects park at Y=240 (off-screen) — that's how you
88
- * "hide" a sprite; no oamSetEx needed (oamInitGfxSet leaves slots shown).
89
- * Call oamUpdate() after to DMA the table to hardware. */
177
+ static void spawn_enemy(void) {
178
+ u8 i;
179
+ for (i = 0; i < MAX_ENEMIES; i++) {
180
+ if (!enemies[i].alive) {
181
+ enemies[i].x = (s16)(rand8() % 224) + 8;
182
+ enemies[i].y = HUD_Y;
183
+ enemies[i].alive = 1;
184
+ return;
185
+ }
186
+ }
187
+ }
188
+
189
+ /* AABB, both boxes 8×8. */
190
+ static u8 hits(Obj *a, Obj *b) {
191
+ return a->x < b->x + 8 && a->x + 8 > b->x
192
+ && a->y < b->y + 8 && a->y + 8 > b->y;
193
+ }
194
+
195
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
196
+ * Stage every OAM slot every frame, then ONE oamUpdate(). Inactive objects
197
+ * park at Y=240 (below the 224-line display) — that's how you "hide" a
198
+ * sprite without touching the OAM high table; oamInitGfxSet leaves slots
199
+ * shown. The invulnerability blink also parks the ship every few frames.
200
+ * CHANNEL BUDGET NOTE: oamUpdate only marks the shadow table; PVSnesLib's
201
+ * VBlank ISR DMAs it on CHANNEL 7 every frame, and ch 0 carries the console
202
+ * text upload. If you add HDMA effects (gradient sky, per-line scroll —
203
+ * see the Mode 7 racing example) park them on channels 2-6: a channel can't
204
+ * serve HDMA and that GP-DMA in the same frame, and the ISR silently
205
+ * rewrites ch 7's params each NMI. */
90
206
  static void stage_frame(void) {
91
- u16 i, by, ey;
92
- oamSet(SPR(0), player.x, player.y, 3, 0, 0, 0, 0); /* player tile 0 */
93
- for (i = 0; i < MAX_BULLETS; i++) {
94
- by = bullets[i].alive ? bullets[i].y : 240;
95
- oamSet(SPR(1 + i), bullets[i].x, by, 3, 0, 0, 1, 0); /* gfxoffset = tile INDEX 1 (not a byte offset) */
207
+ u8 i, hide;
208
+ s16 y;
209
+ for (i = 0; i < 2; i++) {
210
+ hide = (u8)(!ships[i].alive || (ship_inv[i] & 4));
211
+ y = hide ? 240 : ships[i].y;
212
+ /* P2 = same tile, OBJ palette 1 (oamSet's LAST arg; CGRAM entry 145
213
+ * is recoloured green in main). */
214
+ oamSet(SPR(OAM_SHIP + i), ships[i].x, y, 3, 0, 0, TILE_SHIP, i);
215
+ }
216
+ for (i = 0; i < MAX_BULLETS; i++) {
217
+ y = bullets[i].alive ? bullets[i].y : 240;
218
+ oamSet(SPR(OAM_BULLET + i), bullets[i].x, y, 3, 0, 0, TILE_BULLET, 0);
219
+ }
220
+ for (i = 0; i < MAX_ENEMIES; i++) {
221
+ y = enemies[i].alive ? enemies[i].y : 240;
222
+ oamSet(SPR(OAM_ENEMY + i), enemies[i].x, y, 3, 0, 0, TILE_ENEMY, 0);
223
+ }
224
+ }
225
+
226
+ /* ── GAME LOGIC (clay) — state entries ─────────────────────────────────────── */
227
+ static void clear_pools(void) {
228
+ u8 i;
229
+ for (i = 0; i < MAX_BULLETS; i++) bullets[i].alive = 0;
230
+ for (i = 0; i < MAX_ENEMIES; i++) enemies[i].alive = 0;
231
+ ships[0].alive = ships[1].alive = 0;
232
+ }
233
+
234
+ static void title_enter(void) {
235
+ clear_pools();
236
+ clear_rows(0, 27);
237
+ consoleDrawText((32 - sizeof(GAME_TITLE) + 1) / 2, 6, GAME_TITLE);
238
+ consoleDrawText(12, 9, "HI");
239
+ fmt5(hiscore); consoleDrawText(15, 9, nbuf);
240
+ consoleDrawText(10, 12, "A - 1P START");
241
+ consoleDrawText(10, 14, "B - 2P CO-OP");
242
+ consoleDrawText(7, 20, "D-PAD MOVE B FIRE");
243
+ prev_pad0 = 0xFFFF; /* swallow the press that ENTERED this state — without
244
+ * this, the START that left the game-over screen
245
+ * instantly restarts (classic edge-detect reuse bug) */
246
+ state = ST_TITLE;
247
+ }
248
+
249
+ static void respawn_ship(u8 p) {
250
+ ships[p].x = p ? 144 : (two_player ? 96 : 124);
251
+ ships[p].y = 200;
252
+ fire_cd[p] = 0;
253
+ }
254
+
255
+ static void play_enter(u8 players) {
256
+ two_player = players;
257
+ clear_pools();
258
+ ships[0].alive = 1;
259
+ ships[1].alive = two_player;
260
+ respawn_ship(0);
261
+ respawn_ship(1);
262
+ ship_inv[0] = ship_inv[1] = 0;
263
+ lives = START_LIVES;
264
+ score = 0;
265
+ spawn_timer = 0;
266
+ clear_rows(0, 27);
267
+ consoleDrawText(0, 1, "SC");
268
+ consoleDrawText(10, 1, "HI");
269
+ consoleDrawText(20, 1, "LV");
270
+ draw_hud();
271
+ state = ST_PLAY;
272
+ }
273
+
274
+ static void game_over(void) {
275
+ u8 newhi = 0;
276
+ if (score > hiscore) {
277
+ hiscore = score;
278
+ /* ── HARDWARE IDIOM (load-bearing) — persists via battery SRAM at
279
+ * $70:0000; works because hdr.asm declares CARTRIDGETYPE $02 +
280
+ * SRAMSIZE $01. Magic+checksum layout, magic written last. ── */
281
+ hiscore_save(hiscore);
282
+ newhi = 1;
283
+ hud_dirty = 1;
284
+ }
285
+ consoleDrawText(11, 13, "GAME OVER");
286
+ if (newhi) consoleDrawText(10, 15, "NEW HI SCORE");
287
+ consoleDrawText(10, 17, "PRESS START");
288
+ if (sound_ok) sfx_play(2);
289
+ prev_pad0 = 0xFFFF; /* swallow the held pad into ST_OVER */
290
+ state = ST_OVER;
291
+ }
292
+
293
+ /* ── GAME LOGIC (clay) — per-player update. THE 2P wiring is one line:
294
+ * padsCurrent(p) reads controller port p (0 = pad 1, 1 = pad 2). ──────────── */
295
+ static void update_ship(u8 p) {
296
+ u16 pad = padsCurrent(p);
297
+ if (!ships[p].alive) return;
298
+ if ((pad & KEY_LEFT) && ships[p].x > 8) ships[p].x -= 2;
299
+ if ((pad & KEY_RIGHT) && ships[p].x < 240) ships[p].x += 2;
300
+ if ((pad & KEY_UP) && ships[p].y > HUD_Y + 8) ships[p].y -= 2;
301
+ if ((pad & KEY_DOWN) && ships[p].y < FIELD_BOT) ships[p].y += 2;
302
+ if ((pad & KEY_B) && fire_cd[p] == 0) {
303
+ fire_bullet(p);
304
+ fire_cd[p] = 10;
305
+ }
306
+ if (fire_cd[p]) --fire_cd[p];
307
+ if (ship_inv[p]) --ship_inv[p];
308
+ }
309
+
310
+ /* ── GAME LOGIC (clay) — the playfield tick ────────────────────────────────── */
311
+ static void play_update(void) {
312
+ u8 i, j;
313
+ u16 interval;
314
+
315
+ update_ship(0);
316
+ if (two_player) update_ship(1);
317
+
318
+ for (i = 0; i < MAX_BULLETS; i++) {
319
+ if (!bullets[i].alive) continue;
320
+ bullets[i].y -= 4;
321
+ if (bullets[i].y < HUD_Y) bullets[i].alive = 0;
322
+ }
323
+
324
+ for (i = 0; i < MAX_ENEMIES; i++) {
325
+ if (!enemies[i].alive) continue;
326
+ enemies[i].y += 1;
327
+ if (enemies[i].y >= 224) enemies[i].alive = 0; /* escaped — no penalty */
328
+ }
329
+
330
+ /* difficulty ramp: spawn faster as the score grows */
331
+ interval = score >> 6;
332
+ interval = (interval >= 20) ? 12 : (32 - interval);
333
+ if (++spawn_timer >= interval) { spawn_timer = 0; spawn_enemy(); }
334
+
335
+ /* bullets ↔ enemies */
336
+ for (i = 0; i < MAX_BULLETS; i++) {
337
+ if (!bullets[i].alive) continue;
338
+ for (j = 0; j < MAX_ENEMIES; j++) {
339
+ if (!enemies[j].alive) continue;
340
+ if (hits(&bullets[i], &enemies[j])) {
341
+ bullets[i].alive = 0;
342
+ enemies[j].alive = 0;
343
+ if (score < 65500) score += 10;
344
+ hud_dirty = 1;
345
+ if (sound_ok) sfx_play(2); /* boom */
346
+ break;
347
+ }
96
348
  }
97
- for (i = 0; i < MAX_ENEMIES; i++) {
98
- ey = enemies[i].alive ? enemies[i].y : 240;
99
- oamSet(SPR(7 + i), enemies[i].x, ey, 3, 0, 0, 2, 0); /* gfxoffset = tile INDEX 2 (not a byte offset) */
349
+ }
350
+
351
+ /* enemies ships: SHARED life pool (arcade co-op) + invulnerability
352
+ * blink, so one bad wave can't drain every life in a single overlap */
353
+ for (j = 0; j < MAX_ENEMIES; j++) {
354
+ if (!enemies[j].alive) continue;
355
+ for (i = 0; i < 2; i++) {
356
+ if (!ships[i].alive || ship_inv[i]) continue;
357
+ if (hits(&enemies[j], &ships[i])) {
358
+ enemies[j].alive = 0;
359
+ if (sound_ok) sfx_play(2);
360
+ if (lives) --lives;
361
+ hud_dirty = 1;
362
+ if (lives == 0) { game_over(); return; }
363
+ respawn_ship(i);
364
+ ship_inv[i] = INV_FRAMES;
365
+ }
100
366
  }
367
+ }
101
368
  }
102
369
 
103
- static void spawn(void) {
104
- u16 i;
105
- for (i = 0; i < MAX_ENEMIES; i++) {
106
- if (!enemies[i].alive) {
107
- enemies[i].x = ((spawn_timer * 37) & 0xFF) % (256 - 16) + 4;
108
- enemies[i].y = 0;
109
- enemies[i].alive = 1;
110
- return;
111
- }
370
+ /* ── GAME LOGIC (clay) — boot-time starfield composition ─────────────────────
371
+ * Two space tones in a checker (so no single colour ever dominates the
372
+ * screen) + LFSR-scattered star tiles. Map entries: tile index | 0x0400 =
373
+ * palette block 1 (bits 10-12), keeping the console font palette (block 0)
374
+ * untouched HUD text stays white/legible. */
375
+ static void build_starfield(void) {
376
+ u16 r, c, e;
377
+ u8 v;
378
+ for (r = 0; r < 32; r++) {
379
+ for (c = 0; c < 32; c++) {
380
+ e = ((r ^ c) & 1) ? 1 : 0; /* space A / space B checker */
381
+ v = rand8();
382
+ if ((v & 0x1F) == 0) e = 2; /* bright star (on tone A) */
383
+ else if ((v & 0x1F) == 1) e = 3; /* gold star (on tone B) */
384
+ bg_map[(r << 5) + c] = (u16)(0x0400 | e);
112
385
  }
386
+ }
387
+ }
388
+
389
+ static void telem_update(void) {
390
+ u8 i;
391
+ telem[0] = 'S'; telem[1] = 'B'; telem[2] = 0xB7;
392
+ telem[3] = state;
393
+ telem[4] = lives;
394
+ telem[5] = (u8)((sound_ok << 7) | (two_player << 1));
395
+ telem[6] = (u8)score; telem[7] = (u8)(score >> 8);
396
+ telem[8] = (u8)hiscore; telem[9] = (u8)(hiscore >> 8);
397
+ telem[10] = (u8)ships[0].x; telem[11] = (u8)ships[0].y;
398
+ telem[12] = (u8)ships[1].x; telem[13] = (u8)ships[1].y;
399
+ telem[14] = (u8)(ships[0].alive | (ships[1].alive << 1)
400
+ | ((ship_inv[0] != 0) << 2) | ((ship_inv[1] != 0) << 3));
401
+ for (i = 0; i < MAX_ENEMIES; i++) {
402
+ telem[15 + (i << 1)] = enemies[i].alive ? (u8)enemies[i].x : 0xFF;
403
+ telem[16 + (i << 1)] = enemies[i].alive ? (u8)enemies[i].y : 0xFF;
404
+ }
405
+ telem[27] = (u8)frame_ct;
113
406
  }
114
407
 
115
408
  int main(void) {
116
- u16 i, j, pad;
117
- u16 prev = 0;
118
- u16 by, ey;
119
-
120
- dmaClearVram();
121
-
122
- consoleSetTextMapPtr(0x6800);
123
- consoleSetTextGfxPtr(0x3000);
124
- consoleSetTextOffset(0x0000); /* tile index = (char-0x20); font is at the BG char base */
125
- consoleInitText(0, 16 * 2, &tilfont, &palfont);
126
- setMode(BG_MODE1, 0);
127
- /* BG0 is the text-console HUD layer. consoleInitText DMAs the font
128
- * but does NOT set the PPU BG base registers — point BG0 at the same
129
- * font ($3000) + map ($6800) so the HUD text renders. */
130
- bgSetGfxPtr(0, 0x3000);
131
- bgSetMapPtr(0, 0x6800, SC_32x32);
132
-
133
- /* BG1 = full-screen wallpaper so the playfield never reads as blank.
134
- * Tiles → VRAM $2000, map → VRAM $4000 (clear of sprites $0000 and the
135
- * console gfx $3000 / map $6800). One 8×8 tile = 32 bytes of gfx +
136
- * 32 bytes of palette. */
137
- bgInitTileSet(1, (u8 *)&tilbg, (u8 *)&palbg,
138
- 1, /* load palbg into CGRAM palette block 1 */
139
- 32, 32, BG_16COLORS, 0x2000);
140
- /* Every map entry: tile 0, palette block 1 (bits 10-12 = 1 → 0x0400),
141
- * so the wallpaper uses palbg and leaves the console font palette
142
- * (block 0) untouched HUD text stays white/legible. */
143
- for (i = 0; i < 32 * 32; i++) bg_map[i] = 0x0400;
144
- bgInitMapSet(1, (u8 *)bg_map, sizeof(bg_map), SC_32x32, 0x4000);
145
- bgSetEnable(1);
146
-
147
- bgSetDisable(2);
148
- setPaletteColor(0, RGB5(0, 0, 6)); /* dark-blue backdrop (CGRAM 0) */
149
-
150
- /* 3 sprite tiles (ship/bullet/enemy) × 32 bytes = 96 bytes. */
151
- oamInitGfxSet(&tilsprite, 96, &palsprite, 32, 0,
152
- 0x0000, OBJ_SIZE8_L16);
153
-
154
- player.x = 120; player.y = 180; player.alive = 1;
155
- for (i = 0; i < MAX_BULLETS; i++) bullets[i].alive = 0;
156
- for (i = 0; i < MAX_ENEMIES; i++) enemies[i].alive = 0;
157
- score = 0;
158
- spawn_timer = 0;
159
-
160
- consoleDrawText(14, 1, "SCORE");
161
- consoleDrawText(2, 26, "D-PAD MOVE B FIRE");
162
-
163
- /* Pre-stage all 13 OAM slots off-screen (SPR() = slot*4 oamSet's id
164
- * is a BYTE OFFSET). After oamInitGfxSet, sprites default to shown, so
165
- * just set position; hide an inactive sprite by parking it at Y=240. */
166
- for (i = 0; i < 13; i++) oamSet(SPR(i), 0, 240, 3, 0, 0, 0, 0);
167
-
168
- /* Stage the first real frame + flush it to OAM BEFORE the screen turns
169
- * on, so frame 1 shows the game (not power-on garbage). */
409
+ u16 pad;
410
+
411
+ /* ── HARDWARE IDIOM (load-bearing — see TROUBLESHOOTING) ──
412
+ * Init order: console text pointers FIRST, then mode, then per-BG base
413
+ * registers, then VRAM uploads — all while the screen is still off.
414
+ * consoleInitText DMAs the font but does NOT set the PPU BG base
415
+ * registers; bgSetGfxPtr/bgSetMapPtr for BG0 must repeat the same
416
+ * addresses or the HUD renders garbage. */
417
+ consoleSetTextMapPtr(0x6800);
418
+ consoleSetTextGfxPtr(0x3000);
419
+ consoleSetTextOffset(0x0000); /* tile index = (char - 0x20) */
420
+ consoleInitText(0, 16 * 2, &tilfont, &palfont);
421
+ setMode(BG_MODE1, 0);
422
+ bgSetGfxPtr(0, 0x3000);
423
+ bgSetMapPtr(0, 0x6800, SC_32x32);
424
+
425
+ /* BG1 = the scrolling starfield. 4 tiles → VRAM $2000, map → $4000
426
+ * (clear of sprites $0000, the console font $3000 and map $6800). */
427
+ bgInitTileSet(1, (u8 *)&tilbg, (u8 *)&palbg,
428
+ 1, /* palbg CGRAM palette block 1 */
429
+ 4 * 32, 32, BG_16COLORS, 0x2000);
430
+ build_starfield();
431
+ bgInitMapSet(1, (u8 *)bg_map, sizeof(bg_map), SC_32x32, 0x4000);
432
+ bgSetEnable(1);
433
+ bgSetDisable(2); /* BG2 carries garbage in mode 1 off */
434
+
435
+ setPaletteColor(0, RGB5(0, 0, 3)); /* backdrop: near-black space */
436
+ /* P2's ship: OBJ palette 1 (CGRAM 144+), colour 1 recoloured green.
437
+ * Same tile as P1 — only oamSet's palette argument differs. */
438
+ setPaletteColor(145, RGB5(6, 28, 10));
439
+
440
+ /* 3 sprite tiles (ship/bullet/enemy) × 32 bytes = 96 bytes. */
441
+ oamInitGfxSet(&tilsprite, 96, &palsprite, 32, 0, 0x0000, OBJ_SIZE8_L16);
442
+
443
+ /* ── HARDWARE IDIOM (load-bearing) stage + flush OAM BEFORE the screen
444
+ * turns on, so frame 1 shows the game (not power-on OAM garbage). ── */
445
+ clear_pools();
446
+ stage_frame();
447
+ oamUpdate();
448
+
449
+ setScreenOn();
450
+
451
+ /* ── HARDWARE IDIOM (load-bearing) — sfx_init AFTER setScreenOn, and CHECK
452
+ * the return: a wedged SPC700 must not take the video down with it. ── */
453
+ sound_ok = (sfx_init() == 0);
454
+ /* ── HARDWARE IDIOM (load-bearing) one frame between init and the first
455
+ * command. sfx_init returns the instant the SPC echoes the jump command,
456
+ * but the driver then spends ~50 port writes initialising the DSP BEFORE
457
+ * it seeds its command edge-detector from $2140. Send a command in that
458
+ * window and the seed swallows it music silently never starts. A
459
+ * WaitForVBlank is thousands of SPC cycles deterministic cure. ── */
460
+ WaitForVBlank();
461
+ if (sound_ok) sfx_music_play();
462
+
463
+ hiscore = hiscore_load(); /* battery SRAM — 0 on first boot */
464
+ star_v = 0;
465
+ frame_ct = 0;
466
+ title_enter();
467
+
468
+ while (1) {
469
+ pad = padsCurrent(0);
470
+
471
+ if (state == ST_TITLE) {
472
+ /* ── GAME LOGIC (clay) — title: A/START = 1P, B = 2P co-op ── */
473
+ if ((pad & KEY_A && !(prev_pad0 & KEY_A)) ||
474
+ (pad & KEY_START && !(prev_pad0 & KEY_START))) {
475
+ play_enter(0);
476
+ } else if (pad & KEY_B && !(prev_pad0 & KEY_B)) {
477
+ play_enter(1);
478
+ }
479
+ } else if (state == ST_PLAY) {
480
+ play_update();
481
+ } else { /* ST_OVER — field frozen, stars keep drifting */
482
+ if (pad & KEY_START && !(prev_pad0 & KEY_START)) title_enter();
483
+ }
484
+ prev_pad0 = pad;
485
+
486
+ /* starfield drift: ~0.5 px/frame downward. VOFS decreasing moves BG
487
+ * content DOWN the screen; the 256-px map wraps in hardware. */
488
+ if (frame_ct & 1) --star_v;
489
+ frame_ct++;
490
+
491
+ telem_update();
170
492
  stage_frame();
171
493
  oamUpdate();
494
+ if (hud_dirty) draw_hud();
172
495
 
173
- /* Screen ON first, THEN sound. If the SPC wedges, sfx_init returns
174
- * nonzero and we keep running silently — never a black screen. */
175
- setScreenOn();
176
- sfx_init();
177
-
178
- while (1) {
179
- pad = padsCurrent(0);
180
-
181
- if (pad & KEY_LEFT && player.x > 8) player.x -= 2;
182
- if (pad & KEY_RIGHT && player.x < 256 - 16) player.x += 2;
183
- if (pad & KEY_UP && player.y > 16) player.y -= 2;
184
- if (pad & KEY_DOWN && player.y < 224 - 16) player.y += 2;
185
- if ((pad & KEY_B) && !(prev & KEY_B)) { fire(); sfx_play(1); }
186
- prev = pad;
187
-
188
- for (i = 0; i < MAX_BULLETS; i++) {
189
- if (!bullets[i].alive) continue;
190
- bullets[i].y -= 4;
191
- if (bullets[i].y < 0 || bullets[i].y > 230) bullets[i].alive = 0;
192
- }
193
- for (i = 0; i < MAX_ENEMIES; i++) {
194
- if (!enemies[i].alive) continue;
195
- enemies[i].y += 1;
196
- if (enemies[i].y >= 224) enemies[i].alive = 0;
197
- }
198
- if (++spawn_timer >= 28) { spawn_timer = 0; spawn(); }
199
-
200
- for (i = 0; i < MAX_BULLETS; i++) {
201
- if (!bullets[i].alive) continue;
202
- for (j = 0; j < MAX_ENEMIES; j++) {
203
- if (!enemies[j].alive) continue;
204
- if (aabb(&bullets[i], &enemies[j])) {
205
- bullets[i].alive = 0;
206
- enemies[j].alive = 0;
207
- if (score < 65500) score += 10;
208
- sfx_play(2);
209
- break;
210
- }
211
- }
212
- }
213
-
214
- stage_frame();
215
- oamUpdate();
216
-
217
- render_score();
218
- WaitForVBlank();
219
- consoleVblank();
220
- }
221
- return 0;
496
+ WaitForVBlank();
497
+ bgSetScroll(1, 0, star_v); /* scroll regs: write inside vblank */
498
+ consoleVblank();
499
+ }
500
+ return 0;
222
501
  }