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,186 +1,607 @@
1
- /* ── platformer.c — SNES PVSnesLib SIDE-SCROLLING platformer scaffold
1
+ /* ── platformer.c — SNES side-scrolling platformer (complete example game) ───
2
2
  *
3
- * A horizontally scrolling platformer: gravity + jump physics with
4
- * land-on-top collision against a static platform list spread across a
5
- * 512-px world. A camera follows the player; the BG scrolls in hardware
6
- * via bgSetScroll(0, camX, 0) and the player sprite draws in SCREEN
7
- * space (worldX - camX). Mirrors the GB/Genesis side-scroller scaffolds.
3
+ * CRAG CAPER a COMPLETE, working game: title screen, 1P mode and 2P
4
+ * ALTERNATING-TURNS mode (arcade-classic: players swap on death; each player
5
+ * has their own score and own 3 lives; player 2 plays on CONTROLLER 2),
6
+ * coins + distance scoring, persistent hi-score (battery SRAM), SPC music +
7
+ * SFX, and the SNES's answer to the fixed-HUD-over-scrolling-field problem:
8
+ * the HUD is simply ANOTHER BACKGROUND LAYER with its own scroll register.
8
9
  *
9
- * Physics is fixed-point: x/y in 1/16-pixel subpixel units for sub-pixel
10
- * acceleration.
10
+ * THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
11
+ * very different one. The markers tell you what's what:
12
+ * HARDWARE IDIOM (load-bearing) — dodges a documented SNES footgun; reshape
13
+ * your gameplay around it (see TROUBLESHOOTING before changing).
14
+ * GAME LOGIC (clay) — level layout, physics tuning, scoring, art: reshape
15
+ * freely.
11
16
  *
12
- * NOTE ON VISUALS: this scaffold uses the PVSnesLib console (text) BG,
13
- * which has no tiled platform art so as you scroll you see the on-BG
14
- * text slide (proof the hardware scroll register is moving) while the
15
- * player stays screen-centered. For visible tiled platforms across a
16
- * wide world, build a real tileset with gfx2snes + bgInitTileSet on a
17
- * 64-wide map and stream tilemap columns into VRAM during vblank as the
18
- * camera advances. See the SNES MENTAL_MODEL.md "Horizontal scrolling".
17
+ * What depends on what:
18
+ * data.asmfont + sprite/level tiles, sram_read16/write16 (battery SRAM
19
+ * needs 24-bit addressing tcc can't emit), and the bank-$7E telem block.
20
+ * Load-bearing.
21
+ * hdr.asm THIS PROJECT OVERRIDES the stock header to declare battery
22
+ * SRAM (CARTRIDGETYPE $02 + SRAMSIZE $01). Delete that file and saves
23
+ * silently stop existing the build still succeeds.
24
+ * snes_sfx.{h,c} + snes_sfx_data.asm + apu_blob.bin — the SPC700 sound
25
+ * driver (music + 2 one-shot samples). #include'd, not separately built.
26
+ *
27
+ * ── THE TWO-LAYER SPLIT (the SNES bonus this example teaches) ───────────────
28
+ * Mode 1 gives three independent background layers, EACH with its own
29
+ * H/V scroll registers. So a fixed HUD over a scrolling playfield is just:
30
+ * BG0 (text console) — HUD + all menu text. Its scroll stays (0,0). Ever.
31
+ * BG1 — the level. One register write per frame (bgSetScroll) moves it.
32
+ * Zero raster tricks, zero CPU. Contrast the NES platformer example (this
33
+ * game's direct ancestor): the NES has ONE scroll for the WHOLE frame, so
34
+ * its fixed HUD costs a sprite-0-hit polling spin — ~35 scanlines of CPU
35
+ * burned EVERY frame waiting for the beam to clear the HUD before rewriting
36
+ * PPUSCROLL mid-frame. On SNES you only reach for that kind of mid-frame
37
+ * machinery (HDMA) when one layer must be two things at once — see the
38
+ * racing example's Mode 1/Mode 7 split.
39
+ *
40
+ * The level itself: a 256-px-wide COLUMN MAP (ground height + one-way
41
+ * platforms + pits) painted once into a 32x32 tilemap. 256 px is exactly
42
+ * the map's width, so a uint8 scroll wraps seamlessly — an endless looping
43
+ * run of pits, platforms, coins and spikes. Coins/spikes are sprites that
44
+ * drift with the scroll (world-anchored while on screen, respawning at the
45
+ * right edge).
19
46
  */
20
47
 
21
48
  #include <snes.h>
22
49
  #include "snes_sfx.c"
23
50
 
24
- extern char tilfont, palfont;
25
- extern char tilsprite, palsprite;
26
- extern char tilbg, palbg; /* wallpaper tile + palette (data.asm) */
51
+ /* The title screen renders this — examples({op:'fork'}) stamps your game's
52
+ * name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
53
+ #define GAME_TITLE "CRAG CAPER"
54
+
55
+ extern char tilfont, palfont; /* HUD font + text palette (data.asm) */
56
+ extern char tilsprite, palsprite; /* player/coin/spike tiles + palette */
57
+ extern char tilbg, palbg; /* level tiles + sky/dirt/grass colours*/
27
58
 
28
59
  /* consoleVblank() copies the dirty text tilemap to VRAM during VBlank.
29
60
  * No public prototype in console.h, so declare it; call once per frame. */
30
61
  extern void consoleVblank(void);
31
62
 
32
- /* BG1 wallpaper map: a full 32x32 screen of the 4-colour tile so the
33
- * playfield reads as a real backdrop, not flat blank. Filled at runtime. */
34
- static u16 bg_map[32 * 32];
35
-
36
- typedef struct { s16 x, y, w, h; } Rect;
37
-
38
- #define WORLD_W 256 /* = the 32x32 console map width, so platforms are drawable */
39
- #define SCREEN_W 256
40
-
41
- /* Platforms in WORLD coords. The world is 256 px — exactly the 32x32
42
- * console map so every platform can be DRAWN on the text layer and
43
- * scrolls 1:1 with the physics. (The old 512-px world only existed as
44
- * invisible collision rects: the 32-col map can't show cols 32-63, so
45
- * the player appeared to stand and jump on nothing.) */
46
- static const Rect platforms[] = {
47
- { 0, 200, 256, 24 }, /* floor spans the world */
48
- { 24, 168, 56, 8 },
49
- { 104, 144, 64, 8 },
50
- { 184, 112, 48, 8 },
51
- { 48, 96, 40, 8 },
52
- { 208, 72, 40, 8 },
63
+ /* data.asm exports battery SRAM accessors ($70:0000 long addressing) and
64
+ * the bank-$7E telemetry block a headless test can find by scanning. */
65
+ extern u16 sram_read16(u16 offset);
66
+ extern void sram_write16(u16 offset, u16 value);
67
+ extern u8 telem[];
68
+
69
+ /* ── GAME LOGIC (clay reshape freely) ──────────────────────────────────────
70
+ * VRAM budget (word addresses):
71
+ * $0000 OBJ tiles, $2000 level tiles, $3000 HUD font,
72
+ * $4000 level map (BG1), $6800 HUD/console text map (BG0).
73
+ * Sprite tile numbers + the level tile numbers the map painter uses. */
74
+ #define TILE_IDLE 0
75
+ #define TILE_JUMP 1
76
+ #define TILE_COIN 2
77
+ #define TILE_SPIKE 3
78
+ #define BG_CLOUD 1
79
+ #define BG_DIRT 2
80
+ #define BG_GRASS 3 /* also used for floating platforms (grass slabs) */
81
+
82
+ /* ── GAME LOGIC (clay) — the level ───────────────────────────────────────────
83
+ * A 32-column map; world x = (screen x + scroll) mod 256.
84
+ * ground_row[c] — tilemap row of the ground's grass top, 0xFF = pit.
85
+ * plat_row[c] — row of a one-way floating platform, 0 = none.
86
+ * Rows are tilemap rows (y = row*8). The SNES screen shows rows 0-27. */
87
+ #define NO_GROUND 0xFF
88
+ static const u8 ground_row[32] = {
89
+ 26, 26, 26, 26, 26, 26, 26, 26, /* start runway */
90
+ 26, NO_GROUND, NO_GROUND, 26, 26, 26, 26, 26, /* pit 1 (16 px) */
91
+ 26, 26, 26, 26, NO_GROUND, NO_GROUND, NO_GROUND, /* pit 2 (24 px) */
92
+ 26, 26, 26, 26, 26, 26, 26, 26, 26,
53
93
  };
54
- #define N_PLATFORMS (sizeof(platforms) / sizeof(platforms[0]))
55
-
56
- #define GRAVITY 12
57
- #define MOVE_SPEED 24
58
- #define JUMP_VEL -200
59
- #define MAX_FALL 320
60
-
61
- static u8 on_platform(s16 px, s16 py) {
62
- u16 i;
63
- const Rect* p;
64
- for (i = 0; i < N_PLATFORMS; i++) {
65
- p = &platforms[i];
66
- if (py + 8 == p->y && px + 8 > p->x && px < p->x + p->w) return 1;
94
+ static const u8 plat_row[32] = {
95
+ 0, 0, 0, 0, 21, 21, 21, 0, /* slab before pit 1 */
96
+ 0, 0, 0, 0, 0, 0, 20, 20, /* slab mid-level */
97
+ 20, 0, 0, 0, 0, 0, 0, 0,
98
+ 0, 21, 21, 21, 0, 0, 0, 0, /* slab near the loop */
99
+ };
100
+
101
+ /* ── GAME LOGIC (clay) physics + tuning ── */
102
+ #define GRAVITY_Q44 1 /* +1/16 px per frame per frame */
103
+ #define JUMP_VEL_Q44 (-40) /* launch vy (Q4.4) → ~50 px / ~6 tile apex */
104
+ #define MAX_VY_Q44 80 /* terminal velocity, 5 px/frame MUST stay *
105
+ * under 6: the landing probe's 6-px window *
106
+ * can't catch a faster fall (tunnelling) */
107
+ #define MOVE_SPEED 2 /* px/frame walk + scroll speed */
108
+ #define SCROLL_WALL 112 /* px: past this the world scrolls, not you */
109
+ #define GROUND_TOP 208 /* ground_row 26 * 8 */
110
+ #define SPIKE_Y 200 /* spikes stand on the ground */
111
+ #define NUM_COINS 3
112
+ #define NUM_SPIKES 2
113
+ #define START_LIVES 3
114
+
115
+ /* SRAM layout: [0]=magic "CG", [2]=hi-score, [4]=hi ^ 0x5AC3.
116
+ * Magic is written LAST in hi_save so a torn write never validates. */
117
+ #define SRAM_MAGIC 0x4743u
118
+
119
+ /* Game states — the shell every example shares: title → play → game over. */
120
+ #define ST_TITLE 0
121
+ #define ST_PLAY 1
122
+ #define ST_OVER 2
123
+
124
+ static u8 state;
125
+ static u8 px; /* player screen x */
126
+ static u16 py_q44; /* player y, Q4.4 fixed point — gravity adds
127
+ * <1 px/frame near the jump apex, so we
128
+ * need sub-pixel precision */
129
+ static s8 vy_q44;
130
+ static u8 on_ground;
131
+ static u8 scroll_x; /* level scroll — u8 wraps at 256 = exactly *
132
+ * one level loop (seamless) */
133
+ static u8 dist_sub; /* sub-counter: 64 px scrolled = +1 pt */
134
+ static u8 coin_x[NUM_COINS], coin_y[NUM_COINS];
135
+ static u8 spike_x[NUM_SPIKES], spike_active[NUM_SPIKES];
136
+
137
+ /* Players: index 0 = P1 (controller 1), 1 = P2 (controller 2 — alternating
138
+ * turns, arcade-classic style). Each has own score + own lives; the HUD
139
+ * shows the CURRENT player's numbers. */
140
+ static u8 two_player;
141
+ static u8 cur_player;
142
+ static u8 p_lives[2];
143
+ static u16 p_score[2];
144
+ static u16 hiscore;
145
+ static u8 turn_pause; /* freeze frames after a turn change */
146
+ static u8 sound_ok;
147
+ static u16 rng = 0xC0DE;
148
+ static u16 prev_pad0, prev_padP;
149
+ static u8 attract_sub; /* title attract: scroll every 2nd frame */
150
+ static char tbuf[8]; /* 5-digit score formatter output */
151
+
152
+ static u16 bg_map[32 * 32]; /* level tilemap staging (DMA'd at boot) */
153
+
154
+ /* ── GAME LOGIC (clay) — xorshift16 PRNG (~tens of cycles per call) ── */
155
+ static u8 random8(void) {
156
+ u16 r = rng;
157
+ r ^= r << 7;
158
+ r ^= r >> 9;
159
+ r ^= r << 8;
160
+ rng = r;
161
+ return (u8)r;
162
+ }
163
+
164
+ static u8 dist8(u8 a, u8 b) {
165
+ return (a > b) ? (u8)(a - b) : (u8)(b - a);
166
+ }
167
+
168
+ /* ── GAME LOGIC (clay) — battery SRAM hi-score (see sram_* in data.asm) ───── */
169
+ static u16 hi_load(void) {
170
+ u16 v;
171
+ if (sram_read16(0) != SRAM_MAGIC) return 0;
172
+ v = sram_read16(2);
173
+ if (sram_read16(4) != (u16)(v ^ 0x5AC3u)) return 0;
174
+ return v;
175
+ }
176
+
177
+ static void hi_save(u16 v) {
178
+ sram_write16(2, v);
179
+ sram_write16(4, (u16)(v ^ 0x5AC3u));
180
+ sram_write16(0, SRAM_MAGIC); /* magic LAST — torn write = no record */
181
+ }
182
+
183
+ /* ── GAME LOGIC (clay) — text helpers ──────────────────────────────────────── */
184
+ static void fmt_u16(u16 v) { /* 5 right-aligned digits into tbuf */
185
+ u8 i;
186
+ for (i = 0; i < 5; i++) { tbuf[4 - i] = (char)('0' + v % 10); v /= 10; }
187
+ tbuf[5] = 0;
188
+ }
189
+
190
+ static void clear_row(u16 y) {
191
+ consoleDrawText(0, y, " ");
192
+ }
193
+
194
+ static void clear_rows(u16 a, u16 b) {
195
+ u16 y;
196
+ for (y = a; y <= b; y++) clear_row(y);
197
+ }
198
+
199
+ /* HUD row 1, on BG0 — fixed because BG0's scroll never moves (see the
200
+ * two-layer split note up top). Layout: "P1 L3 SC 00000 HI 00000". */
201
+ static void draw_hud(void) {
202
+ consoleDrawText(1, 1, cur_player ? "P2" : "P1");
203
+ tbuf[0] = 'L'; tbuf[1] = (char)('0' + p_lives[cur_player]); tbuf[2] = 0;
204
+ consoleDrawText(4, 1, tbuf);
205
+ fmt_u16(p_score[cur_player]);
206
+ consoleDrawText(10, 1, tbuf);
207
+ }
208
+
209
+ static void draw_hud_labels(void) {
210
+ consoleDrawText(7, 1, "SC");
211
+ consoleDrawText(17, 1, "HI");
212
+ fmt_u16(hiscore);
213
+ consoleDrawText(20, 1, tbuf);
214
+ }
215
+
216
+ /* ── GAME LOGIC (clay) — paint the level from the column map ─────────────────
217
+ * Composed once in WRAM and DMA'd to VRAM at boot (bgInitMapSet). The level
218
+ * is static; only the scroll register moves it. Rows 0-2 stay sky so the
219
+ * HUD text floats over clean backdrop. Map entries are plain tile numbers
220
+ * (palette block 0, no flips, no priority). */
221
+ static void paint_level(void) {
222
+ u8 r, c, g;
223
+ u16 t;
224
+ for (r = 0; r < 32; r++) {
225
+ for (c = 0; c < 32; c++) {
226
+ g = ground_row[c];
227
+ t = 0; /* sky backdrop */
228
+ if (plat_row[c] && r == plat_row[c]) t = BG_GRASS; /* floating slab */
229
+ else if (g != NO_GROUND) {
230
+ if (r == g) t = BG_GRASS; /* ground surface */
231
+ else if (r > g) t = BG_DIRT; /* ground body */
232
+ }
233
+ if (t == 0 && r >= 14 && r <= 18) { /* sparse cloud band */
234
+ if (((r * 7 + c * 5) & 15) == 0) t = BG_CLOUD;
235
+ }
236
+ bg_map[(u16)(r << 5) + c] = t;
67
237
  }
68
- return 0;
238
+ }
69
239
  }
70
240
 
71
- int main(void) {
72
- s32 px = 32 << 4, py = 100 << 4;
73
- s32 vx = 0, vy = 0;
74
- s32 np;
75
- s16 ipx, ipy, npy, camX = 0;
76
- u8 grounded;
77
- u16 i, pad, prev = 0;
78
- const Rect* p;
79
-
80
- consoleSetTextMapPtr(0x6800);
81
- consoleSetTextGfxPtr(0x3000);
82
- consoleSetTextOffset(0x0000); /* tile index = (char-0x20); font is at the BG char base */
83
- consoleInitText(0, 16 * 2, &tilfont, &palfont);
84
- setMode(BG_MODE1, 0);
85
- /* consoleInitText DMAs the font but does NOT set the PPU BG base
86
- * registers — point BG0 at the same font ($3000) + map ($6800). */
87
- bgSetGfxPtr(0, 0x3000);
88
- bgSetMapPtr(0, 0x6800, SC_32x32);
89
-
90
- /* BG1 = full-screen wallpaper so the playfield never reads as blank.
91
- * Tiles -> VRAM $2000, map -> VRAM $4000 (clear of sprites $0000 and
92
- * the console gfx $3000 / map $6800). Map entries use palette block 1
93
- * (0x0400) so the wallpaper palette doesn't disturb the console font
94
- * palette in block 0 (HUD text stays legible). */
95
- bgInitTileSet(1, (u8 *)&tilbg, (u8 *)&palbg, 1,
96
- 32, 32, BG_16COLORS, 0x2000);
97
-
98
- /* Per-genre backdrop tint every SNES scaffold used to ship the same
99
- * blue checkered wallpaper ('no variety'). Recolor the wallpaper's
100
- * CGRAM entries (block 1 = entries 16+) to a sky blue scheme. */
101
- setPaletteColor(0, RGB5(10,18,28));
102
- setPaletteColor(17, RGB5(13,21,29));
103
- setPaletteColor(18, RGB5(8,15,26));
104
- for (i = 0; i < 32 * 32; i++) bg_map[i] = 0x0400;
105
- bgInitMapSet(1, (u8 *)bg_map, sizeof(bg_map), SC_32x32, 0x4000);
106
- bgSetEnable(1);
107
- bgSetDisable(2);
108
-
109
- oamInitGfxSet(&tilsprite, 32, &palsprite, 32, 0, 0x0000, OBJ_SIZE8_L16);
110
-
111
- consoleDrawText(2, 1, "D-PAD MOVE A JUMP");
112
- /* Column markers across the BG so the hardware scroll is visible as
113
- * you move (the console map is 32 cells / 256 px and wraps). */
114
- /* Draw every platform as a row of '=' on the (scrolling) text layer
115
- * so the player can SEE what they're standing on. */
116
- {
117
- char buf[33];
118
- u16 pi, k, cols;
119
- for (pi = 0; pi < N_PLATFORMS; pi++) {
120
- cols = platforms[pi].w / 8;
121
- if (cols > 32) cols = 32;
122
- for (k = 0; k < cols; k++) buf[k] = '=';
123
- buf[cols] = 0;
124
- consoleDrawText(platforms[pi].x / 8, platforms[pi].y / 8, buf);
125
- }
241
+ /* ── GAME LOGIC (clay) — coins + spikes (sprite objects in the world) ── */
242
+ static const u8 coin_heights[4] = { 184, 160, 128, 152 };
243
+ static void respawn_coin(u8 i) {
244
+ coin_x[i] = (u8)(232 + (random8() & 15)); /* enter at the right */
245
+ coin_y[i] = coin_heights[random8() & 3];
246
+ }
247
+
248
+ static void try_spawn_spike(u8 i) {
249
+ /* Anchor only over ground: an inactive spike rolls a low per-frame
250
+ * chance, and only spawns if the level column entering at the right
251
+ * edge has ground under it (never floats over a pit). */
252
+ u8 c = (u8)(248 + scroll_x) >> 3;
253
+ if (ground_row[c] == NO_GROUND) return;
254
+ if (random8() > 4) return;
255
+ spike_x[i] = 248;
256
+ spike_active[i] = 1;
257
+ }
258
+
259
+ /* Hide every gameplay sprite (OAM ids 0,4,..,20 = player, 3 coins, 2 spikes). */
260
+ static void hide_actors(void) {
261
+ u8 i;
262
+ for (i = 0; i < 24; i += 4) oamSetVisible(i, OBJ_HIDE);
263
+ }
264
+
265
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
266
+ * Stage ALL sprites BEFORE WaitForVBlank. PVSnesLib's NMI handler DMAs the
267
+ * shadow OAM to the real OAM every vblank (on channel 7 — never park HDMA
268
+ * there), copying whatever the shadow holds AT THAT MOMENT. Stage-then-wait;
269
+ * flipping it shows stale/empty sprites. oamSet rewrites x/y, which is also
270
+ * what un-hides a sprite after OBJ_HIDE (hide just parks it off-screen). */
271
+ static void stage_actors(void) {
272
+ u8 i, y8;
273
+ y8 = (u8)(py_q44 >> 4);
274
+ /* Blink the player during the turn-change breather. */
275
+ if (turn_pause == 0 || (turn_pause & 4))
276
+ oamSet(0, px, y8, 3, 0, 0, on_ground ? TILE_IDLE : TILE_JUMP, 0);
277
+ else
278
+ oamSetVisible(0, OBJ_HIDE);
279
+ for (i = 0; i < NUM_COINS; i++)
280
+ oamSet((u16)(4 + (i << 2)), coin_x[i], coin_y[i], 3, 0, 0, TILE_COIN, 0);
281
+ for (i = 0; i < NUM_SPIKES; i++) {
282
+ if (spike_active[i])
283
+ oamSet((u16)(16 + (i << 2)), spike_x[i], SPIKE_Y, 3, 0, 0, TILE_SPIKE, 0);
284
+ else
285
+ oamSetVisible((u16)(16 + (i << 2)), OBJ_HIDE);
286
+ }
287
+ }
288
+
289
+ /* ── GAME LOGIC (clay) state entries ─────────────────────────────────────── */
290
+ static void title_enter(void) {
291
+ bgSetEnable(1); /* the level scrolls behind the title */
292
+ hide_actors();
293
+ clear_rows(0, 27);
294
+ consoleDrawText(11, 3, GAME_TITLE);
295
+ consoleDrawText(10, 6, "A - 1P GAME");
296
+ consoleDrawText(10, 7, "B - 2P TURNS");
297
+ consoleDrawText(11, 9, "HI");
298
+ fmt_u16(hiscore);
299
+ consoleDrawText(14, 9, tbuf);
300
+ state = ST_TITLE;
301
+ }
302
+
303
+ /* ── GAME LOGIC (clay) — start a turn / a run ── */
304
+ static void begin_turn(void) {
305
+ px = 24;
306
+ py_q44 = (u16)(GROUND_TOP - 8) << 4;
307
+ vy_q44 = 0;
308
+ on_ground = 1;
309
+ scroll_x = 0;
310
+ dist_sub = 0;
311
+ coin_x[0] = 88; coin_y[0] = 184;
312
+ coin_x[1] = 152; coin_y[1] = 160;
313
+ coin_x[2] = 216; coin_y[2] = 128;
314
+ spike_x[0] = 136; spike_active[0] = 1; /* both anchored on ground at */
315
+ spike_x[1] = 224; spike_active[1] = 1; /* scroll 0 — see ground_row */
316
+ turn_pause = 48; /* "P1/P2 GO" breather */
317
+ prev_padP = 0xFFFF; /* swallow held buttons across the turn change —
318
+ * without this the A that picked 1P on the title
319
+ * instantly jumps (classic edge-detect reuse bug) */
320
+ draw_hud();
321
+ if (two_player)
322
+ consoleDrawText(11, 4, cur_player ? "PLAYER 2 GO" : "PLAYER 1 GO");
323
+ }
324
+
325
+ static void start_game(u8 players) {
326
+ u8 i;
327
+ two_player = players;
328
+ cur_player = 0;
329
+ p_score[0] = p_score[1] = 0;
330
+ p_lives[0] = START_LIVES;
331
+ p_lives[1] = players ? START_LIVES : 0;
332
+ clear_rows(0, 27);
333
+ draw_hud_labels();
334
+ for (i = 0; i < 24; i += 4) oamSetEx(i, OBJ_SMALL, OBJ_SHOW);
335
+ begin_turn();
336
+ if (sound_ok) sfx_play(1); /* start blip */
337
+ state = ST_PLAY;
338
+ }
339
+
340
+ static void game_over(void) {
341
+ u16 best = p_score[0];
342
+ if (two_player && p_score[1] > best) best = p_score[1];
343
+ if (best > hiscore) { hiscore = best; hi_save(hiscore); }
344
+ bgSetDisable(1); /* clean card: sky backdrop + text only */
345
+ hide_actors();
346
+ clear_rows(0, 27);
347
+ consoleDrawText(11, 6, "GAME OVER");
348
+ consoleDrawText(9, 10, "P1");
349
+ fmt_u16(p_score[0]); consoleDrawText(15, 10, tbuf);
350
+ if (two_player) {
351
+ consoleDrawText(9, 12, "P2");
352
+ fmt_u16(p_score[1]); consoleDrawText(15, 12, tbuf);
353
+ }
354
+ consoleDrawText(9, 15, "HI");
355
+ fmt_u16(hiscore); consoleDrawText(15, 15, tbuf);
356
+ consoleDrawText(9, 20, "START - TITLE");
357
+ if (sound_ok) sfx_play(2); /* game-over thud */
358
+ state = ST_OVER;
359
+ }
360
+
361
+ /* ── GAME LOGIC (clay) — death + alternating-turn handoff ── */
362
+ static void kill_player(void) {
363
+ u8 other;
364
+ if (sound_ok) sfx_play(2);
365
+ if (p_lives[cur_player] > 0) --p_lives[cur_player];
366
+ if (two_player) {
367
+ other = cur_player ^ 1;
368
+ if (p_lives[other] > 0) cur_player = other; /* swap turns */
369
+ else if (p_lives[cur_player] == 0) { game_over(); return; }
370
+ } else if (p_lives[0] == 0) {
371
+ game_over();
372
+ return;
373
+ }
374
+ begin_turn();
375
+ }
376
+
377
+ /* ── GAME LOGIC (clay) — landing probe against the column map ──────────────
378
+ * One-way platforms, classic style: only catch the player while FALLING
379
+ * through a narrow window at the surface. The window is 6 px tall —
380
+ * top-1 (the standing snap parks feet at top, and gravity's sub-pixel
381
+ * trickle doesn't move the integer Y every frame; without the -1 slack the
382
+ * player "stands" with on_ground=0 most frames, so jumps only register on
383
+ * lucky frames and the idle/jump sprite flickers) through top+4 (so a
384
+ * 5 px/frame terminal-velocity fall can't step over it). */
385
+ static u8 land_top(u8 c, u8 feet) {
386
+ u8 r, top;
387
+ r = plat_row[c];
388
+ if (r) {
389
+ top = (u8)(r << 3);
390
+ if ((u8)(feet + 1) >= top && feet <= (u8)(top + 4)) return top;
391
+ }
392
+ r = ground_row[c];
393
+ if (r != NO_GROUND) {
394
+ top = (u8)(r << 3);
395
+ if ((u8)(feet + 1) >= top && feet <= (u8)(top + 4)) return top;
396
+ }
397
+ return 0;
398
+ }
399
+
400
+ /* ── GAME LOGIC (clay) — one frame of gameplay ─────────────────────────────── */
401
+ static void play_update(void) {
402
+ u16 pad;
403
+ u8 i, delta, y8, feet, c0, c1, top, killed;
404
+
405
+ if (turn_pause) { /* freeze gameplay, keep the frame honest */
406
+ --turn_pause;
407
+ if (turn_pause == 0) clear_row(4); /* drop the "Pn GO" banner */
408
+ stage_actors();
409
+ return;
410
+ }
411
+
412
+ /* Input — the CURRENT player's controller (alternating turns: P2 is on
413
+ * controller 2 — padsCurrent(1); that one index IS the 2P wiring). Past
414
+ * SCROLL_WALL the world scrolls instead of the player (the camera never
415
+ * scrolls back — the classic one-way camera). */
416
+ pad = padsCurrent(cur_player);
417
+ delta = 0;
418
+ if (pad & KEY_RIGHT) {
419
+ if (px < SCROLL_WALL) px += MOVE_SPEED;
420
+ else { scroll_x += MOVE_SPEED; delta = MOVE_SPEED; }
421
+ }
422
+ if ((pad & KEY_LEFT) && px > 8) px -= MOVE_SPEED;
423
+ if ((pad & (KEY_B | KEY_A)) && !(prev_padP & (KEY_B | KEY_A)) && on_ground) {
424
+ vy_q44 = JUMP_VEL_Q44;
425
+ on_ground = 0;
426
+ if (sound_ok) sfx_play(1); /* jump blip */
427
+ }
428
+ prev_padP = pad;
429
+
430
+ /* World objects drift left as the level scrolls (world-anchored). */
431
+ if (delta) {
432
+ dist_sub += delta;
433
+ if (dist_sub >= 64) { /* distance pay */
434
+ dist_sub -= 64;
435
+ ++p_score[cur_player];
436
+ draw_hud();
437
+ }
438
+ for (i = 0; i < NUM_COINS; i++) {
439
+ if (coin_x[i] < 16 + delta) respawn_coin(i);
440
+ else coin_x[i] -= delta;
441
+ }
442
+ for (i = 0; i < NUM_SPIKES; i++) {
443
+ if (!spike_active[i]) continue;
444
+ if (spike_x[i] < 16 + delta) spike_active[i] = 0;
445
+ else spike_x[i] -= delta;
126
446
  }
447
+ }
448
+ for (i = 0; i < NUM_SPIKES; i++)
449
+ if (!spike_active[i]) try_spawn_spike(i);
450
+
451
+ /* Physics: gravity + sub-pixel Y. */
452
+ if (vy_q44 < MAX_VY_Q44) vy_q44 += GRAVITY_Q44;
453
+ py_q44 += vy_q44;
454
+ y8 = (u8)(py_q44 >> 4);
455
+
456
+ /* Fell into a pit (below the screen) → lose the turn. */
457
+ if (y8 >= 232) {
458
+ kill_player();
459
+ return;
460
+ }
127
461
 
128
- oamSet(0, 32, 100, 3, 0, 0, 0, 0);
129
- /* Screen ON first, THEN sound. sfx_init() must run AFTER setScreenOn()
130
- * (snes_sfx.h:63) if the SPC stalls before the screen is on you get a
131
- * black/forced-blank screen forever. */
132
- setScreenOn();
133
- sfx_init();
134
-
135
- while (1) {
136
- pad = padsCurrent(0);
137
- vx = 0;
138
- if (pad & KEY_LEFT) vx = -MOVE_SPEED;
139
- if (pad & KEY_RIGHT) vx = MOVE_SPEED;
140
-
141
- ipx = px >> 4;
142
- ipy = py >> 4;
143
- grounded = on_platform(ipx, ipy);
144
- if ((pad & KEY_A) && !(prev & KEY_A) && grounded) { vy = JUMP_VEL; sfx_play(1); }
145
- prev = pad;
146
-
147
- vy += GRAVITY;
148
- if (vy > MAX_FALL) vy = MAX_FALL;
149
- if (grounded && vy > 0) vy = 0;
150
-
151
- px += vx;
152
- if (px < 0) px = 0;
153
- if (px > (WORLD_W - 8) << 4) px = (WORLD_W - 8) << 4;
154
-
155
- np = py + vy;
156
- npy = np >> 4;
157
- if (vy > 0) {
158
- for (i = 0; i < N_PLATFORMS; i++) {
159
- p = &platforms[i];
160
- if (ipy + 8 <= p->y && npy + 8 >= p->y
161
- && ipx + 8 > p->x && ipx < p->x + p->w) {
162
- py = (p->y - 8) << 4;
163
- vy = 0;
164
- goto done;
165
- }
166
- }
167
- }
168
- py = np;
169
- if (py > 224 << 4) { py = 0; vy = 0; }
170
- done: ;
171
-
172
- /* Camera follows the player, centered, clamped to the world.
173
- * bgSetScroll moves the BG in hardware; the player draws in
174
- * SCREEN space (worldX - camX). */
175
- camX = (px >> 4) - (SCREEN_W / 2 - 4);
176
- if (camX < 0) camX = 0;
177
- if (camX > WORLD_W - SCREEN_W) camX = WORLD_W - SCREEN_W;
178
- bgSetScroll(0, camX, 0);
179
-
180
- oamSetXY(0, (px >> 4) - camX, py >> 4);
181
- oamUpdate();
182
- WaitForVBlank();
183
- consoleVblank();
462
+ /* Landing probe the two level columns under the player's feet. */
463
+ if (vy_q44 >= 0) {
464
+ feet = (u8)(y8 + 8);
465
+ c0 = (u8)(px + scroll_x) >> 3;
466
+ c1 = (u8)(px + scroll_x + 7) >> 3;
467
+ top = land_top(c0, feet);
468
+ if (top == 0) top = land_top(c1, feet);
469
+ if (top) {
470
+ py_q44 = (u16)(top - 8) << 4;
471
+ vy_q44 = 0;
472
+ on_ground = 1;
473
+ } else {
474
+ on_ground = 0; /* walked off */
184
475
  }
185
- return 0;
476
+ }
477
+
478
+ /* Coins (collect) + spikes (death). */
479
+ for (i = 0; i < NUM_COINS; i++) {
480
+ if (dist8(coin_x[i], px) < 8 && dist8(coin_y[i], y8) < 8) {
481
+ p_score[cur_player] += 10;
482
+ if (sound_ok) sfx_play(1); /* coin ping */
483
+ draw_hud();
484
+ respawn_coin(i);
485
+ }
486
+ }
487
+ killed = 0;
488
+ for (i = 0; i < NUM_SPIKES; i++) {
489
+ if (!spike_active[i]) continue;
490
+ if (dist8(spike_x[i], px) < 7 && dist8(SPIKE_Y, y8) < 7) {
491
+ killed = 1;
492
+ break;
493
+ }
494
+ }
495
+ if (killed) { kill_player(); return; }
496
+
497
+ stage_actors();
498
+ }
499
+
500
+ /* Headless-test telemetry — written once per frame into the bank-$7E telem
501
+ * block (data.asm). A test harness finds it by scanning WRAM for the
502
+ * "CG"+0xBD signature, then plays the game from real state instead of
503
+ * parsing pixels. spike_x is always even (spawns at 248, drifts by 2), so
504
+ * its bit 0 carries the active flag. Costs ~20 byte-writes; delete freely. */
505
+ static void telem_update(void) {
506
+ telem[0] = 'C'; telem[1] = 'G'; telem[2] = 0xBD;
507
+ telem[3] = state;
508
+ telem[4] = (u8)((sound_ok << 7) | (two_player << 1) | cur_player);
509
+ telem[5] = p_lives[0];
510
+ telem[6] = p_lives[1];
511
+ telem[7] = px;
512
+ telem[8] = (u8)(py_q44 >> 4);
513
+ telem[9] = scroll_x;
514
+ telem[10] = on_ground;
515
+ telem[11] = (u8)p_score[0]; telem[12] = (u8)(p_score[0] >> 8);
516
+ telem[13] = (u8)p_score[1]; telem[14] = (u8)(p_score[1] >> 8);
517
+ telem[15] = turn_pause;
518
+ telem[16] = (u8)(spike_x[0] | spike_active[0]);
519
+ telem[17] = (u8)(spike_x[1] | spike_active[1]);
520
+ }
521
+
522
+ int main(void) {
523
+ u16 pad;
524
+
525
+ /* ── HARDWARE IDIOM (load-bearing — see TROUBLESHOOTING) ──
526
+ * Init order: console text pointers FIRST, then mode, then VRAM uploads
527
+ * while the screen is still off (forced blank — VRAM DMA during active
528
+ * display is lost or corrupts). consoleInitText DMAs the font but does
529
+ * NOT set the PPU BG base registers — bgSetGfxPtr/bgSetMapPtr must agree
530
+ * with the console pointers or text renders as garbage tiles. */
531
+ consoleSetTextMapPtr(0x6800);
532
+ consoleSetTextGfxPtr(0x3000);
533
+ consoleSetTextOffset(0x0000);
534
+ consoleInitText(0, 16 * 2, &tilfont, &palfont);
535
+ setMode(BG_MODE1, 0);
536
+ bgSetGfxPtr(0, 0x3000);
537
+ bgSetMapPtr(0, 0x6800, SC_32x32);
538
+
539
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
540
+ * The two-layer split (see the header essay): BG0 = HUD/text, scroll
541
+ * pinned at (0,0); BG1 = the level, moved by one bgSetScroll per frame.
542
+ * palbg loads into CGRAM block 0 AFTER the font palette and is a superset
543
+ * of it (colour 1 stays white) — so HUD ink and level tiles share the
544
+ * block without fighting. BG2 carries power-on garbage in Mode 1 — keep
545
+ * it disabled. */
546
+ paint_level();
547
+ bgInitTileSet(1, (u8 *)&tilbg, (u8 *)&palbg, 0, 4 * 32, 32, BG_16COLORS, 0x2000);
548
+ bgInitMapSet(1, (u8 *)bg_map, sizeof(bg_map), SC_32x32, 0x4000);
549
+ bgSetEnable(1);
550
+ bgSetDisable(2);
551
+
552
+ /* OBJ: 8x8 sprites (player, coins, spikes) at VRAM $0000. */
553
+ oamInitGfxSet(&tilsprite, 4 * 32, &palsprite, 32, 0, 0x0000, OBJ_SIZE8_L16);
554
+
555
+ setScreenOn();
556
+
557
+ /* ── HARDWARE IDIOM (load-bearing) — sfx_init AFTER setScreenOn, and CHECK
558
+ * the return: a wedged SPC700 must not take the video down with it. ── */
559
+ sound_ok = (sfx_init() == 0);
560
+ /* ── HARDWARE IDIOM (load-bearing) — one frame between init and the first
561
+ * command. sfx_init returns the instant the SPC echoes the jump command,
562
+ * but the driver then spends ~50 port writes initialising the DSP BEFORE
563
+ * it seeds its command edge-detector from $2140. Send a command in that
564
+ * window and the seed swallows it — music silently never starts. A
565
+ * WaitForVBlank is thousands of SPC cycles — deterministic cure. ── */
566
+ WaitForVBlank();
567
+ if (sound_ok) sfx_music_play();
568
+
569
+ hiscore = hi_load(); /* battery SRAM — 0 on first boot */
570
+ prev_pad0 = prev_padP = 0;
571
+ title_enter();
572
+
573
+ while (1) {
574
+ pad = padsCurrent(0);
575
+
576
+ if (state == ST_TITLE) {
577
+ /* attract: the level drifts by under the title — the scroll register
578
+ * demo, and the first thing a fork breaks if the layers get swapped */
579
+ attract_sub ^= 1;
580
+ if (attract_sub) scroll_x++;
581
+ if ((pad & KEY_A && !(prev_pad0 & KEY_A)) ||
582
+ (pad & KEY_START && !(prev_pad0 & KEY_START))) {
583
+ start_game(0);
584
+ } else if (pad & KEY_B && !(prev_pad0 & KEY_B)) {
585
+ start_game(1);
586
+ }
587
+ } else if (state == ST_PLAY) {
588
+ play_update();
589
+ } else { /* ST_OVER */
590
+ if ((pad & (KEY_START | KEY_A)) && !(prev_pad0 & (KEY_START | KEY_A)))
591
+ title_enter();
592
+ }
593
+ prev_pad0 = pad;
594
+ telem_update();
595
+ oamUpdate();
596
+
597
+ WaitForVBlank();
598
+ /* ── HARDWARE IDIOM (load-bearing) — scroll + text commits in vblank.
599
+ * bgSetScroll writes the BG1 scroll registers directly; mid-frame the
600
+ * beam would render the top of the frame with the old value and the
601
+ * bottom with the new (a shear). BG0 gets NO scroll write, ever —
602
+ * that's the whole fixed-HUD trick. ── */
603
+ bgSetScroll(1, scroll_x, 0);
604
+ consoleVblank();
605
+ }
606
+ return 0;
186
607
  }