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,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,148 +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();
227
- msx_music_tick();
228
-
229
- dir = msx_read_joystick(1);
230
- if (dir == STICK_CENTER) dir = msx_read_joystick(0);
231
- trig = (uint8_t)(gttrig(1) || gttrig(2));
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
+ }
232
501
 
233
- if ((dir == STICK_LEFT || dir == STICK_UL || dir == STICK_DL)
234
- && player.x > 4) player.x = (uint8_t)(player.x - 2);
235
- if ((dir == STICK_RIGHT || dir == STICK_UR || dir == STICK_DR)
236
- && player.x < 248) player.x = (uint8_t)(player.x + 2);
237
- if ((dir == STICK_UP || dir == STICK_UL || dir == STICK_UR)
238
- && player.y > 16) player.y = (uint8_t)(player.y - 2);
239
- if ((dir == STICK_DOWN || dir == STICK_DL || dir == STICK_DR)
240
- && 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
+ }
241
513
 
242
- if (trig && !prev_trig) fire();
243
- prev_trig = trig;
514
+ /* ── ST_PLAY GAME LOGIC (clay) from here down ─────────────────── */
515
+ update_ship(0);
516
+ if (two_player) update_ship(1);
244
517
 
245
- /* advance bullets */
246
518
  for (i = 0; i < MAX_BULLETS; i++) {
247
519
  if (!bullets[i].alive) continue;
248
- if (bullets[i].y < 18) { bullets[i].alive = 0; continue; }
520
+ if (bullets[i].y < 14) { bullets[i].alive = 0; continue; }
249
521
  bullets[i].y = (uint8_t)(bullets[i].y - 4);
250
522
  }
251
- /* advance enemies */
252
523
  for (i = 0; i < MAX_ENEMIES; i++) {
253
524
  if (!enemies[i].alive) continue;
254
525
  enemies[i].y = (uint8_t)(enemies[i].y + 1);
255
526
  if (enemies[i].y >= 184) enemies[i].alive = 0;
256
527
  }
257
- spawn_timer = (uint8_t)(spawn_timer + 1);
258
- if (spawn_timer >= 28) { spawn_timer = 0; spawn(); }
528
+ spawn_timer++;
529
+ if (spawn_timer >= 28) { spawn_timer = 0; spawn_enemy(); }
259
530
 
260
- /* bullet vs enemy */
531
+ /* bullets enemies */
261
532
  for (i = 0; i < MAX_BULLETS; i++) {
262
533
  if (!bullets[i].alive) continue;
263
534
  for (j = 0; j < MAX_ENEMIES; j++) {
@@ -265,25 +536,36 @@ void main(void) {
265
536
  if (aabb(&bullets[i], &enemies[j])) {
266
537
  bullets[i].alive = 0;
267
538
  enemies[j].alive = 0;
268
- if (score < 999) { score++; draw_score(); }
269
- msx_psg_tone(1, 0x400, 14);
270
- 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;
271
543
  break;
272
544
  }
273
545
  }
274
546
  }
275
547
 
276
- if (blip) { blip--; if (!blip) { msx_psg_off(0); msx_psg_off(1); } }
277
-
278
- /* push sprites */
279
- msx_set_sprite(0, player.x, player.y, PAT_SHIP, COL_SHIP);
280
- for (i = 0; i < MAX_BULLETS; i++)
281
- msx_set_sprite((uint8_t)(1 + i), bullets[i].x,
282
- bullets[i].alive ? bullets[i].y : SPRITE_END_Y,
283
- PAT_BULLET, COL_BULLET);
284
- for (i = 0; i < MAX_ENEMIES; i++)
285
- msx_set_sprite((uint8_t)(5 + i), enemies[i].x,
286
- enemies[i].alive ? enemies[i].y : SPRITE_END_Y,
287
- 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();
288
570
  }
289
571
  }