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,114 +1,289 @@
1
- /* ── shmup/main.c — MSX vertical-shooter scaffold (screen 2) ─────────
1
+ /* ── shmup/main.c — MSX vertical shooter (complete example game) ─────────────
2
2
  *
3
- * Mirrors the NES/Genesis/SNES/GB/SMS shmup scaffolds, translated to the
4
- * MSX VDP via the romdev MSX helper lib (msx_hw.h + msx_vdp.c).
3
+ * A COMPLETE, working game — title screen, 1P and 2P co-op modes (MSX has two
4
+ * joystick ports), lives, score + session hi-score, music + SFX on the
5
+ * AY-3-8910 PSG, and the MSX's signature trick: SCREEN-2 PER-ROW COLOR
6
+ * (the color table gives every 8x1 pixel row of every tile its own
7
+ * foreground/background pair, in THREE independent screen thirds — used here
8
+ * for a depth-banded starfield, a HUD band in its own colors, and an 8-color
9
+ * gradient inside a single tile).
5
10
  *
6
- * Player ship (sprite plane 0) + 4 bullet slots (planes 1-4) + 4 enemy
7
- * slots (planes 5-8), a wave spawner, and AABB collisions. Score is drawn
8
- * as on-screen tiles ("SCORE 000") along the top row. The whole 32x24
9
- * screen-2 name table is painted with a banded starfield so the display is
10
- * clearly space, not a flat backdrop.
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 MSX footgun; reshape
14
+ * your gameplay around it (see TROUBLESHOOTING before changing).
15
+ * GAME LOGIC (clay) — enemy patterns, scoring, tuning, art: reshape freely.
11
16
  *
12
- * Controls: joystick LEFT/RIGHT/UP/DOWN moves the ship, trigger A fires.
13
- * We read joystick PORT 1 (and the keyboard cursor on stick 0 as a
14
- * fallback), and GTTRIG for the fire button.
17
+ * What depends on what:
18
+ * msx_hw.h / msx_vdp.c VDP + PSG + joystick helpers (direct Z80 ports;
19
+ * the PSG functions carry a DI/EI guard against the BIOS KEYINT race —
20
+ * read msx_vdp.c before adding your own PSG pokes).
21
+ * msx_crt0.s — the $4000 "AB" cart header + static-init copy. Load-bearing;
22
+ * INIT must never return, so main() ends in for(;;).
15
23
  *
16
- * Cartridge rule: INIT must never return, so main() ends in for(;;).
24
+ * Controls: joystick PORT 1 (or keyboard cursors+space) flies ship 1,
25
+ * trigger A fires. PORT 2 flies ship 2 in co-op. On the title screen
26
+ * trigger A starts 1P; trigger B (or player 2's trigger) starts 2P co-op.
17
27
  *
18
- * Hardware path (all through the MSX helper lib):
19
- * - msx_set_screen2() screen 2 (GRAPHIC II), 256x192, display ON
20
- * - msx_vram_write() upload tile font + sprite patterns to VRAM
21
- * - msx_set_sprite() position the ship/bullets/enemies each frame
22
- * - msx_read_joystick() BIOS GTSTCK 0=center, 1-8 = direction CW
23
- * - msx_psg_tone/off() fire blip + explosion noise
24
- * - vsync() one game step per VDP frame (interrupt-free)
28
+ * Hi-score honesty: the bundled bluemsx core build does NOT expose a battery
29
+ * save path (retro_get_memory(SAVE_RAM) is unimplemented for MSX carts), so
30
+ * the hi-score lives in plain RAM: it survives title↔game cycles but NOT a
31
+ * power cycle. Never fake persistence — if you need real saves, that's a
32
+ * future core round (SRAM-mapper cart types like ASCII8-SRAM exist; the
33
+ * core just doesn't surface their RAM yet).
25
34
  */
26
35
  #include "msx_hw.h"
27
36
 
28
- /* ── interrupt-free vblank sync (poll VDP status S#0 bit 7) ────────────── */
37
+ /* The title screen renders this examples({op:'fork'}) stamps your game's
38
+ * name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
39
+ #define GAME_TITLE "NEBULA WARDEN"
40
+
41
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
42
+ * Interrupt-free vblank sync: poll VDP status S#0 bit 7 (port 0x99). Reading
43
+ * the port ALSO clears the flag, so one read per frame = one game step per
44
+ * frame. We deliberately do NOT use the BIOS JIFFY counter here: this poll
45
+ * works even with interrupts masked, and never depends on the BIOS ISR
46
+ * keeping pace. (The BIOS KEYINT also reads S#0 — on rare frames it eats the
47
+ * flag first and this loop just waits for the next one; a one-frame hiccup,
48
+ * never a hang.) */
29
49
  __sfr __at 0x99 VDPSTATUS;
30
50
  static void vsync(void) {
31
- (void)VDPSTATUS;
51
+ (void)VDPSTATUS; /* throw away a possibly-stale flag */
32
52
  while (!(VDPSTATUS & 0x80)) {
33
53
  }
34
54
  }
35
55
 
36
- /* fire-button trigger uses the BIOS GTTRIG wrapper (gttrig) from msx_hw.h:
37
- * gttrig(0)=space/any, gttrig(1)/gttrig(2)=port-A/B triggers. */
38
-
39
- #define MAX_BULLETS 4
40
- #define MAX_ENEMIES 4
41
-
42
- /* ── tile font: SPACE, S C O R E, digits, plus a couple starfield tiles ── */
43
- #define T_SPACE 0
44
- #define T_S 1
45
- #define T_C 2
46
- #define T_O 3
47
- #define T_R 4
48
- #define T_E 5
49
- #define T_0 6 /* digits 0..9 are consecutive: T_0 + n */
50
- #define T_DEEP 16 /* solid deep-space band (dark blue) */
51
- #define T_BAND 17 /* solid lighter space band (medium blue) */
52
- #define T_STAR1 18 /* deep-space cell with a faint star */
53
- #define T_STAR2 19 /* lighter-band cell with a bright star */
54
-
55
- static const uint8_t font[20][8] = {
56
- /* 0 SPACE */ {0,0,0,0,0,0,0,0},
57
- /* 1 S */ {0x7C,0xC0,0xC0,0x78,0x0C,0x0C,0xF8,0x00},
58
- /* 2 C */ {0x7C,0xC6,0xC0,0xC0,0xC0,0xC6,0x7C,0x00},
59
- /* 3 O */ {0x7C,0xC6,0xC6,0xC6,0xC6,0xC6,0x7C,0x00},
60
- /* 4 R */ {0xFC,0xC6,0xC6,0xFC,0xD8,0xCC,0xC6,0x00},
61
- /* 5 E */ {0xFE,0xC0,0xC0,0xF8,0xC0,0xC0,0xFE,0x00},
62
- /* 6 0 */ {0x7C,0xCE,0xDE,0xF6,0xE6,0xC6,0x7C,0x00},
63
- /* 7 1 */ {0x18,0x38,0x18,0x18,0x18,0x18,0x7E,0x00},
64
- /* 8 2 */ {0x7C,0xC6,0x06,0x1C,0x70,0xC0,0xFE,0x00},
65
- /* 9 3 */ {0x7C,0xC6,0x06,0x3C,0x06,0xC6,0x7C,0x00},
66
- /* 10 4 */ {0x1C,0x3C,0x6C,0xCC,0xFE,0x0C,0x0C,0x00},
67
- /* 11 5 */ {0xFE,0xC0,0xFC,0x06,0x06,0xC6,0x7C,0x00},
68
- /* 12 6 */ {0x3C,0x60,0xC0,0xFC,0xC6,0xC6,0x7C,0x00},
69
- /* 13 7 */ {0xFE,0x06,0x0C,0x18,0x30,0x30,0x30,0x00},
70
- /* 14 8 */ {0x7C,0xC6,0xC6,0x7C,0xC6,0xC6,0x7C,0x00},
71
- /* 15 9 */ {0x7C,0xC6,0xC6,0x7E,0x06,0x0C,0x78,0x00},
72
- /* 16 DEEP (solid fill) */ {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF},
73
- /* 17 BAND (solid fill) */ {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF},
74
- /* 18 STAR1 (deep + dot) */ {0xFF,0xFF,0xFF,0xEF,0xFF,0xFF,0xFF,0xFF},
75
- /* 19 STAR2 (band + dot) */ {0xFF,0xEF,0xEF,0x83,0xEF,0xEF,0xFF,0xFF}
56
+ /* ── GAME LOGIC (clay reshape freely) ──────────────────────────────────────
57
+ * Tile font: index 0 = space, 1-26 = A-Z, 27-36 = 0-9, 37 = dash, then the
58
+ * starfield tiles. One 8x8 pattern = 8 bytes, one bit per pixel; set bits
59
+ * draw in the tile's FOREGROUND color, clear bits in its BACKGROUND color
60
+ * (both come from the screen-2 color table — see the idiom block below). */
61
+ #define T_SPACE 0
62
+ #define T_A 1 /* 'A'..'Z' = T_A + (c - 'A') */
63
+ #define T_0 27 /* '0'..'9' = T_0 + (c - '0') */
64
+ #define T_DASH 37
65
+ #define T_FIELD 38 /* empty space cell (pattern all 0 = all bg) */
66
+ #define T_STAR1 39 /* faint single-pixel star */
67
+ #define T_STAR2 40 /* bright cross star */
68
+ #define T_NEBULA 41 /* the per-8x1-row gradient tile (see below) */
69
+ #define NUM_TILES 42
70
+
71
+ static const uint8_t font[NUM_TILES][8] = {
72
+ /* SPACE */ {0,0,0,0,0,0,0,0},
73
+ /* 1 A */ {0x38,0x6C,0xC6,0xC6,0xFE,0xC6,0xC6,0x00},
74
+ /* 2 B */ {0xFC,0xC6,0xC6,0xFC,0xC6,0xC6,0xFC,0x00},
75
+ /* 3 C */ {0x7C,0xC6,0xC0,0xC0,0xC0,0xC6,0x7C,0x00},
76
+ /* 4 D */ {0xF8,0xCC,0xC6,0xC6,0xC6,0xCC,0xF8,0x00},
77
+ /* 5 E */ {0xFE,0xC0,0xC0,0xF8,0xC0,0xC0,0xFE,0x00},
78
+ /* 6 F */ {0xFE,0xC0,0xC0,0xF8,0xC0,0xC0,0xC0,0x00},
79
+ /* 7 G */ {0x7C,0xC6,0xC0,0xCE,0xC6,0xC6,0x7C,0x00},
80
+ /* 8 H */ {0xC6,0xC6,0xC6,0xFE,0xC6,0xC6,0xC6,0x00},
81
+ /* 9 I */ {0x7E,0x18,0x18,0x18,0x18,0x18,0x7E,0x00},
82
+ /* 10 J */ {0x1E,0x06,0x06,0x06,0xC6,0xC6,0x7C,0x00},
83
+ /* 11 K */ {0xC6,0xCC,0xD8,0xF0,0xD8,0xCC,0xC6,0x00},
84
+ /* 12 L */ {0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xFE,0x00},
85
+ /* 13 M */ {0xC6,0xEE,0xFE,0xD6,0xC6,0xC6,0xC6,0x00},
86
+ /* 14 N */ {0xC6,0xE6,0xF6,0xDE,0xCE,0xC6,0xC6,0x00},
87
+ /* 15 O */ {0x7C,0xC6,0xC6,0xC6,0xC6,0xC6,0x7C,0x00},
88
+ /* 16 P */ {0xFC,0xC6,0xC6,0xFC,0xC0,0xC0,0xC0,0x00},
89
+ /* 17 Q */ {0x7C,0xC6,0xC6,0xC6,0xD6,0xCC,0x76,0x00},
90
+ /* 18 R */ {0xFC,0xC6,0xC6,0xFC,0xD8,0xCC,0xC6,0x00},
91
+ /* 19 S */ {0x7C,0xC0,0xC0,0x78,0x0C,0x0C,0xF8,0x00},
92
+ /* 20 T */ {0x7E,0x18,0x18,0x18,0x18,0x18,0x18,0x00},
93
+ /* 21 U */ {0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0x7C,0x00},
94
+ /* 22 V */ {0xC6,0xC6,0xC6,0xC6,0x6C,0x38,0x10,0x00},
95
+ /* 23 W */ {0xC6,0xC6,0xC6,0xD6,0xFE,0xEE,0xC6,0x00},
96
+ /* 24 X */ {0xC6,0x6C,0x38,0x10,0x38,0x6C,0xC6,0x00},
97
+ /* 25 Y */ {0x66,0x66,0x66,0x3C,0x18,0x18,0x18,0x00},
98
+ /* 26 Z */ {0xFE,0x0C,0x18,0x30,0x60,0xC0,0xFE,0x00},
99
+ /* 27 0 */ {0x7C,0xCE,0xDE,0xF6,0xE6,0xC6,0x7C,0x00},
100
+ /* 28 1 */ {0x18,0x38,0x18,0x18,0x18,0x18,0x7E,0x00},
101
+ /* 29 2 */ {0x7C,0xC6,0x06,0x1C,0x70,0xC0,0xFE,0x00},
102
+ /* 30 3 */ {0x7C,0xC6,0x06,0x3C,0x06,0xC6,0x7C,0x00},
103
+ /* 31 4 */ {0x1C,0x3C,0x6C,0xCC,0xFE,0x0C,0x0C,0x00},
104
+ /* 32 5 */ {0xFE,0xC0,0xFC,0x06,0x06,0xC6,0x7C,0x00},
105
+ /* 33 6 */ {0x3C,0x60,0xC0,0xFC,0xC6,0xC6,0x7C,0x00},
106
+ /* 34 7 */ {0xFE,0x06,0x0C,0x18,0x30,0x30,0x30,0x00},
107
+ /* 35 8 */ {0x7C,0xC6,0xC6,0x7C,0xC6,0xC6,0x7C,0x00},
108
+ /* 36 9 */ {0x7C,0xC6,0xC6,0x7E,0x06,0x0C,0x78,0x00},
109
+ /* 37 - */ {0x00,0x00,0x00,0x7E,0x00,0x00,0x00,0x00},
110
+ /* 38 FIELD (all bg) */ {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
111
+ /* 39 STAR1 (one pixel) */ {0x00,0x00,0x00,0x10,0x00,0x00,0x00,0x00},
112
+ /* 40 STAR2 (cross) */ {0x00,0x10,0x10,0x7C,0x10,0x10,0x00,0x00},
113
+ /* 41 NEBULA (solid fg — its COLOR bytes paint the gradient) */
114
+ {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF},
76
115
  };
77
116
 
78
- /* colour bytes per glyph: high nibble fg, low nibble bg.
79
- * TMS9918 fixed palette: 1=black, 4=dark blue, 5=med blue, 14=grey, 15=white.
80
- * The solid space tiles fill entirely with their fg colour. The star tiles
81
- * use a white star (fg) over the band colour (bg). */
82
- #define COL_TEXT 0xF1 /* white text on black */
83
- #define COL_DEEP 0x44 /* solid dark-blue deep space */
84
- #define COL_BAND 0x55 /* solid medium-blue band */
85
- #define COL_STAR1 0xF4 /* white star pixel over dark-blue field */
86
- #define COL_STAR2 0xF5 /* white star over medium-blue band */
87
-
88
- /* ── sprite patterns (8x8) ─────────────────────────────────────────────── */
117
+ /* ── HARDWARE IDIOM (load-bearing reshape gameplay around this; see TROUBLESHOOTING) ──
118
+ * SCREEN-2 PER-ROW COLOR the MSX's signature background trick.
119
+ *
120
+ * Screen 2 (GRAPHIC II) is NOT "one color byte per tile" like most consoles:
121
+ *
122
+ * 1. The 256x192 screen is THREE INDEPENDENT THIRDS of 8 rows each
123
+ * (name-table rows 0-7, 8-15, 16-23). Each third has its OWN 2KB
124
+ * pattern table slice and its OWN 2KB color table slice:
125
+ * patterns: VRAM_PATTERN + third*0x800, colors: VRAM_COLOR + third*0x800
126
+ * The SAME tile index can look completely different in each third —
127
+ * we exploit exactly that for the depth-banded starfield below.
128
+ *
129
+ * 2. Within a tile, the color table holds EIGHT bytes — one per 8x1 pixel
130
+ * row — each packing (foreground<<4)|background from the fixed TMS9918
131
+ * palette. So one tile can carry an 8-color vertical gradient
132
+ * (T_NEBULA's whole "glow horizon" is a single tile, colors only).
133
+ *
134
+ * Requires: the screen-2 table layout set by msx_set_screen2() (R3=0xFF,
135
+ * R4=0x03 — the "thirds" configuration; some games set R3/R4 so all thirds
136
+ * SHARE one slice, which saves VRAM but kills this trick), and pattern +
137
+ * color uploads to EVERY third a tile is used in. Upload with the display
138
+ * idle or accept a partial frame: tile N's slot is pattern[N*8] / color[N*8].
139
+ *
140
+ * Depth scheme taught here (TMS9918 fixed palette: 1 black, 4 dark blue,
141
+ * 5 light blue, 7 cyan, 11 light yellow, 14 gray, 15 white):
142
+ * third 0 (top) = deep space: black field, gray stars — far, dim
143
+ * third 1 (middle) = mid space: dark blue, yellow stars — closer
144
+ * third 2 (bottom) = near space: light blue, white stars — closest
145
+ * ...and the HUD text band (row 0, third 0) gets its OWN colors, distinct
146
+ * from everything below it, without costing any extra tiles. */
147
+ static const uint8_t col_text[3] = { 0xF4, 0xB4, 0x15 }; /* HUD white-on-blue; title yellow-on-blue; bottom black-on-lightblue */
148
+ static const uint8_t col_field[3] = { 0x11, 0x44, 0x55 }; /* the three depth bands (bg shows: pattern is all 0) */
149
+ static const uint8_t col_star1[3] = { 0xE1, 0xB4, 0xF5 }; /* faint star per band: gray/yellow/white on its band bg */
150
+ static const uint8_t col_star2[3] = { 0xF1, 0xF4, 0xB5 }; /* bright star per band */
151
+ /* T_NEBULA: 8 DIFFERENT color bytes inside ONE tile = an 8-pixel-row glow
152
+ * gradient (dark blue → light blue → cyan → white and back down to black).
153
+ * The pattern is solid 0xFF so only the fg nibbles show. */
154
+ static const uint8_t col_nebula[8] = { 0x45,0x55,0x75,0xF5,0x75,0x55,0x45,0x15 };
155
+
156
+ static void load_tiles(void) {
157
+ uint8_t third, i;
158
+ uint16_t patbase, colbase;
159
+ for (third = 0; third < 3; third++) {
160
+ patbase = (uint16_t)(VRAM_PATTERN + ((uint16_t)third << 11));
161
+ colbase = (uint16_t)(VRAM_COLOR + ((uint16_t)third << 11));
162
+ for (i = 0; i < NUM_TILES; i++) {
163
+ uint8_t col;
164
+ /* pattern bits are the same in every third — only COLOR varies */
165
+ msx_vram_write((uint16_t)(patbase + ((uint16_t)i << 3)), font[i], 8);
166
+ if (i == T_NEBULA) { /* the one per-pixel-row gradient */
167
+ msx_vram_write((uint16_t)(colbase + ((uint16_t)i << 3)), col_nebula, 8);
168
+ continue;
169
+ }
170
+ if (i == T_FIELD) col = col_field[third];
171
+ else if (i == T_STAR1) col = col_star1[third];
172
+ else if (i == T_STAR2) col = col_star2[third];
173
+ else col = col_text[third];
174
+ msx_fill_vram((uint16_t)(colbase + ((uint16_t)i << 3)), 8, col);
175
+ }
176
+ }
177
+ }
178
+
179
+ /* ── GAME LOGIC (clay — reshape freely) — name-table drawing helpers ────────
180
+ * Screen 2 VRAM writes are safe at any point in the frame at C speed: the
181
+ * TMS9918 needs ~29 Z80 cycles between VRAM accesses during active display,
182
+ * and SDCC-compiled loops are slower than that. (Hand-tuned asm OTIR bursts
183
+ * are the thing that outruns the VDP — see TROUBLESHOOTING.) */
184
+ static void put_tile(uint8_t col, uint8_t row, uint8_t tile) {
185
+ msx_vram_write((uint16_t)(VRAM_NAME + (uint16_t)row * 32 + col), &tile, 1);
186
+ }
187
+
188
+ static void draw_text(uint8_t col, uint8_t row, const char *s) {
189
+ uint8_t buf[32];
190
+ uint8_t n = 0;
191
+ while (*s && n < 32) {
192
+ char c = *s++;
193
+ if (c >= 'A' && c <= 'Z') buf[n] = (uint8_t)(T_A + c - 'A');
194
+ else if (c >= '0' && c <= '9') buf[n] = (uint8_t)(T_0 + c - '0');
195
+ else if (c == '-') buf[n] = T_DASH;
196
+ else buf[n] = T_SPACE;
197
+ n++;
198
+ }
199
+ msx_vram_write((uint16_t)(VRAM_NAME + (uint16_t)row * 32 + col), buf, n);
200
+ }
201
+
202
+ static void draw_num4(uint8_t col, uint8_t row, uint16_t v) {
203
+ uint8_t buf[4];
204
+ buf[0] = (uint8_t)(T_0 + (v / 1000) % 10);
205
+ buf[1] = (uint8_t)(T_0 + (v / 100) % 10);
206
+ buf[2] = (uint8_t)(T_0 + (v / 10) % 10);
207
+ buf[3] = (uint8_t)(T_0 + v % 10);
208
+ msx_vram_write((uint16_t)(VRAM_NAME + (uint16_t)row * 32 + col), buf, 4);
209
+ }
210
+
211
+ /* Paint the full 32x24 starfield. The TILE INDICES are the same everywhere —
212
+ * the three depth bands come ENTIRELY from the per-third color tables (the
213
+ * screen-2 idiom above). Row 0 is the HUD band; row 23 is the one-tile
214
+ * nebula gradient. */
215
+ static void paint_starfield(void) {
216
+ uint8_t row, col, h;
217
+ uint8_t buf[32];
218
+ msx_fill_vram(VRAM_NAME, 32, T_SPACE); /* row 0: HUD band */
219
+ for (row = 1; row < 23; row++) {
220
+ for (col = 0; col < 32; col++) {
221
+ h = (uint8_t)((row * 7 + col * 5) & 15); /* cheap static hash */
222
+ if (h == 0) buf[col] = T_STAR1;
223
+ else if (h == 8) buf[col] = T_STAR2;
224
+ else buf[col] = T_FIELD;
225
+ }
226
+ msx_vram_write((uint16_t)(VRAM_NAME + (uint16_t)row * 32), buf, 32);
227
+ }
228
+ msx_fill_vram((uint16_t)(VRAM_NAME + 23u * 32), 32, T_NEBULA);
229
+ }
230
+
231
+ /* ── GAME LOGIC (clay — reshape freely) — sprites ────────────────────────────
232
+ * 8x8 one-color hardware sprites. Plane layout (lower plane = on top):
233
+ * 0-1 ships, 2-7 bullets, 8-12 enemies. */
89
234
  static const uint8_t spr_ship[8] = {0x18,0x3C,0x7E,0x7E,0xFF,0xFF,0xDB,0x81};
90
235
  static const uint8_t spr_bullet[8] = {0x18,0x3C,0x3C,0x3C,0x3C,0x3C,0x18,0x00};
91
236
  static const uint8_t spr_enemy[8] = {0x81,0x42,0x24,0x18,0x18,0x24,0x42,0x81};
92
-
93
237
  #define PAT_SHIP 0
94
238
  #define PAT_BULLET 1
95
239
  #define PAT_ENEMY 2
96
-
97
- /* TMS9918 fixed sprite palette: 15=white, 10=yellow, 9=red(ish) */
98
- #define COL_SHIP 15
99
- #define COL_BULLET 10
100
- #define COL_ENEMY 9
101
-
240
+ #define COL_SHIP1 15 /* white */
241
+ #define COL_SHIP2 3 /* light green */
242
+ #define COL_BULLET 11 /* light yellow*/
243
+ #define COL_ENEMY 9 /* light red */
244
+
245
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
246
+ * Sprite limits + the Y=208 terminator:
247
+ * - A sprite Y of 0xD0 (208) tells the TMS9918 to STOP SCANNING the
248
+ * attribute table — every higher-numbered plane vanishes, not just that
249
+ * one. (msx_clear_sprites parks ALL planes at 0xD0, which is fine at the
250
+ * END of the list.) To hide ONE sprite mid-list, park it OFFSCREEN at
251
+ * PARK_Y (192 = first line below the display) — never at 0xD0.
252
+ * (On MSX2's V9938 sprite mode 2 the terminator moves to 0xD8 and 0xD0
253
+ * is "just offscreen" — code that leans on that breaks on MSX1.)
254
+ * - Per scanline the TMS9918 draws only 4 sprites (V9938: 8); the rest drop
255
+ * out for that line. Pools here are sized so a worst-case pileup is rare;
256
+ * if you raise MAX_* counts, expect flicker on crowded rows. */
257
+ #define PARK_Y 192
258
+
259
+ #define MAX_BULLETS 6
260
+ #define MAX_ENEMIES 5
261
+
262
+ /* ── GAME LOGIC (clay — reshape freely) — game state ─────────────────────── */
102
263
  typedef struct { uint8_t x, y, alive; } Obj;
103
264
 
104
- static Obj player;
265
+ static Obj ships[2];
266
+ static uint8_t fire_cd[2];
105
267
  static Obj bullets[MAX_BULLETS];
106
268
  static Obj enemies[MAX_ENEMIES];
269
+ static uint8_t two_player; /* mode chosen on the title screen */
270
+ static uint8_t lives; /* shared pool in co-op (arcade style) */
107
271
  static uint16_t score;
272
+ static uint16_t hiscore; /* SESSION-ONLY: plain RAM. The bundled
273
+ * bluemsx build exposes no SAVE_RAM region,
274
+ * so there is nothing battery-backed to
275
+ * write — survives title↔game cycles, not a
276
+ * power cycle (honest, not faked). */
108
277
  static uint8_t spawn_timer;
109
278
  static uint16_t rng;
110
- static uint8_t blip;
111
279
 
280
+ #define ST_TITLE 0
281
+ #define ST_PLAY 1
282
+ #define ST_OVER 2
283
+ static uint8_t state;
284
+ static uint8_t prev_t1, prev_t2; /* trigger edge detection across states */
285
+
286
+ /* xorshift16 PRNG — a few dozen cycles, no tables. */
112
287
  static uint8_t next_rand(void) {
113
288
  rng ^= (uint16_t)(rng << 7);
114
289
  rng ^= (uint16_t)(rng >> 9);
@@ -116,147 +291,244 @@ static uint8_t next_rand(void) {
116
291
  return (uint8_t)(rng & 0xFF);
117
292
  }
118
293
 
119
- /* upload the glyph patterns into ALL THREE screen-2 pattern thirds */
120
- static void load_font(void) {
121
- uint8_t third, i;
122
- uint16_t patbase, colbase;
123
- for (third = 0; third < 3; third++) {
124
- patbase = (uint16_t)(VRAM_PATTERN + ((uint16_t)third << 11));
125
- colbase = (uint16_t)(VRAM_COLOR + ((uint16_t)third << 11));
126
- for (i = 0; i < 20; i++) {
127
- uint8_t col = COL_TEXT;
128
- if (i == T_DEEP) col = COL_DEEP;
129
- else if (i == T_BAND) col = COL_BAND;
130
- else if (i == T_STAR1) col = COL_STAR1;
131
- else if (i == T_STAR2) col = COL_STAR2;
132
- msx_vram_write((uint16_t)(patbase + ((uint16_t)i << 3)), font[i], 8);
133
- msx_fill_vram((uint16_t)(colbase + ((uint16_t)i << 3)), 8, col);
134
- }
294
+ /* ── GAME LOGIC (clay reshape freely) music + SFX on the AY-3-8910 ──────
295
+ * Channel plan: A = fire blip, B = explosion noise, C = music. The PSG has 3
296
+ * tone channels + ONE shared noise generator, mixed per-channel in reg 7.
297
+ * All register traffic goes through msx_psg_tone/noise/off — they wrap the
298
+ * PSGADDR/PSGWRITE pair in DI/EI because the BIOS KEYINT ISR clobbers the
299
+ * PSG address latch every frame (the bug that once silenced every MSX
300
+ * scaffold see msx_vdp.c).
301
+ *
302
+ * The tune: one period entry per half-beat, 0 = rest. AY period =
303
+ * 1789773 / (16 * freq) e.g. A4 (440Hz) -> 254. Ticked once per frame from
304
+ * the main loop; a note advances every 7 frames (~8.5 notes/sec). The lib's
305
+ * built-in demo loop (msx_music_tick) also uses channel C, so we switch it
306
+ * OFF in main() and run this table instead — edit THIS table to rescore. */
307
+ static const uint16_t tune[32] = {
308
+ 254, 0, 285, 254, 339, 0, 285, 339, /* A4 G4 A4 E4 G4 E4 (A-minor riff) */
309
+ 427, 0, 339, 427, 508, 0, 0, 0, /* C4 E4 C4 A3 rest */
310
+ 380, 0, 427, 380, 320, 0, 380, 427, /* D4 C4 D4 F4 D4 C4 */
311
+ 508, 0, 427, 339, 285, 0, 0, 0, /* A3 C4 E4 G4 rest */
312
+ };
313
+ static uint8_t music_step, music_timer;
314
+ static uint8_t sfx_fire_t, sfx_boom_t; /* frames left on each SFX channel */
315
+
316
+ static void music_tick(void) {
317
+ if (music_timer == 0) {
318
+ uint16_t p = tune[music_step & 31];
319
+ if (p) msx_psg_tone(2, p, 10);
320
+ else msx_psg_off(2);
321
+ music_step++;
135
322
  }
323
+ music_timer++;
324
+ if (music_timer >= 7) music_timer = 0;
136
325
  }
137
326
 
138
- static void put_tile(uint8_t col, uint8_t row, uint8_t tile) {
139
- msx_vram_write((uint16_t)(VRAM_NAME + (uint16_t)row * 32 + col), &tile, 1);
327
+ static void sfx_tick(void) {
328
+ if (sfx_fire_t) { sfx_fire_t--; if (!sfx_fire_t) msx_psg_off(0); }
329
+ if (sfx_boom_t) { sfx_boom_t--; if (!sfx_boom_t) msx_psg_noise(1, 0, 0); }
140
330
  }
141
331
 
142
- /* paint a banded starfield across the WHOLE 32x24 name table: alternating
143
- * 3-row bands of deep/medium blue (so neither colour dominates) with a sparse
144
- * scattering of white stars. Row 0 stays blank for the SCORE line. */
145
- static void draw_starfield(void) {
146
- uint8_t row, col, band, tile, h;
147
- msx_fill_vram(VRAM_NAME, 32, T_SPACE); /* blank row 0 (HUD) */
148
- for (row = 1; row < 24; row++) {
149
- band = (uint8_t)(((row / 3) & 1)); /* alternate every 3 rows */
150
- for (col = 0; col < 32; col++) {
151
- h = (uint8_t)((row * 7 + col * 5) & 15);
152
- if (h == 0) tile = band ? T_STAR2 : T_STAR1; /* a star */
153
- else tile = band ? T_BAND : T_DEEP; /* solid band */
154
- put_tile(col, row, tile);
155
- }
156
- }
332
+ /* ── GAME LOGIC (clay reshape freely) HUD ──────────────────────────────
333
+ * Row 0 = the HUD band (third 0's text colors make it a distinct strip).
334
+ * SC=score, HI=hi-score, SH=ships(lives). */
335
+ static void draw_hud_labels(void) {
336
+ draw_text(1, 0, "SC");
337
+ draw_text(11, 0, "HI");
338
+ draw_text(21, 0, "SH");
339
+ }
340
+ static void draw_score(void) { draw_num4(4, 0, score); }
341
+ static void draw_hi(void) { draw_num4(14, 0, hiscore); }
342
+ static void draw_lives(void) { put_tile(24, 0, (uint8_t)(T_0 + lives)); }
343
+
344
+ /* ── GAME LOGIC (clay reshape freely) — screens ──────────────────────────
345
+ * Title rows land in third 1 (yellow-on-blue) and third 2 (the HI line) —
346
+ * the same glyph tiles as the HUD, recolored for free by the thirds idiom. */
347
+ static void paint_title(void) {
348
+ uint8_t len = 0, col;
349
+ const char *p = GAME_TITLE;
350
+ while (*p++) len++;
351
+ col = (uint8_t)((32 - len) / 2);
352
+ paint_starfield();
353
+ draw_text(col, 8, GAME_TITLE);
354
+ draw_text(7, 12, "1P START - FIRE A");
355
+ draw_text(7, 14, "2P CO-OP - FIRE B");
356
+ draw_text(12, 19, "HI 0000"); /* the space blanks the cell between */
357
+ draw_num4(15, 19, hiscore);
157
358
  }
158
359
 
159
- static void draw_label(void) {
160
- put_tile(1, 0, T_S); put_tile(2, 0, T_C); put_tile(3, 0, T_O);
161
- put_tile(4, 0, T_R); put_tile(5, 0, T_E);
360
+ static void start_game(uint8_t players) {
361
+ uint8_t i;
362
+ two_player = players;
363
+ for (i = 0; i < MAX_BULLETS; i++) bullets[i].alive = 0;
364
+ for (i = 0; i < MAX_ENEMIES; i++) enemies[i].alive = 0;
365
+ ships[0].x = two_player ? 96 : 120; ships[0].y = 160; ships[0].alive = 1;
366
+ ships[1].x = 144; ships[1].y = 160; ships[1].alive = two_player;
367
+ fire_cd[0] = fire_cd[1] = 0;
368
+ lives = 3;
369
+ score = 0;
370
+ spawn_timer = 0;
371
+ paint_starfield();
372
+ draw_hud_labels();
373
+ draw_score(); draw_hi(); draw_lives();
374
+ state = ST_PLAY;
162
375
  }
163
376
 
164
- static void draw_score(void) {
165
- uint16_t s = score;
166
- put_tile(7, 0, (uint8_t)(T_0 + (s / 100) % 10));
167
- put_tile(8, 0, (uint8_t)(T_0 + (s / 10) % 10));
168
- put_tile(9, 0, (uint8_t)(T_0 + s % 10));
377
+ static void game_over(void) {
378
+ if (score > hiscore) { hiscore = score; draw_hi(); }
379
+ draw_text(11, 11, "GAME OVER");
380
+ draw_text(8, 13, "FIRE FOR TITLE");
381
+ prev_t1 = prev_t2 = 1; /* swallow a fire button still held from play */
382
+ state = ST_OVER;
169
383
  }
170
384
 
385
+ /* ── GAME LOGIC (clay — reshape freely) — combat ─────────────────────────── */
171
386
  static uint8_t aabb(Obj *a, Obj *b) {
172
387
  return a->x < b->x + 8 && a->x + 8 > b->x
173
388
  && a->y < b->y + 8 && a->y + 8 > b->y;
174
389
  }
175
390
 
176
- static void fire(void) {
391
+ static void fire(uint8_t p) {
177
392
  uint8_t i;
178
393
  for (i = 0; i < MAX_BULLETS; i++) {
179
394
  if (!bullets[i].alive) {
180
- bullets[i].x = player.x;
181
- bullets[i].y = (uint8_t)(player.y - 8);
395
+ bullets[i].x = ships[p].x;
396
+ bullets[i].y = (uint8_t)(ships[p].y - 8);
182
397
  bullets[i].alive = 1;
183
- msx_psg_tone(0, 0x100, 12);
184
- blip = 4;
398
+ msx_psg_tone(0, 0x080, 12); /* high blip on channel A */
399
+ sfx_fire_t = 4;
185
400
  return;
186
401
  }
187
402
  }
188
403
  }
189
404
 
190
- static void spawn(void) {
405
+ static void spawn_enemy(void) {
191
406
  uint8_t i;
192
407
  for (i = 0; i < MAX_ENEMIES; i++) {
193
408
  if (!enemies[i].alive) {
194
409
  enemies[i].x = (uint8_t)(8 + (next_rand() % 232));
195
- enemies[i].y = 16;
410
+ enemies[i].y = 12;
196
411
  enemies[i].alive = 1;
197
412
  return;
198
413
  }
199
414
  }
200
415
  }
201
416
 
417
+ /* Per-player input + movement. Stick mapping: P1 = joystick port 1 with the
418
+ * keyboard cursors (stick 0) as fallback; P2 = joystick port 2. GTSTCK
419
+ * returns 0=center then 1-8 clockwise from up. */
420
+ static void update_ship(uint8_t p) {
421
+ uint8_t dir, trig;
422
+ if (!ships[p].alive) return;
423
+ if (p == 0) {
424
+ dir = msx_read_joystick(1);
425
+ if (dir == STICK_CENTER) dir = msx_read_joystick(0);
426
+ trig = (uint8_t)(gttrig(1) || gttrig(0)); /* port-1 trig A or SPACE */
427
+ } else {
428
+ dir = msx_read_joystick(2);
429
+ trig = gttrig(2); /* port-2 trigger A */
430
+ }
431
+ if ((dir == STICK_LEFT || dir == STICK_UL || dir == STICK_DL)
432
+ && ships[p].x > 4) ships[p].x = (uint8_t)(ships[p].x - 2);
433
+ if ((dir == STICK_RIGHT || dir == STICK_UR || dir == STICK_DR)
434
+ && ships[p].x < 248) ships[p].x = (uint8_t)(ships[p].x + 2);
435
+ if ((dir == STICK_UP || dir == STICK_UL || dir == STICK_UR)
436
+ && ships[p].y > 24) ships[p].y = (uint8_t)(ships[p].y - 2);
437
+ if ((dir == STICK_DOWN || dir == STICK_DL || dir == STICK_DR)
438
+ && ships[p].y < 168) ships[p].y = (uint8_t)(ships[p].y + 2);
439
+ if (trig && fire_cd[p] == 0) { fire(p); fire_cd[p] = 10; }
440
+ if (fire_cd[p]) fire_cd[p]--;
441
+ }
442
+
443
+ /* Push every object to its sprite plane. Dead objects park at PARK_Y
444
+ * (offscreen), NEVER 0xD0 — see the sprite idiom block above. */
445
+ static void push_sprites(void) {
446
+ uint8_t i;
447
+ msx_set_sprite(0, ships[0].x, ships[0].alive ? ships[0].y : PARK_Y,
448
+ PAT_SHIP, COL_SHIP1);
449
+ msx_set_sprite(1, ships[1].x, ships[1].alive ? ships[1].y : PARK_Y,
450
+ PAT_SHIP, COL_SHIP2);
451
+ for (i = 0; i < MAX_BULLETS; i++)
452
+ msx_set_sprite((uint8_t)(2 + i), bullets[i].x,
453
+ bullets[i].alive ? bullets[i].y : PARK_Y,
454
+ PAT_BULLET, COL_BULLET);
455
+ for (i = 0; i < MAX_ENEMIES; i++)
456
+ msx_set_sprite((uint8_t)(8 + i), enemies[i].x,
457
+ enemies[i].alive ? enemies[i].y : PARK_Y,
458
+ PAT_ENEMY, COL_ENEMY);
459
+ }
460
+
202
461
  void main(void) {
203
- uint8_t i, j, dir, trig, prev_trig;
462
+ uint8_t i, j, t1, t2;
204
463
 
464
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
465
+ * Init order: set the video mode FIRST (INIGRP also clears VRAM — any
466
+ * upload done before it is wiped), then tiles, then sprites. The crt0's
467
+ * INIT contract means main() must NEVER return — the BIOS has nothing
468
+ * sane to fall back to — hence the for(;;) below. */
205
469
  msx_set_screen2();
206
470
  msx_clear_sprites();
207
- load_font();
208
- draw_starfield();
209
- draw_label();
210
-
471
+ load_tiles();
211
472
  msx_vram_write((uint16_t)(VRAM_SPRPAT + PAT_SHIP * 8), spr_ship, 8);
212
473
  msx_vram_write((uint16_t)(VRAM_SPRPAT + PAT_BULLET * 8), spr_bullet, 8);
213
474
  msx_vram_write((uint16_t)(VRAM_SPRPAT + PAT_ENEMY * 8), spr_enemy, 8);
214
475
 
215
- player.x = 120; player.y = 160; player.alive = 1;
216
- for (i = 0; i < MAX_BULLETS; i++) bullets[i].alive = 0;
217
- for (i = 0; i < MAX_ENEMIES; i++) enemies[i].alive = 0;
218
- score = 0;
219
- spawn_timer = 0;
476
+ msx_music(0); /* the lib's demo loop also owns channel C —
477
+ * hand the channel to OUR tune table instead */
478
+ hiscore = 0; /* session hi-score (no SAVE_RAM on this core) */
220
479
  rng = 0xACE1;
221
- blip = 0;
222
- prev_trig = 0;
223
- draw_score();
480
+ music_step = music_timer = 0;
481
+ sfx_fire_t = sfx_boom_t = 0;
482
+ prev_t1 = prev_t2 = 1; /* swallow a held trigger across state changes */
483
+ state = ST_TITLE;
484
+ paint_title();
224
485
 
225
486
  for (;;) {
226
487
  vsync();
488
+ music_tick();
489
+ sfx_tick();
490
+
491
+ if (state == ST_TITLE) {
492
+ /* ── GAME LOGIC (clay) — title: trig A = 1P; trig B (port-1
493
+ * button 2, gttrig 3) OR player 2's trigger = 2P co-op. */
494
+ t1 = (uint8_t)(gttrig(1) || gttrig(0));
495
+ t2 = (uint8_t)(gttrig(3) || gttrig(2));
496
+ if (t2 && !prev_t2) start_game(1);
497
+ else if (t1 && !prev_t1) start_game(0);
498
+ prev_t1 = t1; prev_t2 = t2;
499
+ continue;
500
+ }
227
501
 
228
- dir = msx_read_joystick(1);
229
- if (dir == STICK_CENTER) dir = msx_read_joystick(0);
230
- trig = (uint8_t)(gttrig(1) || gttrig(2));
231
-
232
- if ((dir == STICK_LEFT || dir == STICK_UL || dir == STICK_DL)
233
- && player.x > 4) player.x = (uint8_t)(player.x - 2);
234
- if ((dir == STICK_RIGHT || dir == STICK_UR || dir == STICK_DR)
235
- && player.x < 248) player.x = (uint8_t)(player.x + 2);
236
- if ((dir == STICK_UP || dir == STICK_UL || dir == STICK_UR)
237
- && player.y > 16) player.y = (uint8_t)(player.y - 2);
238
- if ((dir == STICK_DOWN || dir == STICK_DL || dir == STICK_DR)
239
- && player.y < 168) player.y = (uint8_t)(player.y + 2);
502
+ if (state == ST_OVER) {
503
+ /* Freeze the final frame; any fire button returns to the title. */
504
+ t1 = (uint8_t)(gttrig(1) || gttrig(0) || gttrig(2));
505
+ if (t1 && !prev_t1) {
506
+ state = ST_TITLE;
507
+ msx_clear_sprites();
508
+ paint_title();
509
+ }
510
+ prev_t1 = t1; prev_t2 = t1;
511
+ continue;
512
+ }
240
513
 
241
- if (trig && !prev_trig) fire();
242
- prev_trig = trig;
514
+ /* ── ST_PLAY GAME LOGIC (clay) from here down ─────────────────── */
515
+ update_ship(0);
516
+ if (two_player) update_ship(1);
243
517
 
244
- /* advance bullets */
245
518
  for (i = 0; i < MAX_BULLETS; i++) {
246
519
  if (!bullets[i].alive) continue;
247
- if (bullets[i].y < 18) { bullets[i].alive = 0; continue; }
520
+ if (bullets[i].y < 14) { bullets[i].alive = 0; continue; }
248
521
  bullets[i].y = (uint8_t)(bullets[i].y - 4);
249
522
  }
250
- /* advance enemies */
251
523
  for (i = 0; i < MAX_ENEMIES; i++) {
252
524
  if (!enemies[i].alive) continue;
253
525
  enemies[i].y = (uint8_t)(enemies[i].y + 1);
254
526
  if (enemies[i].y >= 184) enemies[i].alive = 0;
255
527
  }
256
- spawn_timer = (uint8_t)(spawn_timer + 1);
257
- if (spawn_timer >= 28) { spawn_timer = 0; spawn(); }
528
+ spawn_timer++;
529
+ if (spawn_timer >= 28) { spawn_timer = 0; spawn_enemy(); }
258
530
 
259
- /* bullet vs enemy */
531
+ /* bullets enemies */
260
532
  for (i = 0; i < MAX_BULLETS; i++) {
261
533
  if (!bullets[i].alive) continue;
262
534
  for (j = 0; j < MAX_ENEMIES; j++) {
@@ -264,25 +536,36 @@ void main(void) {
264
536
  if (aabb(&bullets[i], &enemies[j])) {
265
537
  bullets[i].alive = 0;
266
538
  enemies[j].alive = 0;
267
- if (score < 999) { score++; draw_score(); }
268
- msx_psg_tone(1, 0x400, 14);
269
- blip = 6;
539
+ if (score < 9999) score++;
540
+ draw_score();
541
+ msx_psg_noise(1, 12, 13); /* explosion: shared noise */
542
+ sfx_boom_t = 8;
270
543
  break;
271
544
  }
272
545
  }
273
546
  }
274
547
 
275
- if (blip) { blip--; if (!blip) { msx_psg_off(0); msx_psg_off(1); } }
276
-
277
- /* push sprites */
278
- msx_set_sprite(0, player.x, player.y, PAT_SHIP, COL_SHIP);
279
- for (i = 0; i < MAX_BULLETS; i++)
280
- msx_set_sprite((uint8_t)(1 + i), bullets[i].x,
281
- bullets[i].alive ? bullets[i].y : SPRITE_END_Y,
282
- PAT_BULLET, COL_BULLET);
283
- for (i = 0; i < MAX_ENEMIES; i++)
284
- msx_set_sprite((uint8_t)(5 + i), enemies[i].x,
285
- enemies[i].alive ? enemies[i].y : SPRITE_END_Y,
286
- PAT_ENEMY, COL_ENEMY);
548
+ /* enemies ships: shared life pool (arcade co-op) */
549
+ for (j = 0; j < MAX_ENEMIES; j++) {
550
+ if (!enemies[j].alive) continue;
551
+ for (i = 0; i < 2; i++) {
552
+ if (!ships[i].alive) continue;
553
+ if (aabb(&enemies[j], &ships[i])) {
554
+ enemies[j].alive = 0;
555
+ msx_psg_noise(1, 28, 14); /* deeper, longer crunch */
556
+ sfx_boom_t = 20;
557
+ if (lives) lives--;
558
+ draw_lives();
559
+ if (lives == 0) {
560
+ game_over();
561
+ } else {
562
+ ships[i].y = 160; /* respawn knockback */
563
+ ships[i].x = i ? 144 : (two_player ? 96 : 120);
564
+ }
565
+ }
566
+ }
567
+ }
568
+
569
+ push_sprites();
287
570
  }
288
571
  }