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,169 +1,335 @@
1
- /*
2
- * PC Engine "shmup" — a vertical shoot-'em-up scaffold.
1
+ /* ── main.c — PC Engine vertical shooter (complete example game) ─────────────
3
2
  *
4
- * Fly a ship around the bottom of the screen with the d-pad and fire upward
5
- * with button I. Enemies spawn at the top in waves and drift down; a bullet
6
- * that overlaps an enemy destroys it and scores 10. The HUD shows the score
7
- * with background digit tiles. A scrolling starfield BG keeps the screen full
8
- * (so it clears the verify gate and the sprites read clearly).
3
+ * A COMPLETE, working game title screen, lives, score + persistent hi-score
4
+ * (in-session a bare HuCard can't save), music + SFX, enemy waves, and the PCE's signature
5
+ * hardware feature: LARGE MULTI-SPRITE OBJECTS. The boss is a 64x32 war
6
+ * machine built from two 32x32 HuC6270 sprites that move as one unit — the
7
+ * kind of object that needs 8+ hardware sprites on the NES and exactly TWO
8
+ * SATB entries here.
9
9
  *
10
- * Mirrors the NES/Genesis/SNES/GB/SMS shmup scaffolds, translated to the PCE
11
- * helper API:
12
- * - object pools (player + bullets + enemies) updated each frame
13
- * - AABB collision
14
- * - a wave spawner on a frame counter
15
- * - 64-sprite shadow SATB + satb_dma()
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 PCE footgun; reshape
13
+ * your gameplay around it (see TROUBLESHOOTING before changing).
14
+ * GAME LOGIC (clay) enemy patterns, scoring, tuning, art: reshape freely.
16
15
  *
17
- * PCE notes (see pce_hw.h / MENTAL_MODEL.md):
18
- * - disp_enable() turns on BG + sprites AND the VBlank IRQ so waitvsync()
19
- * actually returns (without the IRQ bit the loop spins forever).
20
- * - .bss must be non-empty; pce_video.c's _pce_keep[] covers that, and we
21
- * touch _pce_keep[0] for clarity.
22
- * - sprites get the SPBG-front bit from set_sprite(), so they draw over the
23
- * opaque starfield BG.
16
+ * What depends on what:
17
+ * pce_hw.h / pce_video.c / pce_input.c / pce_sound.c the helper lib
18
+ * (VDC/VCE/PSG register dances + joypad). The HARDWARE IDIOM markers in
19
+ * pce_video.c say which parts are load-bearing.
20
+ * cc65's pce crt0 + pce.lib are auto-linked; the 'rom32k' linker preset
21
+ * (applied automatically to example projects) gives a 32KB HuCard.
24
22
  *
25
- * cc65 is C89 declare locals at the top of a block.
23
+ * SINGLE PLAYER, honestly: the stock PC Engine has ONE controller port;
24
+ * 2P needs a TurboTap. The geargrafx core implements the TurboTap but ships
25
+ * with it disabled (a core option, no headless override today), so a second
26
+ * pad's input never reaches the game — verified by scanning all 5 multitap
27
+ * slots while driving port-1 input (force-enabling geargrafx_turbotap DOES
28
+ * deliver pad 2, so a future host core-option round can unlock PCE 2P).
29
+ * This game is therefore 1P by design.
30
+ *
31
+ * Frame budget (NTSC, 60fps, 7.16MHz 65C02-class CPU): the whole update
32
+ * (6 bullets × 6 enemies + 6 × boss AABB ≈ 42 checks worst case, plus a
33
+ * 256-word SATB copy in vblank) fits comfortably inside one frame.
26
34
  */
27
35
  #include <pce.h>
28
36
  #include "pce_hw.h"
29
37
 
30
- /* ---- VRAM layout (word addresses) --------------------------------------- */
31
- #define BAT_VRAM 0x0000 /* 32x32 background map */
32
- #define FONT_VRAM 0x1000 /* digit/glyph tiles (8x8, 16 words each) */
33
- #define STAR0_VRAM 0x1400 /* BG tile: empty space (solid colour 1) */
34
- #define STAR1_VRAM 0x1410 /* BG tile: space band (solid colour 2) */
35
- #define STAR2_VRAM 0x1420 /* BG tile: space + a star pixel */
36
- #define SHIP_VRAM 0x1800 /* 16x16 player ship */
37
- #define BULLET_VRAM 0x1840 /* 16x16 bullet */
38
- #define ENEMY_VRAM 0x1880 /* 16x16 enemy */
38
+ /* The title screen renders this — examples({op:'fork'}) stamps your game's
39
+ * name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
40
+ #define GAME_TITLE "ZENITH BARRAGE"
41
+
42
+ /* ── HARDWARE IDIOM (load-bearing reshape gameplay around this; see TROUBLESHOOTING) ──
43
+ * VRAM map (WORD addresses — the VDC is a 16-bit-word machine; a tile is 16
44
+ * words, a 16x16 sprite cell is 64). Sprites and BG tiles share one 64KB
45
+ * VRAM, so lay it out ONCE and keep the SATB out of pattern space:
46
+ * $0000 BAT (32x32 background map — matches vdc_init's VDC_MWR setting)
47
+ * $1000 font glyphs (38 tiles: blank, 0-9, A-Z, dash)
48
+ * $1400 starfield BG tiles
49
+ * $1800 16x16 sprite cells: ship, bullet, enemy
50
+ * $1900 BOSS pattern cells — 4-ALIGNED cell index (see the boss idiom)
51
+ * $7F00 shadow SATB destination (satb_dma copies it here, VDC reads it) */
52
+ #define BAT_VRAM 0x0000
53
+ #define FONT_VRAM 0x1000
54
+ #define STAR0_VRAM 0x1400 /* deep-space band tile (solid colour 1) */
55
+ #define STAR1_VRAM 0x1410 /* lighter band tile (solid colour 2) */
56
+ #define STAR2_VRAM 0x1420 /* band tile + a twinkling star pixel */
57
+ #define SHIP_VRAM 0x1800
58
+ #define BULLET_VRAM 0x1840
59
+ #define ENEMY_VRAM 0x1880
60
+ #define BOSS_VRAM 0x1900 /* 8 cells: left half TL,TR,BL,BR + right half */
39
61
 
40
62
  #define BAT_ENTRY(pal, vram) ((u16)(((pal) << 12) | ((vram) >> 4)))
41
63
 
64
+ /* Sprite pattern codes = VRAM >> 6 (the 16x16 cell index). */
65
+ #define SHIP_PAT (SHIP_VRAM >> 6)
66
+ #define BULLET_PAT (BULLET_VRAM >> 6)
67
+ #define ENEMY_PAT (ENEMY_VRAM >> 6)
68
+ #define BOSSL_PAT (BOSS_VRAM >> 6) /* 0x64 — multiple of 4 */
69
+ #define BOSSR_PAT ((BOSS_VRAM >> 6) + 4) /* 0x68 — multiple of 4 */
70
+
71
+ /* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
72
+ * Object pools — fixed slots, no allocation. SATB slot plan (slot order is
73
+ * also priority: LOWER slot wins overlaps on the HuC6270):
74
+ * 0 player ship
75
+ * 1-6 bullets
76
+ * 7-12 enemies (waves + boss drones share the pool)
77
+ * 14,15 the boss's two 32x32 halves
78
+ * Everything else stays parked off-screen. */
42
79
  #define MAX_BULLETS 6
43
80
  #define MAX_ENEMIES 6
81
+ #define SLOT_SHIP 0
82
+ #define SLOT_BULLET 1
83
+ #define SLOT_ENEMY 7
84
+ #define SLOT_BOSS_L 14
85
+ #define SLOT_BOSS_R 15
44
86
 
45
- /* ---- 5x7 glyph font (digits + a few letters for the HUD) ----------------- */
46
- #define G_BLANK 0
47
- #define G_0 1 /* digits 0..9 -> tiles 1..10 */
48
- #define G_S 11
49
- #define G_C 12
50
- #define G_O 13
51
- #define G_R 14
52
- #define G_E 15
53
- #define NUM_GLYPHS 16
87
+ #define PAL_SHIP 0
88
+ #define PAL_BULLET 1
89
+ #define PAL_ENEMY 2
90
+ #define PAL_BOSS 3
54
91
 
55
- static const u8 FONT5x7[NUM_GLYPHS][7] = {
56
- /* BLANK */ {0,0,0,0,0,0,0},
57
- /* 0 */ {0x0E,0x11,0x13,0x15,0x19,0x11,0x0E},
58
- /* 1 */ {0x04,0x0C,0x04,0x04,0x04,0x04,0x0E},
59
- /* 2 */ {0x0E,0x11,0x01,0x02,0x04,0x08,0x1F},
60
- /* 3 */ {0x1F,0x02,0x04,0x02,0x01,0x11,0x0E},
61
- /* 4 */ {0x02,0x06,0x0A,0x12,0x1F,0x02,0x02},
62
- /* 5 */ {0x1F,0x10,0x1E,0x01,0x01,0x11,0x0E},
63
- /* 6 */ {0x06,0x08,0x10,0x1E,0x11,0x11,0x0E},
64
- /* 7 */ {0x1F,0x01,0x02,0x04,0x08,0x08,0x08},
65
- /* 8 */ {0x0E,0x11,0x11,0x0E,0x11,0x11,0x0E},
66
- /* 9 */ {0x0E,0x11,0x11,0x0F,0x01,0x02,0x0C},
67
- /* S */ {0x0F,0x10,0x10,0x0E,0x01,0x01,0x1E},
68
- /* C */ {0x0E,0x11,0x10,0x10,0x10,0x11,0x0E},
69
- /* O */ {0x0E,0x11,0x11,0x11,0x11,0x11,0x0E},
70
- /* R */ {0x1E,0x11,0x11,0x1E,0x14,0x12,0x11},
71
- /* E */ {0x1F,0x10,0x10,0x1E,0x10,0x10,0x1F},
72
- };
92
+ #define START_LIVES 3
93
+ #define SHIP_MIN_Y 80 /* keeps the ship out of the boss's altitude */
94
+ #define OFFSCREEN_Y 0x1F0 /* park unused sprites below the display */
73
95
 
74
- /* ---- game state --------------------------------------------------------- */
75
96
  typedef struct { u16 x, y; u8 alive; } Obj;
76
97
 
77
98
  static Obj player;
78
99
  static Obj bullets[MAX_BULLETS];
79
100
  static Obj enemies[MAX_ENEMIES];
80
- static u16 score;
101
+ static u16 score, hiscore;
102
+ static u8 lives;
103
+ static u8 level; /* +1 per boss defeated — feeds speed/HP */
104
+ static u8 kills; /* kills since the last boss — triggers the next */
105
+ static u8 invuln; /* post-hit mercy frames (ship flickers) */
106
+ static u8 fire_cd;
81
107
  static u8 spawn_timer;
108
+ static u8 twinkle_timer;
82
109
  static u16 rng;
83
110
  static u8 pad, prev_pad;
84
111
  static u8 sfx_timer;
112
+ static u8 hud_dirty;
113
+
114
+ /* Boss state: ONE logical object that happens to be two hardware sprites. */
115
+ static u8 boss_active;
116
+ static u16 boss_x, boss_y;
117
+ static u8 boss_dir;
118
+ static u8 boss_hp;
119
+ static u8 boss_flash; /* hit feedback: swap palette for a few frames */
120
+ static u8 boss_shot_timer;
121
+
122
+ /* Game states — the shell every example shares: title → play → game over. */
123
+ #define ST_TITLE 0
124
+ #define ST_PLAY 1
125
+ #define ST_OVER 2
126
+ static u8 state;
127
+
128
+ static u16 tile_buf[16]; /* scratch for one 8x8 tile */
129
+ static u16 spr_buf[64]; /* scratch for one 16x16 sprite cell */
130
+
131
+ /* ── GAME LOGIC (clay) — 5x7 glyph font: blank, 0-9, A-Z, dash ──────────────
132
+ * Each glyph is 7 rows of 5 bits (bit4 = leftmost). upload_font() expands
133
+ * them into 8x8 1-plane tiles; drawn with BG sub-palette 1 (white). */
134
+ #define G_BLANK 0
135
+ #define G_DIGIT 1 /* '0'..'9' -> glyphs 1..10 */
136
+ #define G_ALPHA 11 /* 'A'..'Z' -> glyphs 11..36 */
137
+ #define G_DASH 37
138
+ #define NUM_GLYPHS 38
139
+
140
+ static const u8 FONT5x7[NUM_GLYPHS][7] = {
141
+ {0,0,0,0,0,0,0},
142
+ {0x0E,0x11,0x13,0x15,0x19,0x11,0x0E}, {0x04,0x0C,0x04,0x04,0x04,0x04,0x0E},
143
+ {0x0E,0x11,0x01,0x02,0x04,0x08,0x1F}, {0x1F,0x02,0x04,0x02,0x01,0x11,0x0E},
144
+ {0x02,0x06,0x0A,0x12,0x1F,0x02,0x02}, {0x1F,0x10,0x1E,0x01,0x01,0x11,0x0E},
145
+ {0x06,0x08,0x10,0x1E,0x11,0x11,0x0E}, {0x1F,0x01,0x02,0x04,0x08,0x08,0x08},
146
+ {0x0E,0x11,0x11,0x0E,0x11,0x11,0x0E}, {0x0E,0x11,0x11,0x0F,0x01,0x02,0x0C},
147
+ {0x0E,0x11,0x11,0x1F,0x11,0x11,0x11}, {0x1E,0x11,0x11,0x1E,0x11,0x11,0x1E},
148
+ {0x0E,0x11,0x10,0x10,0x10,0x11,0x0E}, {0x1E,0x11,0x11,0x11,0x11,0x11,0x1E},
149
+ {0x1F,0x10,0x10,0x1E,0x10,0x10,0x1F}, {0x1F,0x10,0x10,0x1E,0x10,0x10,0x10},
150
+ {0x0E,0x11,0x10,0x17,0x11,0x11,0x0F}, {0x11,0x11,0x11,0x1F,0x11,0x11,0x11},
151
+ {0x0E,0x04,0x04,0x04,0x04,0x04,0x0E}, {0x07,0x02,0x02,0x02,0x02,0x12,0x0C},
152
+ {0x11,0x12,0x14,0x18,0x14,0x12,0x11}, {0x10,0x10,0x10,0x10,0x10,0x10,0x1F},
153
+ {0x11,0x1B,0x15,0x15,0x11,0x11,0x11}, {0x11,0x19,0x15,0x13,0x11,0x11,0x11},
154
+ {0x0E,0x11,0x11,0x11,0x11,0x11,0x0E}, {0x1E,0x11,0x11,0x1E,0x10,0x10,0x10},
155
+ {0x0E,0x11,0x11,0x11,0x15,0x12,0x0D}, {0x1E,0x11,0x11,0x1E,0x14,0x12,0x11},
156
+ {0x0F,0x10,0x10,0x0E,0x01,0x01,0x1E}, {0x1F,0x04,0x04,0x04,0x04,0x04,0x04},
157
+ {0x11,0x11,0x11,0x11,0x11,0x11,0x0E}, {0x11,0x11,0x11,0x11,0x11,0x0A,0x04},
158
+ {0x11,0x11,0x11,0x15,0x15,0x15,0x0A}, {0x11,0x11,0x0A,0x04,0x0A,0x11,0x11},
159
+ {0x11,0x11,0x0A,0x04,0x04,0x04,0x04}, {0x1F,0x01,0x02,0x04,0x08,0x10,0x1F},
160
+ {0x00,0x00,0x00,0x1F,0x00,0x00,0x00},
161
+ };
85
162
 
86
- static u16 tile_buf[16]; /* scratch for one 8x8 tile */
87
- static u16 spr_buf[64]; /* scratch for one 16x16 sprite */
163
+ /* ── GAME LOGIC (clay) — sprite masks (16 rows × 16 bits, bit15 leftmost) ── */
164
+ static const u16 ship_mask[16] = {
165
+ 0x0180, 0x0180, 0x03C0, 0x03C0, 0x07E0, 0x07E0, 0x0FF0, 0x0FF0,
166
+ 0x1FF8, 0x1FF8, 0x3FFC, 0x7FFE, 0xFFFF, 0xE187, 0xC003, 0x8001
167
+ };
168
+ static const u16 bullet_mask[16] = {
169
+ 0x0000, 0x0180, 0x03C0, 0x03C0, 0x07E0, 0x07E0, 0x07E0, 0x07E0,
170
+ 0x07E0, 0x07E0, 0x03C0, 0x03C0, 0x0180, 0x0000, 0x0000, 0x0000
171
+ };
172
+ static const u16 enemy_mask[16] = {
173
+ 0x0000, 0x4002, 0x6006, 0x7FFE, 0x7FFE, 0xFDBF, 0xFFFF, 0xFFFF,
174
+ 0xFFFF, 0x7FFE, 0x3FFC, 0x1FF8, 0x300C, 0x6006, 0x4002, 0x0000
175
+ };
176
+
177
+ /* ── GAME LOGIC (clay) — the boss's LEFT half (32x32). 2 u16 per row
178
+ * (cols 0-15, cols 16-31). The right half is this art MIRRORED at upload
179
+ * time — symmetric bosses cost half the data. body = hull (colour 1);
180
+ * core = the glowing eye + cannon tips (colour 3, a subset of body). */
181
+ static const u16 boss_body[64] = {
182
+ 0x0000,0x0000, 0x0000,0x001F, 0x0000,0x007F, 0x0000,0x00FF,
183
+ 0x0000,0x01FF, 0x0000,0x7FFF, 0x0003,0xFFFF, 0x001F,0xFFFF,
184
+ 0x007F,0xFFFF, 0x01FF,0xFFFF, 0x07FF,0xFFFF, 0x0FFF,0xFFFF,
185
+ 0x1FFF,0xFFFF, 0x1FFF,0xFFFF, 0x3FFF,0xFFFF, 0x3FFF,0xFFFF,
186
+ 0x3FFF,0xFFFF, 0x3FFF,0xFFFF, 0x3FFF,0xFFFF, 0x3FFF,0xFFFF,
187
+ 0x3FFF,0xFFFF, 0x3FFF,0xFFFF, 0x3FFF,0xFFFF, 0x1FFF,0xFFFF,
188
+ 0x1FFF,0xEFFF, 0x0FF3,0xEFFF, 0x07E0,0x6FFF, 0x0000,0x01FF,
189
+ 0x0000,0x0000, 0x0000,0x0000, 0x0000,0x0000, 0x0000,0x0000,
190
+ };
191
+ static const u16 boss_core[64] = {
192
+ 0x0000,0x0000, 0x0000,0x0000, 0x0000,0x0000, 0x0000,0x0000,
193
+ 0x0000,0x0000, 0x0000,0x0000, 0x0000,0x0000, 0x0000,0x0000,
194
+ 0x0000,0x0000, 0x0000,0x0000, 0x0000,0x0000, 0x0000,0x000F,
195
+ 0x0000,0x001F, 0x0000,0x003F, 0x0000,0x003F, 0x0000,0x003F,
196
+ 0x0000,0x003F, 0x0000,0x003F, 0x03C0,0x003F, 0x03C0,0x001F,
197
+ 0x07E0,0x000F, 0x03C0,0x0000, 0x03C0,0x0000, 0x0000,0x0000,
198
+ 0x0000,0x0000, 0x0000,0x0000, 0x0000,0x0000, 0x0000,0x0000,
199
+ 0x0000,0x0000, 0x0000,0x0000, 0x0000,0x0000, 0x0000,0x0000,
200
+ };
88
201
 
89
- /* ---- tile/sprite builders ----------------------------------------------- */
202
+ /* ── GAME LOGIC (clay) — tile/sprite builders ────────────────────────────── */
90
203
  static void make_solid_tile(u16 *t, u8 ci) {
91
204
  u8 r;
92
205
  u8 p0 = (ci & 1) ? 0xFF : 0x00;
93
206
  u8 p1 = (ci & 2) ? 0xFF : 0x00;
94
- u8 p2 = (ci & 4) ? 0xFF : 0x00;
95
- u8 p3 = (ci & 8) ? 0xFF : 0x00;
96
207
  for (r = 0; r < 8; ++r) {
97
208
  t[r] = (u16)(p0 | (p1 << 8));
98
- t[r + 8] = (u16)(p2 | (p3 << 8));
209
+ t[r + 8] = 0;
99
210
  }
100
211
  }
101
212
 
102
- /* space tile with one star pixel in colour index 3 at (row 2, col 5) */
103
- static void make_star_tile(u16 *t) {
104
- u8 r;
105
- for (r = 0; r < 8; ++r) { t[r] = 0x00FF; t[r + 8] = 0x0000; } /* base = colour 1 */
106
- /* star = colour 3 (planes 0+1) at row 2: set plane1 bit too for that row */
107
- t[2] = (u16)(0x00FF | (0x04 << 8)); /* plane0 row + plane1 single pixel */
108
- }
109
-
110
- /* upload one 16x16 sprite from a 16-row body mask in colour `ci` */
111
- static void make_sprite(u16 vram, const u16 *body, u8 ci) {
213
+ /* one-colour 16x16 sprite cell from a 16-row mask */
214
+ static void make_sprite16(u16 vram, const u16 *mask, u8 ci) {
112
215
  u8 r;
113
216
  for (r = 0; r < 64; ++r) spr_buf[r] = 0;
114
217
  for (r = 0; r < 16; ++r) {
115
- if (ci & 1) spr_buf[r] = body[r]; /* plane0 */
116
- if (ci & 2) spr_buf[r + 16] = body[r]; /* plane1 */
117
- if (ci & 4) spr_buf[r + 32] = body[r]; /* plane2 */
118
- if (ci & 8) spr_buf[r + 48] = body[r]; /* plane3 */
218
+ if (ci & 1) spr_buf[r] = mask[r]; /* plane 0 */
219
+ if (ci & 2) spr_buf[r + 16] = mask[r]; /* plane 1 */
119
220
  }
120
221
  load_tiles(vram, spr_buf, 64);
121
222
  }
122
223
 
123
224
  static void upload_font(void) {
124
- u8 g, row, bits, plane0;
225
+ u8 g, row, bits, px;
125
226
  for (g = 0; g < NUM_GLYPHS; ++g) {
126
227
  for (row = 0; row < 16; ++row) tile_buf[row] = 0;
127
228
  for (row = 0; row < 7; ++row) {
128
229
  bits = FONT5x7[g][row];
129
- plane0 = 0;
130
- if (bits & 0x10) plane0 |= 0x40;
131
- if (bits & 0x08) plane0 |= 0x20;
132
- if (bits & 0x04) plane0 |= 0x10;
133
- if (bits & 0x02) plane0 |= 0x08;
134
- if (bits & 0x01) plane0 |= 0x04;
135
- tile_buf[row] = (u16)plane0;
230
+ px = 0;
231
+ if (bits & 0x10) px |= 0x40;
232
+ if (bits & 0x08) px |= 0x20;
233
+ if (bits & 0x04) px |= 0x10;
234
+ if (bits & 0x02) px |= 0x08;
235
+ if (bits & 0x01) px |= 0x04;
236
+ tile_buf[row] = (u16)px;
136
237
  }
137
238
  load_tiles((u16)(FONT_VRAM + g * 16), tile_buf, 16);
138
239
  }
139
240
  }
140
241
 
242
+ /* mirror a 16-bit row (bit15 <-> bit0) for the boss's right half */
243
+ static u16 rev16(u16 v) {
244
+ u16 out = 0;
245
+ u8 i;
246
+ for (i = 0; i < 16; ++i) {
247
+ out <<= 1;
248
+ if (v & 1) out |= 1;
249
+ v >>= 1;
250
+ }
251
+ return out;
252
+ }
253
+
254
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
255
+ * LARGE-SPRITE PATTERN LAYOUT — the half of the boss trick that lives in
256
+ * VRAM. A 32x32 HuC6270 sprite is FOUR 16x16 cells (64 words each) stored
257
+ * consecutively in TL, TR, BL, BR order, and its SATB pattern code must be
258
+ * 4-ALIGNED (the hardware ignores the low 2 bits and adds them back as
259
+ * column/row). Get the order wrong and the boss renders scrambled — four
260
+ * recognizable quarters in the wrong places. The other half of the trick
261
+ * (the SATB attribute bits) is in push_sprites() below.
262
+ *
263
+ * requires: BOSS_VRAM >> 6 a multiple of 4; 8 consecutive free cells
264
+ * (512 words) at BOSS_VRAM; set_sprite_ex() from pce_video.c. */
265
+ static void upload_boss(void) {
266
+ u8 half, cr, cc, row;
267
+ u16 body_bits, core_bits;
268
+ u16 vram = BOSS_VRAM;
269
+ for (half = 0; half < 2; ++half) { /* 0 = left, 1 = right */
270
+ for (cr = 0; cr < 2; ++cr) { /* cell row (top/bottom) */
271
+ for (cc = 0; cc < 2; ++cc) { /* cell col (left/right) */
272
+ for (row = 0; row < 64; ++row) spr_buf[row] = 0;
273
+ for (row = 0; row < 16; ++row) {
274
+ u8 y = (u8)(cr * 16 + row);
275
+ if (half == 0) { /* left half: stored art */
276
+ body_bits = boss_body[y * 2 + cc];
277
+ core_bits = boss_core[y * 2 + cc];
278
+ } else { /* right half: mirrored */
279
+ body_bits = rev16(boss_body[y * 2 + (1 - cc)]);
280
+ core_bits = rev16(boss_core[y * 2 + (1 - cc)]);
281
+ }
282
+ /* hull pixels = colour 1 (plane0), eye/cannon core =
283
+ * colour 3 (planes 0+1) — core is a subset of body. */
284
+ spr_buf[row] = body_bits;
285
+ spr_buf[row + 16] = core_bits;
286
+ }
287
+ load_tiles(vram, spr_buf, 64);
288
+ vram += 64; /* next cell: TL,TR,BL,BR */
289
+ }
290
+ }
291
+ }
292
+ }
293
+
141
294
  static void upload_art(void) {
142
- /* ship: an upward-pointing arrow */
143
- static const u16 ship[16] = {
144
- 0x0180, 0x0180, 0x03C0, 0x03C0, 0x07E0, 0x07E0, 0x0FF0, 0x0FF0,
145
- 0x1FF8, 0x1FF8, 0x3FFC, 0x7FFE, 0xFFFF, 0xE187, 0xC003, 0x8001
146
- };
147
- /* bullet: a small vertical pellet */
148
- static const u16 bullet[16] = {
149
- 0x0000, 0x0180, 0x03C0, 0x03C0, 0x07E0, 0x07E0, 0x07E0, 0x07E0,
150
- 0x07E0, 0x07E0, 0x03C0, 0x03C0, 0x0180, 0x0000, 0x0000, 0x0000
151
- };
152
- /* enemy: a downward, blocky invader */
153
- static const u16 enemy[16] = {
154
- 0x0000, 0x4002, 0x6006, 0x7FFE, 0x7FFE, 0xFDBF, 0xFFFF, 0xFFFF,
155
- 0xFFFF, 0x7FFE, 0x3FFC, 0x1FF8, 0x300C, 0x6006, 0x4002, 0x0000
156
- };
157
295
  upload_font();
158
296
  make_solid_tile(tile_buf, 1); load_tiles(STAR0_VRAM, tile_buf, 16);
159
297
  make_solid_tile(tile_buf, 2); load_tiles(STAR1_VRAM, tile_buf, 16);
160
- make_star_tile(tile_buf); load_tiles(STAR2_VRAM, tile_buf, 16);
161
- make_sprite(SHIP_VRAM, ship, 1); /* white */
162
- make_sprite(BULLET_VRAM, bullet, 1); /* white (sub-pal 1 = yellow) */
163
- make_sprite(ENEMY_VRAM, enemy, 1); /* white (sub-pal 2 = red) */
298
+ make_solid_tile(tile_buf, 1); tile_buf[2] |= 0x0010; tile_buf[2 + 8] = 0x0010;
299
+ load_tiles(STAR2_VRAM, tile_buf, 16); /* band + colour-3 star px */
300
+ make_sprite16(SHIP_VRAM, ship_mask, 1);
301
+ make_sprite16(BULLET_VRAM, bullet_mask, 1);
302
+ make_sprite16(ENEMY_VRAM, enemy_mask, 1);
303
+ upload_boss();
164
304
  }
165
305
 
166
- /* ---- BAT / HUD ---------------------------------------------------------- */
306
+ /* ── GAME LOGIC (clay) — BAT text + starfield ─────────────────────────────── */
307
+ static void put_glyph(u8 col, u8 row, u8 glyph) {
308
+ u16 e = BAT_ENTRY(1, (u16)(FONT_VRAM + glyph * 16)); /* pal 1 = white */
309
+ vram_set_write_addr((u16)(BAT_VRAM + row * 32 + col));
310
+ VDC_DATA_LO = (u8)(e & 0xFF);
311
+ VDC_DATA_HI = (u8)(e >> 8);
312
+ }
313
+
314
+ static void draw_text(u8 col, u8 row, const char *s) {
315
+ u8 c;
316
+ while ((c = (u8)*s++) != 0) {
317
+ u8 g = G_BLANK;
318
+ if (c >= '0' && c <= '9') g = (u8)(G_DIGIT + c - '0');
319
+ else if (c >= 'A' && c <= 'Z') g = (u8)(G_ALPHA + c - 'A');
320
+ else if (c == '-') g = G_DASH;
321
+ put_glyph(col++, row, g);
322
+ }
323
+ }
324
+
325
+ static void draw_num5(u8 col, u8 row, u16 v) {
326
+ u8 i, d[5];
327
+ for (i = 0; i < 5; ++i) { d[i] = (u8)(v % 10); v /= 10; }
328
+ for (i = 0; i < 5; ++i) put_glyph((u8)(col + i), row, (u8)(G_DIGIT + d[4 - i]));
329
+ }
330
+
331
+ /* banded starfield over the whole 32x32 BAT (two band colours + sparse
332
+ * twinkle tiles — the bands keep the screen from being one flat colour) */
167
333
  static void draw_starfield(void) {
168
334
  u8 r, c;
169
335
  u16 e0 = BAT_ENTRY(0, STAR0_VRAM);
@@ -173,51 +339,118 @@ static void draw_starfield(void) {
173
339
  for (r = 0; r < 32; ++r) {
174
340
  vram_set_write_addr((u16)(BAT_VRAM + r * 32));
175
341
  for (c = 0; c < 32; ++c) {
176
- e = (r & 2) ? e1 : e0; /* depth bands */
177
- if (((r * 7 + c * 5) & 7) == 0) e = e2; /* sparse stars */
342
+ e = (r & 2) ? e1 : e0;
343
+ if (((r * 7 + c * 5) & 7) == 0) e = e2;
178
344
  VDC_DATA_LO = (u8)(e & 0xFF);
179
345
  VDC_DATA_HI = (u8)(e >> 8);
180
346
  }
181
347
  }
182
348
  }
183
349
 
184
- static void put_glyph(u8 col, u8 row, u8 glyph) {
185
- u16 e = BAT_ENTRY(0, (u16)(FONT_VRAM + glyph * 16));
186
- vram_set_write_addr((u16)(BAT_VRAM + row * 32 + col));
187
- VDC_DATA_LO = (u8)(e & 0xFF);
188
- VDC_DATA_HI = (u8)(e >> 8);
350
+ /* HUD: row 1 = "SC 00000 HI 00000 SH 3" */
351
+ static void draw_hud_labels(void) {
352
+ draw_text(1, 1, "SC");
353
+ draw_text(12, 1, "HI");
354
+ draw_text(23, 1, "SH");
189
355
  }
190
356
 
191
- static void draw_hud_label(void) {
192
- static const u8 lbl[5] = { G_S, G_C, G_O, G_R, G_E };
193
- u8 i;
194
- for (i = 0; i < 5; ++i) put_glyph((u8)(1 + i), 1, lbl[i]);
357
+ static void draw_hud_numbers(void) {
358
+ draw_num5(4, 1, score);
359
+ draw_num5(15, 1, hiscore);
360
+ put_glyph(26, 1, (u8)(G_DIGIT + lives));
195
361
  }
196
362
 
197
- static void draw_score(void) {
198
- u16 v = score;
199
- u8 d0, d1, d2, d3;
200
- d3 = (u8)(v % 10); v /= 10;
201
- d2 = (u8)(v % 10); v /= 10;
202
- d1 = (u8)(v % 10); v /= 10;
203
- d0 = (u8)(v % 10);
204
- put_glyph(7, 1, (u8)(G_0 + d0));
205
- put_glyph(8, 1, (u8)(G_0 + d1));
206
- put_glyph(9, 1, (u8)(G_0 + d2));
207
- put_glyph(10, 1, (u8)(G_0 + d3));
363
+ /* ── HARDWARE TRUTH: a bare HuCard CANNOT save a hi-score (in-session only) ──
364
+ * This was researched and corrected: earlier versions wrote the hi-score to
365
+ * BRAM ("backup RAM", bank $F7) and claimed it persisted across power cycles.
366
+ * That is NOT honest for a HuCard game. On REAL hardware a plain HuCard plugged
367
+ * into a base PC Engine / TurboGrafx-16 has NO backup RAM at all — BRAM exists
368
+ * ONLY when a peripheral is attached: the CD-ROM² System (2KB kept by a
369
+ * supercapacitor), the Tennokoe Bank HuCard, or the Memory Base 128. No
370
+ * commercial HuCard self-saved; they used PASSWORDS. (The often-cited Populous
371
+ * "ROMRAM" SRAM was the game's own working RAM, not a battery save.) An
372
+ * emulator like geargrafx exposes BRAM unconditionally, so the old code
373
+ * "worked" in emulation in a way the real machine never would.
374
+ *
375
+ * So this game keeps an IN-SESSION hi-score only (like the honest 2600/Lynx
376
+ * examples) — it survives game-overs within a power-on, resets to 0 on a cold
377
+ * boot. To make it ACTUALLY persist on real hardware you would target a
378
+ * peripheral: write to BRAM only after detecting one (and go through the System
379
+ * Card BIOS's 'HUBM' directory for CD saves), or move the game to a CD-ROM²
380
+ * build. Either is a real-hardware feature, not a property of the cartridge. */
381
+ static u16 hiscore_load(void) {
382
+ return 0; /* cold boot: no persistence on a bare HuCard */
383
+ }
384
+
385
+ static void hiscore_save(u16 v) {
386
+ (void)v; /* in-session only — nowhere to persist on real HW */
208
387
  }
209
388
 
210
- /* ---- gameplay helpers --------------------------------------------------- */
211
- static u8 aabb(Obj *a, Obj *b) {
212
- return (u8)(a->x < b->x + 14 && a->x + 14 > b->x &&
213
- a->y < b->y + 14 && a->y + 14 > b->y);
389
+ /* ── GAME LOGIC (clay) — music: a 2-channel tune ticked once per frame ──────
390
+ * PSG channel plan: 5 = melody, 4 = bass, 2/3 = SFX (tones cut by sfx_timer).
391
+ * PCE frequency regs are DIVIDERS: pitch 3.58MHz / (32 × value), so a
392
+ * BIGGER number is a LOWER note. Note indices into NOTE_DIV below. */
393
+ enum { R = 0, A2N, C3, F3, G3, A3, B3, C4, D4, E4, F4, G4, A4, B4, C5, D5, E5 };
394
+ static const u16 NOTE_DIV[17] = {
395
+ 0, 1017, 854, 641, 571, 508, 453, 427, 381, 339, 320, 285, 254, 226, 214, 190, 170
396
+ };
397
+ /* 16 melody steps + 8 bass steps (one bass note per 2 melody steps) */
398
+ static const u8 MEL_TITLE[16] = { C4,E4,G4,C5, B4,G4,E4,G4, A3,C4,E4,A4, G4,E4,D4,B3 };
399
+ static const u8 BAS_TITLE[8] = { C3,C3, A2N,A2N, F3,F3, G3,G3 };
400
+ static const u8 MEL_PLAY[16] = { E4,R,E4,G4, A4,G4,E4,D4, C4,D4,E4,G4, E4,D4,C4,R };
401
+ static const u8 BAS_PLAY[8] = { A2N,A2N, F3,F3, C3,C3, G3,G3 };
402
+ static const u8 MEL_OVER[16] = { C5,R,B4,R, A4,R,G4,R, E4,R,D4,R, C4,R,R,R };
403
+
404
+ static u8 music_song; /* reuses the ST_* ids */
405
+ static u8 music_step, music_timer, music_done;
406
+
407
+ static void music_set(u8 song) {
408
+ music_song = song;
409
+ music_step = 0;
410
+ music_timer = 0;
411
+ music_done = 0;
412
+ psg_off(4);
413
+ psg_off(5);
214
414
  }
215
415
 
416
+ static void music_tick(void) {
417
+ const u8 *mel;
418
+ u8 n;
419
+ if (music_done) return;
420
+ if (music_timer == 0) {
421
+ mel = (music_song == ST_PLAY) ? MEL_PLAY
422
+ : (music_song == ST_OVER) ? MEL_OVER : MEL_TITLE;
423
+ n = mel[music_step & 15];
424
+ if (n != R) psg_tone(5, NOTE_DIV[n], 26);
425
+ else psg_off(5);
426
+ if (music_song != ST_OVER) { /* the game-over jingle has no bass */
427
+ n = ((music_step & 1) == 0)
428
+ ? ((music_song == ST_PLAY) ? BAS_PLAY[(music_step >> 1) & 7]
429
+ : BAS_TITLE[(music_step >> 1) & 7])
430
+ : R;
431
+ if (n != R) psg_tone(4, NOTE_DIV[n], 20);
432
+ }
433
+ ++music_step;
434
+ if (music_song == ST_OVER && music_step >= 16) { /* play once, stop */
435
+ music_done = 1;
436
+ psg_off(4);
437
+ psg_off(5);
438
+ }
439
+ }
440
+ ++music_timer;
441
+ if (music_timer >= 8) music_timer = 0;
442
+ }
443
+
444
+ /* ── GAME LOGIC (clay) — helpers ──────────────────────────────────────────── */
216
445
  static u16 next_rand(void) {
217
446
  rng = (u16)(rng * 25173u + 13849u);
218
447
  return rng;
219
448
  }
220
449
 
450
+ static u8 aabb(u16 ax, u16 ay, u16 aw, u16 ah, u16 bx, u16 by, u16 bw, u16 bh) {
451
+ return (u8)(ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by);
452
+ }
453
+
221
454
  static void fire(void) {
222
455
  u8 i;
223
456
  for (i = 0; i < MAX_BULLETS; ++i) {
@@ -225,123 +458,316 @@ static void fire(void) {
225
458
  bullets[i].x = player.x;
226
459
  bullets[i].y = (u16)(player.y - 10);
227
460
  bullets[i].alive = 1;
228
- psg_tone(2, 0x180, 31); /* max vol — playtest said too quiet */
461
+ psg_tone(2, 0x180, 31);
229
462
  sfx_timer = 4;
230
463
  return;
231
464
  }
232
465
  }
233
466
  }
234
467
 
235
- static void spawn(void) {
468
+ static void spawn_enemy(u16 x, u16 y) {
236
469
  u8 i;
237
470
  for (i = 0; i < MAX_ENEMIES; ++i) {
238
471
  if (!enemies[i].alive) {
239
- enemies[i].x = (u16)(8 + (next_rand() >> 8) % 224);
240
- enemies[i].y = 8;
472
+ enemies[i].x = x;
473
+ enemies[i].y = y;
241
474
  enemies[i].alive = 1;
242
475
  return;
243
476
  }
244
477
  }
245
478
  }
246
479
 
247
- void main(void) {
248
- u8 i, j;
249
-
250
- _pce_keep[0] = 0;
251
-
252
- /* palette: BG sub-pal 0 + sprite sub-pals 0/1/2 */
253
- vce_set_color(0, PCE_RGB(0, 0, 1)); /* backdrop dark blue */
254
- vce_set_color(1, PCE_RGB(0, 0, 3)); /* BG c1: deep space blue */
255
- vce_set_color(2, PCE_RGB(1, 1, 4)); /* BG c2: lighter space band */
256
- vce_set_color(3, PCE_RGB(7, 7, 7)); /* BG c3: star white */
257
- vce_set_color(256, PCE_RGB(0, 0, 0)); /* spr pal0 transparent */
258
- vce_set_color(257, PCE_RGB(2, 6, 7)); /* spr pal0 c1: cyan ship */
259
- vce_set_color(272, PCE_RGB(0, 0, 0)); /* spr pal1 transparent */
260
- vce_set_color(273, PCE_RGB(7, 7, 0)); /* spr pal1 c1: yellow bullet */
261
- vce_set_color(288, PCE_RGB(0, 0, 0)); /* spr pal2 transparent */
262
- vce_set_color(289, PCE_RGB(7, 1, 1)); /* spr pal2 c1: red enemy */
480
+ /* ── GAME LOGIC (clay) — screen painters (full repaint per state change) ── */
481
+ static void paint_title(void) {
482
+ draw_starfield();
483
+ draw_text((u8)((32 - (sizeof(GAME_TITLE) - 1)) / 2), 8, GAME_TITLE);
484
+ draw_text(10, 14, "PRESS RUN");
485
+ draw_text(11, 18, "HI");
486
+ draw_num5(14, 18, hiscore);
487
+ draw_text(7, 22, "BOSS EVERY 10 KILLS");
488
+ }
263
489
 
264
- upload_art();
490
+ static void paint_field(void) {
265
491
  draw_starfield();
266
- draw_hud_label();
492
+ draw_hud_labels();
493
+ draw_hud_numbers();
494
+ }
267
495
 
268
- player.x = 120; player.y = 180; player.alive = 1;
496
+ static void start_game(void) {
497
+ u8 i;
269
498
  for (i = 0; i < MAX_BULLETS; ++i) bullets[i].alive = 0;
270
499
  for (i = 0; i < MAX_ENEMIES; ++i) enemies[i].alive = 0;
500
+ player.x = 120; player.y = 192; player.alive = 1;
501
+ lives = START_LIVES;
271
502
  score = 0;
503
+ level = 0;
504
+ kills = 0;
505
+ invuln = 0;
506
+ fire_cd = 0;
272
507
  spawn_timer = 0;
273
- rng = 0xC0DE;
274
- prev_pad = 0;
275
- sfx_timer = 0;
276
- draw_score();
508
+ boss_active = 0;
509
+ boss_flash = 0;
510
+ paint_field();
511
+ music_set(ST_PLAY);
512
+ state = ST_PLAY;
513
+ }
277
514
 
278
- pce_joy_init();
279
- disp_enable();
515
+ static void game_over(void) {
516
+ if (score > hiscore) {
517
+ hiscore = score;
518
+ hiscore_save(hiscore); /* in-session only (no save on a bare HuCard) */
519
+ }
520
+ draw_text(11, 12, "GAME OVER");
521
+ draw_text(10, 14, "PRESS RUN");
522
+ music_set(ST_OVER);
523
+ state = ST_OVER;
524
+ }
280
525
 
281
- for (;;) {
282
- waitvsync();
283
- psg_music_tick();
284
- pad = pce_joy_read();
526
+ static void boss_enter(void) {
527
+ boss_active = 1;
528
+ boss_x = 96;
529
+ boss_y = 24;
530
+ boss_dir = 1;
531
+ boss_hp = (u8)(10 + level * 4);
532
+ if (boss_hp > 30) boss_hp = 30;
533
+ boss_shot_timer = 0;
534
+ }
285
535
 
286
- /* move ship */
287
- if ((pad & PCE_JOY_LEFT) && player.x > 2) player.x -= 4; /* playtest: 'slow overall' */
288
- if ((pad & PCE_JOY_RIGHT) && player.x < 238) player.x += 4;
289
- if ((pad & PCE_JOY_UP) && player.y > 8) player.y -= 3;
290
- if ((pad & PCE_JOY_DOWN) && player.y < 208) player.y += 3;
291
- if ((pad & PCE_JOY_I) && !(prev_pad & PCE_JOY_I)) fire();
292
- prev_pad = pad;
536
+ static void boss_die(void) {
537
+ boss_active = 0;
538
+ boss_flash = 0;
539
+ if (score < 60000u) score += 500;
540
+ ++level;
541
+ kills = 0;
542
+ hud_dirty = 1;
543
+ psg_tone(3, 0x600, 31); /* low rumble */
544
+ sfx_timer = 24;
545
+ }
293
546
 
294
- /* advance bullets */
295
- for (i = 0; i < MAX_BULLETS; ++i) {
296
- if (!bullets[i].alive) continue;
297
- if (bullets[i].y < 6) { bullets[i].alive = 0; continue; }
298
- bullets[i].y -= 6;
299
- }
547
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
548
+ * SPRITE STAGING + THE SATB DMA. The VDC never reads your RAM: sprites live
549
+ * in its INTERNAL sprite attribute table, refreshed by a DMA you schedule by
550
+ * writing R19 (satb_dma() does the copy + the R19 write; the transfer itself
551
+ * happens at the next vblank). So the per-frame contract is:
552
+ * waitvsync() → restage EVERY slot → satb_dma()
553
+ * Stage during vblank — satb_dma() also streams 256 words through the VWR
554
+ * port, and doing that mid-display tears sprite pattern fetches.
555
+ *
556
+ * THE BOSS (the PCE signature): one logical object, TWO SATB entries. Each
557
+ * half is a 32x32 sprite — SPR_CGX_32|SPR_CGY_32 in the attribute word —
558
+ * placed at (boss_x, boss_y) and (boss_x+32, boss_y). They move as one unit
559
+ * because ONE pair of variables drives both entries; nothing else keeps
560
+ * them glued. A 64x32 boss this way costs 2 sprites of the 64-sprite budget
561
+ * (and 4 of the 16-sprites-per-scanline budget) — the same object on a
562
+ * 8x8/8x16-sprite machine costs 16+. CGY goes to 64 if you want a 32x64
563
+ * tower from a SINGLE entry (SPR_CGY_64, 8-aligned pattern).
564
+ *
565
+ * requires: set_sprite_ex() + the 4-aligned boss cells from upload_boss(). */
566
+ static void push_sprites(void) {
567
+ u8 i;
568
+ /* ship (slot 0) — flickers while invulnerable by parking on odd frames */
569
+ if (player.alive && !(invuln & 2)) set_sprite(SLOT_SHIP, player.x, player.y, SHIP_PAT, PAL_SHIP);
570
+ else set_sprite(SLOT_SHIP, player.x, OFFSCREEN_Y, SHIP_PAT, PAL_SHIP);
571
+ for (i = 0; i < MAX_BULLETS; ++i)
572
+ set_sprite((u8)(SLOT_BULLET + i), bullets[i].x,
573
+ bullets[i].alive ? bullets[i].y : OFFSCREEN_Y, BULLET_PAT, PAL_BULLET);
574
+ for (i = 0; i < MAX_ENEMIES; ++i)
575
+ set_sprite((u8)(SLOT_ENEMY + i), enemies[i].x,
576
+ enemies[i].alive ? enemies[i].y : OFFSCREEN_Y, ENEMY_PAT, PAL_ENEMY);
577
+ if (boss_active) {
578
+ u8 pal = boss_flash ? PAL_ENEMY : PAL_BOSS; /* hit = red flash */
579
+ set_sprite_ex(SLOT_BOSS_L, boss_x, boss_y, BOSSL_PAT, pal,
580
+ SPR_CGX_32 | SPR_CGY_32);
581
+ set_sprite_ex(SLOT_BOSS_R, (u16)(boss_x + 32), boss_y, BOSSR_PAT, pal,
582
+ SPR_CGX_32 | SPR_CGY_32);
583
+ } else {
584
+ set_sprite_ex(SLOT_BOSS_L, 0, OFFSCREEN_Y, BOSSL_PAT, PAL_BOSS, SPR_CGX_32 | SPR_CGY_32);
585
+ set_sprite_ex(SLOT_BOSS_R, 0, OFFSCREEN_Y, BOSSR_PAT, PAL_BOSS, SPR_CGX_32 | SPR_CGY_32);
586
+ }
587
+ }
588
+
589
+ /* twinkle: rewrite the star tile's pixel row every 16 frames — animation
590
+ * without touching the BAT (one 16-word upload in vblank) */
591
+ static void twinkle(void) {
592
+ u8 phase;
593
+ ++twinkle_timer;
594
+ if ((twinkle_timer & 15) != 0) return;
595
+ phase = (u8)((twinkle_timer >> 4) & 3);
596
+ make_solid_tile(tile_buf, 1);
597
+ tile_buf[phase * 2] |= 0x0010;
598
+ tile_buf[phase * 2 + 8] = 0x0010;
599
+ load_tiles(STAR2_VRAM, tile_buf, 16);
600
+ }
601
+
602
+ /* ── GAME LOGIC (clay) — the per-state updates ────────────────────────────── */
603
+ static void hit_ship(void) {
604
+ if (invuln) return;
605
+ psg_tone(3, 0x500, 31);
606
+ sfx_timer = 16;
607
+ if (lives > 0) --lives;
608
+ hud_dirty = 1;
609
+ if (lives == 0) {
610
+ game_over();
611
+ return;
612
+ }
613
+ invuln = 90;
614
+ player.x = 120;
615
+ player.y = 192;
616
+ }
617
+
618
+ static void update_play(void) {
619
+ u8 i, j;
620
+
621
+ /* ship */
622
+ if (pad & PCE_JOY_LEFT) { if (player.x > 2) player.x -= 3; }
623
+ if (pad & PCE_JOY_RIGHT) { if (player.x < 238) player.x += 3; }
624
+ if (pad & PCE_JOY_UP) { if (player.y > SHIP_MIN_Y) player.y -= 2; }
625
+ if (pad & PCE_JOY_DOWN) { if (player.y < 200) player.y += 2; }
626
+ if ((pad & PCE_JOY_I) && fire_cd == 0) { fire(); fire_cd = 8; }
627
+ if (fire_cd) --fire_cd;
628
+ if (invuln) --invuln;
629
+
630
+ /* bullets */
631
+ for (i = 0; i < MAX_BULLETS; ++i) {
632
+ if (!bullets[i].alive) continue;
633
+ if (bullets[i].y < 14) { bullets[i].alive = 0; continue; }
634
+ bullets[i].y -= 5;
635
+ }
636
+
637
+ /* enemies: drift down, faster each level */
638
+ for (i = 0; i < MAX_ENEMIES; ++i) {
639
+ if (!enemies[i].alive) continue;
640
+ enemies[i].y += (u16)(1 + (level >> 1));
641
+ if (enemies[i].y >= 224) enemies[i].alive = 0;
642
+ }
300
643
 
301
- /* advance enemies */
302
- for (i = 0; i < MAX_ENEMIES; ++i) {
303
- if (!enemies[i].alive) continue;
304
- enemies[i].y += 1;
305
- if (enemies[i].y >= 224) enemies[i].alive = 0;
644
+ if (boss_active) {
645
+ /* the boss sways as ONE unit; drones spawn from its eye */
646
+ boss_x += boss_dir ? 1 : -1;
647
+ if (boss_x >= 184) boss_dir = 0;
648
+ if (boss_x <= 8) boss_dir = 1;
649
+ if (boss_flash) --boss_flash;
650
+ ++boss_shot_timer;
651
+ if (boss_shot_timer >= (u8)(70 - level * 8)) {
652
+ boss_shot_timer = 0;
653
+ spawn_enemy((u16)(boss_x + 24), (u16)(boss_y + 28));
306
654
  }
655
+ } else {
656
+ ++spawn_timer;
657
+ if (spawn_timer >= (u8)(40 - level * 4)) {
658
+ spawn_timer = 0;
659
+ spawn_enemy((u16)(8 + (next_rand() >> 8) % 224), 16);
660
+ }
661
+ }
307
662
 
308
- /* spawn waves */
309
- spawn_timer++;
310
- if (spawn_timer >= 36) { spawn_timer = 0; spawn(); }
311
-
312
- /* bullet vs enemy */
313
- for (i = 0; i < MAX_BULLETS; ++i) {
314
- if (!bullets[i].alive) continue;
315
- for (j = 0; j < MAX_ENEMIES; ++j) {
316
- if (!enemies[j].alive) continue;
317
- if (aabb(&bullets[i], &enemies[j])) {
318
- bullets[i].alive = 0;
319
- enemies[j].alive = 0;
320
- if (score < 9999) score += 10;
321
- draw_score();
322
- psg_tone(3, 0x040, 31);
323
- sfx_timer = 6;
324
- break;
325
- }
663
+ /* bullets vs enemies + boss */
664
+ for (i = 0; i < MAX_BULLETS; ++i) {
665
+ if (!bullets[i].alive) continue;
666
+ for (j = 0; j < MAX_ENEMIES; ++j) {
667
+ if (!enemies[j].alive) continue;
668
+ if (aabb(bullets[i].x, bullets[i].y, 14, 14,
669
+ enemies[j].x, enemies[j].y, 14, 14)) {
670
+ bullets[i].alive = 0;
671
+ enemies[j].alive = 0;
672
+ if (score < 60000u) score += 10;
673
+ ++kills;
674
+ hud_dirty = 1;
675
+ psg_tone(3, 0x040, 31);
676
+ sfx_timer = 6;
677
+ break;
326
678
  }
327
679
  }
680
+ if (!bullets[i].alive) continue;
681
+ if (boss_active &&
682
+ aabb(bullets[i].x, bullets[i].y, 14, 14, boss_x, boss_y, 64, 30)) {
683
+ bullets[i].alive = 0;
684
+ boss_flash = 4;
685
+ psg_tone(3, 0x090, 29);
686
+ sfx_timer = 4;
687
+ if (--boss_hp == 0) boss_die();
688
+ }
689
+ }
690
+
691
+ /* enemies vs ship */
692
+ for (i = 0; i < MAX_ENEMIES; ++i) {
693
+ if (!enemies[i].alive) continue;
694
+ if (aabb(enemies[i].x, enemies[i].y, 14, 14, player.x, player.y, 14, 14)) {
695
+ enemies[i].alive = 0;
696
+ hit_ship();
697
+ if (state != ST_PLAY) return;
698
+ }
699
+ }
700
+
701
+ /* the next boss */
702
+ if (!boss_active && kills >= 10) boss_enter();
703
+ }
704
+
705
+ void main(void) {
706
+ u8 newpad;
707
+
708
+ _pce_keep[0] = 0; /* see the EMPTY-BSS TRAP note in pce_hw.h */
709
+
710
+ /* ── HARDWARE IDIOM (load-bearing — see TROUBLESHOOTING) ──
711
+ * Init order: palette → VRAM uploads → BAT paint → joypad → display ON.
712
+ * disp_enable() also sets the VBlank IRQ bit — without it waitvsync()
713
+ * never returns and the game freezes on its first frame. */
714
+ /* BG sub-pal 0: starfield. BG sub-pal 1: HUD/text (white on band). */
715
+ vce_set_color(0, PCE_RGB(0, 0, 1)); /* backdrop: near-black blue */
716
+ vce_set_color(1, PCE_RGB(0, 0, 3)); /* band A: deep space */
717
+ vce_set_color(2, PCE_RGB(1, 1, 4)); /* band B: lighter space */
718
+ vce_set_color(3, PCE_RGB(7, 7, 7)); /* star pixel: white */
719
+ vce_set_color(17, PCE_RGB(7, 7, 7)); /* text: white */
720
+ /* sprite sub-palettes (256 + pal*16 + index) */
721
+ vce_set_color(257, PCE_RGB(2, 6, 7)); /* pal0 c1: ship cyan */
722
+ vce_set_color(273, PCE_RGB(7, 7, 0)); /* pal1 c1: bullet yellow */
723
+ vce_set_color(289, PCE_RGB(7, 1, 1)); /* pal2 c1: enemy red */
724
+ vce_set_color(290, PCE_RGB(7, 1, 1));
725
+ vce_set_color(291, PCE_RGB(7, 5, 2)); /* pal2 c3: red-flash highlight */
726
+ vce_set_color(305, PCE_RGB(4, 2, 7)); /* pal3 c1: boss hull violet */
727
+ vce_set_color(307, PCE_RGB(7, 6, 1)); /* pal3 c3: boss eye amber */
328
728
 
329
- /* free the SFX channels so they're blips, not drones */
729
+ upload_art();
730
+
731
+ hiscore = hiscore_load(); /* always 0 — no persistence on a bare HuCard */
732
+ state = ST_TITLE;
733
+ paint_title();
734
+ music_set(ST_TITLE);
735
+
736
+ pce_joy_init();
737
+ disp_enable();
738
+
739
+ for (;;) {
740
+ waitvsync();
741
+
742
+ /* vblank work first: sprites + SATB DMA + queued BAT/VRAM writes */
743
+ push_sprites();
744
+ satb_dma();
745
+ if (hud_dirty && state != ST_TITLE) { draw_hud_numbers(); hud_dirty = 0; }
746
+ twinkle();
747
+
748
+ music_tick();
330
749
  if (sfx_timer) {
331
750
  --sfx_timer;
332
751
  if (sfx_timer == 0) { psg_off(2); psg_off(3); }
333
752
  }
334
753
 
335
- /* push sprites: player(0), bullets(1..6), enemies(7..12) */
336
- set_sprite(0, player.x, player.y, SHIP_VRAM >> 6, 0);
337
- for (i = 0; i < MAX_BULLETS; ++i) {
338
- u16 by = bullets[i].alive ? bullets[i].y : 0x1F0; /* park off-screen */
339
- set_sprite((u8)(1 + i), bullets[i].x, by, BULLET_VRAM >> 6, 1);
754
+ pad = pce_joy_read();
755
+ newpad = (u8)(pad & ~prev_pad);
756
+ prev_pad = pad;
757
+
758
+ if (state == ST_TITLE) {
759
+ if (newpad & (PCE_JOY_RUN | PCE_JOY_I)) start_game();
760
+ continue;
340
761
  }
341
- for (i = 0; i < MAX_ENEMIES; ++i) {
342
- u16 ey = enemies[i].alive ? enemies[i].y : 0x1F0;
343
- set_sprite((u8)(7 + i), enemies[i].x, ey, ENEMY_VRAM >> 6, 2);
762
+ if (state == ST_OVER) {
763
+ /* freeze the final frame; RUN (or I) returns to the title */
764
+ if (newpad & (PCE_JOY_RUN | PCE_JOY_I)) {
765
+ state = ST_TITLE;
766
+ paint_title();
767
+ music_set(ST_TITLE);
768
+ }
769
+ continue;
344
770
  }
345
- satb_dma();
771
+ update_play();
346
772
  }
347
773
  }