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,167 +1,785 @@
1
- /* ── platformer.c — Genesis SGDK side-scrolling platformer scaffold
1
+ /* ── platformer.c — Genesis side-scrolling platformer (complete example game)
2
2
  *
3
- * A HORIZONTALLY SCROLLING platformer: the world is wider than the
4
- * screen, a camera follows the player, Plane A scrolls with the world
5
- * and Plane B scrolls at half rate for parallax depth. Gravity, jump,
6
- * and land-on-top collision against a static platform list (in WORLD
7
- * coords). Yours to extend with enemies, goals, pickups, etc.
3
+ * CINDER SPRINT a COMPLETE, working game: title screen, 1P mode and 2P
4
+ * ALTERNATING-TURNS mode (arcade-classic: players swap on death; each player
5
+ * has their own score and own 3 lives; player 2 plays on CONTROLLER 2),
6
+ * coins + distance scoring, persistent hi-score (cartridge SRAM), music +
7
+ * SFX, and the Genesis's signature feature: DUAL-PLANE PARALLAX with
8
+ * per-strip (cell) horizontal scroll — a dusk mountain ridge that slides at
9
+ * HALF the foreground speed, under a hardware-fixed WINDOW-plane HUD.
8
10
  *
9
- * Physics is fixed-point: x/y are in 1/16-pixel subpixel units. The
10
- * player draws at SCREEN x = (worldX>>4) - camX.
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 Genesis footgun;
14
+ * reshape your gameplay around it (see TROUBLESHOOTING before changing).
15
+ * GAME LOGIC (clay) — level layout, physics tuning, scoring, art: reshape
16
+ * freely.
11
17
  *
12
- * World width here is one VDP plane (64 cells = 512 px) so the whole
13
- * level fits in the scroll plane and plain VDP_setHorizontalScroll moves
14
- * it a real working scroller with no streaming. For a world WIDER than
15
- * 512 px you additionally stream the column entering view each time camX
16
- * crosses an 8-px boundary (see MENTAL_MODEL.md "How Sonic-style large
17
- * maps REALLY work"). For a from-scratch smooth-scroll + parallax
18
- * starting point with NO per-frame tilemap writes, see the
19
- * two_plane_parallax template + MENTAL_MODEL.md "Scrolling, parallax &
20
- * the feel trap".
18
+ * What depends on what:
19
+ * genesis_sfx.{h,c} PSG sound wrapper (tones + noise + a background
20
+ * melody loop). For full FM music, see the xgm2_demo template
21
+ * (XGM2_loadDriver + XGM2_play + a .xgc blob incbin'd via a data.s
22
+ * sibling) we use the PSG path here so the platformer stays a
23
+ * single-file game; the swap is three lines plus the data.s sibling.
24
+ * rom_header.c (SGDK) the Sega header at $100. Its 'RA' block at $1B0
25
+ * DECLARES the cartridge SRAM that hiscore_load/save below depend on
26
+ * (see the SRAM idiom). The build assembles it automatically.
27
+ *
28
+ * The level: a 512-px-wide COLUMN MAP (ground height + one-way slabs + pits)
29
+ * painted once into plane A. The plane is exactly 512 px (64 cells) wide and
30
+ * the VDP scroll WRAPS within the plane, so a forever-incrementing camera
31
+ * loops the level seamlessly — an endless run of pits, slabs, coins and
32
+ * spikes with ZERO tilemap writes per frame (hardware scroll is free;
33
+ * rewriting tilemaps in the loop is the #1 "choppy movement" bug).
34
+ *
35
+ * Frame budget (NTSC, 60 fps): player physics + a two-column ground probe +
36
+ * (3 coins + 2 spikes) of AABB + 56 hscroll words + 6 SAT entries queued for
37
+ * vblank DMA — a tiny fraction of the 68000's frame. The vblank DMA budget
38
+ * (~7 KB/frame in H40) is the real ceiling on Genesis; we use < 200 bytes.
21
39
  */
22
40
 
23
41
  #include <genesis.h>
24
42
  #include "genesis_sfx.h"
25
43
 
26
- #define T_BLANK (TILE_USER_INDEX + 0)
27
- #define T_PLATFORM (TILE_USER_INDEX + 1)
28
- #define T_PLAYER (TILE_USER_INDEX + 2)
29
- #define T_BG (TILE_USER_INDEX + 3)
44
+ /* The title screen renders this — examples({op:'fork'}) stamps your game's
45
+ * name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
46
+ #define GAME_TITLE "CINDER SPRINT"
47
+
48
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
49
+ * CONTROLLER MAPPING — two layers, both bite:
50
+ *
51
+ * On the pad: SGDK's JOY_readJoypad(JOY_1/JOY_2) returns BUTTON_A/B/C/
52
+ * START/UP/DOWN/LEFT/RIGHT as a bitmask. Jump is BUTTON_A or BUTTON_C
53
+ * (real Genesis games map action buttons generously — thumbs rest on C).
54
+ *
55
+ * Driving this game HEADLESSLY through an emulator (libretro/gpgx): the
56
+ * core maps Genesis A/B/C onto libretro Y/B/A. So setInput({y:true})
57
+ * presses GENESIS A (jump/start here), setInput({b:true}) presses GENESIS
58
+ * B (2P select), and setInput({a:true}) presses GENESIS C — NOT Genesis A.
59
+ * Getting this wrong looks like "the game ignores input". START is start.
60
+ */
61
+ #define BTN_JUMP (BUTTON_A | BUTTON_C)
62
+
63
+ /* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
64
+ * Tile art. Genesis tiles are 4bpp: each u32 row = 8 pixels, one hex nibble
65
+ * per pixel = a colour index into the tile's palette line (0 = transparent).
66
+ * Sprites use PAL0, plane A (world) PAL1, plane B (backdrop) PAL2. */
67
+ #define T_GRASS (TILE_USER_INDEX + 0) /* plane A: ground surface */
68
+ #define T_DIRT (TILE_USER_INDEX + 1) /* plane A: ground body */
69
+ #define T_SLAB (TILE_USER_INDEX + 2) /* plane A: one-way platform */
70
+ #define T_SKY (TILE_USER_INDEX + 3) /* plane B: dusk sky */
71
+ #define T_CLOUD (TILE_USER_INDEX + 4) /* plane B: drifting cloud */
72
+ #define T_PEAK (TILE_USER_INDEX + 5) /* plane B: mountain ridge tip */
73
+ #define T_MOUNT (TILE_USER_INDEX + 6) /* plane B: mountain body */
74
+ #define T_HUDBAND (TILE_USER_INDEX + 7) /* plane B: flat band behind HUD */
75
+ #define T_PLAYER (TILE_USER_INDEX + 8) /* sprite: idle */
76
+ #define T_JUMP (TILE_USER_INDEX + 9) /* sprite: airborne */
77
+ #define T_COIN (TILE_USER_INDEX + 10) /* sprite: pickup */
78
+ #define T_SPIKE (TILE_USER_INDEX + 11) /* sprite: hazard */
30
79
 
31
- static const u32 tile_blank[8] = { 0,0,0,0,0,0,0,0 };
32
- static const u32 tile_platform[8] = {
33
- 0x11111111, 0x12222221, 0x12222221, 0x12222221,
34
- 0x12222221, 0x12222221, 0x12222221, 0x11111111,
80
+ static const u32 tile_grass[8] = { /* grass lip over speckled dirt */
81
+ 0x11111111, 0x11111111, 0x22222222, 0x22322222,
82
+ 0x22222232, 0x22222222, 0x23222222, 0x22222322,
83
+ };
84
+ static const u32 tile_dirt[8] = { /* speckles make motion visible */
85
+ 0x22222222, 0x22232222, 0x22222222, 0x32222223,
86
+ 0x22222222, 0x22223222, 0x22222222, 0x23222222,
87
+ };
88
+ static const u32 tile_slab[8] = { /* thin one-way platform (top */
89
+ 0x44444444, 0x45555554, 0x55555555, /* half only — jump up through */
90
+ 0x05555550, 0x00000000, 0x00000000, /* the transparent bottom) */
91
+ 0x00000000, 0x00000000,
92
+ };
93
+ static const u32 tile_sky[8] = {
94
+ 0x11111111, 0x11111111, 0x11111111, 0x11111111,
95
+ 0x11111111, 0x11111111, 0x11111111, 0x11111111,
96
+ };
97
+ static const u32 tile_cloud[8] = {
98
+ 0x11111111, 0x11222111, 0x12222221, 0x22222222,
99
+ 0x12222221, 0x11111111, 0x11111111, 0x11111111,
100
+ };
101
+ static const u32 tile_peak[8] = { /* ridge tip: triangle over sky */
102
+ 0x11133111, 0x11333311, 0x13333331, 0x33333333,
103
+ 0x33433333, 0x33333333, 0x33333433, 0x33333333,
104
+ };
105
+ static const u32 tile_mount[8] = { /* body speckled for parallax */
106
+ 0x33333333, 0x33343333, 0x33333333, /* visibility — a flat colour */
107
+ 0x33333343, 0x43333333, 0x33333333, /* shifted N px looks identical */
108
+ 0x33334333, 0x33333333, /* to itself (motion invisible) */
109
+ };
110
+ static const u32 tile_hudband[8] = {
111
+ 0x55555555, 0x55555555, 0x55555555, 0x55555555,
112
+ 0x55555555, 0x55555555, 0x55555555, 0x55555555,
35
113
  };
36
- static const u32 tile_player[8] = {
37
- 0x00033000, 0x00333300, 0x03333330, 0x03333330,
38
- 0x03333330, 0x03333330, 0x00333300, 0x00033000,
114
+ static const u32 tile_player[8] = { /* ember sprite, idle */
115
+ 0x00222200, 0x02222220, 0x22322322, 0x22222222,
116
+ 0x22222222, 0x02222220, 0x02200220, 0x02200220,
39
117
  };
40
- /* A simple background block for the parallax plane. */
41
- static const u32 tile_bg[8] = {
42
- 0x44444444, 0x40000004, 0x40000004, 0x40000004,
43
- 0x40000004, 0x40000004, 0x40000004, 0x44444444,
118
+ static const u32 tile_jump[8] = { /* arms up, legs tucked */
119
+ 0x22000022, 0x22222222, 0x02322320, 0x02222220,
120
+ 0x02222220, 0x00222200, 0x02000020, 0x20000002,
121
+ };
122
+ static const u32 tile_coin[8] = {
123
+ 0x00444400, 0x04555540, 0x45444454, 0x45444454,
124
+ 0x45444454, 0x45444454, 0x04555540, 0x00444400,
125
+ };
126
+ static const u32 tile_spike[8] = {
127
+ 0x00000000, 0x00077000, 0x00077000, 0x00766700,
128
+ 0x00766700, 0x07666670, 0x07666670, 0x76666667,
44
129
  };
45
130
 
46
- #define WORLD_W 512 /* one 64-cell plane wide */
47
- #define SCREEN_W 320
48
-
49
- typedef struct { s16 x, y, w, h; } Rect;
50
-
51
- /* Static platforms in WORLD PIXEL coords (not subpixel), spread across
52
- * the full 512-px world so there's somewhere to scroll to. */
53
- static const Rect platforms[] = {
54
- { 0, 200, 512, 24 }, /* floor spans the whole world */
55
- { 40, 160, 64, 8 },
56
- { 150, 140, 72, 8 },
57
- { 260, 120, 56, 8 },
58
- { 360, 96, 64, 8 },
59
- { 440, 150, 56, 8 },
60
- { 200, 80, 48, 8 },
131
+ /* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
132
+ * The level — a 64-column map; world x = (screen x + camera) mod 512.
133
+ * ground_row[c] — plane row of the grass top, 0xFF = pit.
134
+ * plat_row[c] — row of a one-way slab, 0 = none.
135
+ * Rows are PLANE rows (y = row*8). Rows 0-1 sit under the HUD window;
136
+ * playfield rows are 2..27 (the visible 224-px screen is 28 rows). */
137
+ #define NO_GROUND 0xFF
138
+ #define GROUND_R 24 /* default ground surface row */
139
+ static const u8 ground_row[64] = {
140
+ 24, 24, 24, 24, 24, 24, 24, 24, /* start runway */
141
+ 24, 24, 24, 24, 24, 24, 24, 24,
142
+ 24, 24, 24, 24, NO_GROUND, NO_GROUND, NO_GROUND, /* pit 1 (24 px) */
143
+ 24, 24, 24, 24, 24, 24, 24, 24, 24,
144
+ 24, 24, NO_GROUND, NO_GROUND, 24, 24, 24, /* pit 2 (16 px) */
145
+ 24, 24, 24, 24, 24, 24, 24, 24,
146
+ NO_GROUND, NO_GROUND, NO_GROUND, 24, 24, 24, /* pit 3 (24 px) */
147
+ 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
148
+ };
149
+ static const u8 plat_row[64] = {
150
+ 0, 0, 0, 0, 0, 0, 21, 21, /* warm-up slab */
151
+ 21, 0, 0, 0, 0, 0, 0, 0,
152
+ 0, 19, 19, 19, 19, 0, 0, 0, /* bridge over pit 1*/
153
+ 0, 0, 0, 21, 21, 0, 0, 0,
154
+ 0, 0, 19, 19, 0, 0, 0, 0, /* hop over pit 2 */
155
+ 0, 21, 21, 0, 0, 17, 17, 17, /* stairs up... */
156
+ 0, 0, 19, 19, 0, 0, 0, 0, /* ...and over pit 3*/
157
+ 0, 0, 0, 0, 21, 21, 21, 0,
158
+ };
159
+ /* Mountain ridge silhouette for plane B — 16-column period (128 px), so a
160
+ * half-speed shift is unambiguous to the eye (and to a headless pixel
161
+ * probe). Values are the plane row of each column's ridge tip. */
162
+ static const u8 ridge_top[16] = {
163
+ 13, 12, 11, 10, 10, 11, 12, 13, 14, 13, 11, 10, 11, 12, 13, 14,
61
164
  };
62
- #define N_PLATFORMS (sizeof(platforms) / sizeof(platforms[0]))
63
165
 
64
- static bool on_platform(s16 px, s16 py) {
65
- for (u16 i = 0; i < N_PLATFORMS; i++) {
66
- const Rect* p = &platforms[i];
67
- if (py + 8 == p->y && px + 8 > p->x && px < p->x + p->w) return TRUE;
166
+ /* ── GAME LOGIC (clay) physics + tuning (Q4.4 fixed point: 16 = 1 px) ── */
167
+ #define GRAVITY_Q44 1 /* +1/16 px per frame per frame */
168
+ #define JUMP_VEL_Q44 (-40) /* launch vy → ~50 px apex (~6 tile rows) */
169
+ #define MAX_VY_Q44 80 /* terminal 5 px/frame MUST stay under 6: *
170
+ * the landing probe's 6-px window can't *
171
+ * catch a faster fall (tunnelling) */
172
+ #define MOVE_SPEED 2 /* px/frame walk + scroll speed */
173
+ #define SCROLL_WALL 144 /* px: past this the world scrolls, not you */
174
+ #define GROUND_TOP 192 /* GROUND_R * 8 */
175
+ #define SPIKE_Y 184 /* spikes stand on the ground */
176
+ #define SCREEN_W 320 /* H40 mode */
177
+ #define NUM_COINS 3
178
+ #define NUM_SPIKES 2
179
+ #define START_LIVES 3
180
+ #define HUD_ROWS 2 /* window rows reserved for the HUD */
181
+
182
+ static s16 px; /* player screen x */
183
+ static u16 py_q44; /* player y, Q4.4 — gravity adds <1 px/frame *
184
+ * near the apex; integer y would stick */
185
+ static s16 vy_q44;
186
+ static u8 on_ground;
187
+ static u16 cam; /* camera/world scroll. NEVER reset mid-run *
188
+ * and never wrapped by hand: the plane is *
189
+ * 512 px and the VDP masks scroll values to *
190
+ * the plane, and 65536 is a multiple of 512 *
191
+ * (and of 512*2 and 512*8), so plain u16 *
192
+ * overflow keeps BOTH parallax layers *
193
+ * seamless forever. */
194
+ static u8 dist_sub; /* sub-counter: 64 px scrolled = +1 point */
195
+ static s16 coin_x[NUM_COINS];
196
+ static s16 coin_y[NUM_COINS];
197
+ static s16 spike_x[NUM_SPIKES];
198
+ static u8 spike_active[NUM_SPIKES];
199
+
200
+ /* Players: index 0 = P1 (controller 1), 1 = P2 (controller 2 — alternating
201
+ * turns, arcade-classic style). Each has own score + own lives; the HUD
202
+ * shows the CURRENT player's numbers. */
203
+ static u8 two_player;
204
+ static u8 cur_player;
205
+ static u8 p_lives[2];
206
+ static u16 p_score[2];
207
+ static u16 hiscore;
208
+ static u8 turn_pause; /* freeze frames after a turn change */
209
+ static u16 rng = 0xC0DE;
210
+
211
+ /* Game states — the shell every example shares: title → play → game over. */
212
+ #define ST_TITLE 0
213
+ #define ST_PLAY 1
214
+ #define ST_OVER 2
215
+ static u8 state;
216
+ static u16 prev_pad;
217
+
218
+ /* ── GAME LOGIC (clay) — xorshift16 PRNG (a few 68k instructions) ── */
219
+ static u8 random8(void) {
220
+ u16 r = rng;
221
+ r ^= r << 7;
222
+ r ^= r >> 9;
223
+ r ^= r << 8;
224
+ rng = r;
225
+ return (u8)r;
226
+ }
227
+
228
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
229
+ * CARTRIDGE SRAM — the Genesis battery-save mechanism, three parts:
230
+ *
231
+ * 1. The ROM HEADER declares it: bytes $1B0.. hold 'R','A', a type word
232
+ * ($F820 = battery-backed, byte-wide on ODD addresses — the classic
233
+ * cart wiring), then start/end addresses $200000/$20FFFF. SGDK's
234
+ * rom_header.c (assembled into every build) already declares exactly
235
+ * this — no linker work needed. Emulators allocate the save RAM by
236
+ * READING THIS HEADER; no 'RA' block = writes to $200000+ go nowhere.
237
+ * 2. The MAPPER GATE: writing 1 to $A130F1 banks SRAM into $200000+,
238
+ * 0 banks the ROM back in. SGDK's SRAM_enable()/SRAM_disable() do
239
+ * this. ALWAYS disable after access — on carts >2 MB the SRAM window
240
+ * shadows ROM, and leaving it enabled corrupts later ROM fetches.
241
+ * 3. ODD-BYTE ADDRESSING: SRAM_readByte/writeByte(offset) access 68k
242
+ * address $200001 + offset*2. Headlessly, the emulator's save_ram
243
+ * region interleaves with dead even bytes: SGDK offset k lives at
244
+ * save_ram[k*2 + 1] (the even bytes read back $FF).
245
+ *
246
+ * Hi-score record layout (SGDK offsets): 0='H' 1='S' 2=lo 3=hi
247
+ * 4=checksum(lo^hi^$A5). Fresh SRAM is all $FF — the magic+checksum
248
+ * rejects it (and any corruption) so first boot shows 0, not 65535.
249
+ *
250
+ * Emulator note (verified against gpgx): the core sizes its save_ram
251
+ * region by scanning for the last non-$FF byte, so the region reads as
252
+ * EMPTY until the first write below lands — that's why hiscore_init runs
253
+ * at the very top of main(). Real hardware and .srm-restoring frontends
254
+ * have no such wrinkle. */
255
+ static u16 hiscore_load(void) {
256
+ u8 m0, m1, lo, hi, ck;
257
+ SRAM_enableRO();
258
+ m0 = SRAM_readByte(0);
259
+ m1 = SRAM_readByte(1);
260
+ lo = SRAM_readByte(2);
261
+ hi = SRAM_readByte(3);
262
+ ck = SRAM_readByte(4);
263
+ SRAM_disable();
264
+ if (m0 == 'H' && m1 == 'S' && ck == (u8)(lo ^ hi ^ 0xA5))
265
+ return ((u16)hi << 8) | lo;
266
+ return 0;
267
+ }
268
+
269
+ static void hiscore_save(u16 sc) {
270
+ u8 lo = (u8)sc, hi = (u8)(sc >> 8);
271
+ SRAM_enable();
272
+ SRAM_writeByte(0, 'H');
273
+ SRAM_writeByte(1, 'S');
274
+ SRAM_writeByte(2, lo);
275
+ SRAM_writeByte(3, hi);
276
+ SRAM_writeByte(4, (u8)(lo ^ hi ^ 0xA5));
277
+ SRAM_disable();
278
+ }
279
+
280
+ /* Format-on-first-boot: if the magic is absent (fresh battery), write a
281
+ * valid zero record immediately so the save file exists from frame one. */
282
+ static void hiscore_init(void) {
283
+ hiscore = hiscore_load();
284
+ if (hiscore == 0) hiscore_save(0);
285
+ }
286
+
287
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
288
+ * DUAL-PLANE PARALLAX + per-strip scroll — THE Genesis signature. The VDP
289
+ * composites two independent tilemap planes (A above B), each with its own
290
+ * horizontal scroll, and the scroll can vary DOWN THE SCREEN:
291
+ *
292
+ * VDP_setScrollingMode(HSCROLL_TILE, VSCROLL_PLANE) tells the VDP to
293
+ * fetch one hscroll entry per 8-LINE STRIP from the hscroll table in
294
+ * VRAM (28 strips cover the 224-line screen) instead of one per frame.
295
+ * Each strip entry is a pair of words: plane A's offset, then plane B's.
296
+ *
297
+ * Per frame we queue two 28-word tables:
298
+ * plane A: every strip = -cam (the world, 1:1 with the camera)
299
+ * plane B: sky strips = -(cam / 8) (far: barely moves)
300
+ * ridge strips= -(cam / 2) (the half-speed mountain layer)
301
+ * Three speeds from two planes — banding ONE plane by strip is how real
302
+ * carts faked 3+ layers. POSITIVE camera = NEGATIVE scroll value (the
303
+ * scroll offset slides the plane right; we want the world to slide left).
304
+ *
305
+ * DELUXE VARIANT (not used here, same table): HSCROLL_LINE gives one entry
306
+ * per SCANLINE — 224 s16s per plane. Fill them with smooth per-line speeds
307
+ * for a sky gradient, or add sin(line+frame) ripple inside a water band
308
+ * (VDP_setHorizontalScrollLine). Costs ~1.8 KB/frame of vblank DMA versus
309
+ * our 224 bytes, so budget it (H40 vblank fits ~7 KB).
310
+ *
311
+ * Requires: HSCROLL_TILE mode set BEFORE the first table write; BOTH
312
+ * tables queued every frame you move the camera (a stale plane-A table
313
+ * shears the world); DMA_QUEUE so the VRAM writes land in vblank, never
314
+ * mid-frame (SYS_doVBlankProcess flushes the queue — mid-frame writes
315
+ * tear the strip boundary); the value arrays static (the queue reads
316
+ * them AT FLUSH TIME — stack arrays are gone by then, shipping garbage).
317
+ * Plane-size note: A and B share ONE size setting (default 64x32 cells =
318
+ * 512x256 px). You can't size them independently. */
319
+ #define SKY_STRIPS 10 /* strips 0-9 = HUD band + sky */
320
+ static s16 hsA[28];
321
+ static s16 hsB[28];
322
+ static void apply_camera(void) {
323
+ u16 i;
324
+ for (i = 0; i < 28; i++) {
325
+ hsA[i] = -(s16)cam;
326
+ hsB[i] = (i < SKY_STRIPS) ? -(s16)(cam >> 3) : -(s16)(cam >> 1);
327
+ }
328
+ VDP_setHorizontalScrollTile(BG_A, 0, hsA, 28, DMA_QUEUE);
329
+ VDP_setHorizontalScrollTile(BG_B, 0, hsB, 28, DMA_QUEUE);
330
+ }
331
+
332
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
333
+ * WINDOW-PLANE HUD — the fixed status bar. The window is a third tilemap
334
+ * that REPLACES plane A wherever it's shown and IGNORES ALL SCROLLING —
335
+ * a hardware-fixed HUD with zero per-frame cost. (The NES needs a sprite-0
336
+ * raster trick for this; on Genesis it's one register.)
337
+ * VDP_setWindowOnTop(2) shows it on the top 2 cell rows; text goes in with
338
+ * VDP_drawTextBG(WINDOW, ...). Two footguns:
339
+ * - The window only lives at screen edges (top/bottom N rows or left/
340
+ * right N columns) — it cannot float mid-screen.
341
+ * - It replaces plane A ONLY: plane B and sprites still render behind/
342
+ * over it. We paint plane B's top rows with a flat dark band so HUD
343
+ * text always reads, and nothing in the game flies above y=16. */
344
+ static void hud_init(void) {
345
+ VDP_setWindowOnTop(HUD_ROWS);
346
+ }
347
+
348
+ /* ── GAME LOGIC (clay) — HUD text (window plane, redrawn only on change) ── */
349
+ static void draw_u16(VDPPlane plane, u16 v, u16 x, u16 y) {
350
+ char buf[8];
351
+ uintToStr(v, buf, 5);
352
+ VDP_drawTextBG(plane, buf, x, y);
353
+ }
354
+
355
+ static void draw_hud(void) {
356
+ char b[4];
357
+ b[0] = 'P'; b[1] = '1' + cur_player; b[2] = 0;
358
+ VDP_drawTextBG(WINDOW, b, 1, 0);
359
+ b[0] = 'x'; b[1] = '0' + p_lives[cur_player]; b[2] = 0;
360
+ VDP_drawTextBG(WINDOW, b, 4, 0);
361
+ VDP_drawTextBG(WINDOW, "SC", 8, 0);
362
+ draw_u16(WINDOW, p_score[cur_player], 11, 0);
363
+ VDP_drawTextBG(WINDOW, "HI", 18, 0);
364
+ draw_u16(WINDOW, hiscore, 21, 0);
365
+ }
366
+
367
+ static void draw_hud_title(void) {
368
+ VDP_clearTextAreaBG(WINDOW, 0, 0, 40, HUD_ROWS);
369
+ VDP_drawTextBG(WINDOW, "HI", 18, 0);
370
+ draw_u16(WINDOW, hiscore, 21, 0);
371
+ }
372
+
373
+ /* ── GAME LOGIC (clay) — paint the two planes ───────────────────────────────
374
+ * Plane B (backdrop) is painted ONCE at boot and never touched again.
375
+ * Plane A is repainted on state changes only (title text ↔ the level).
376
+ * NOTHING repaints inside the frame loop — scroll is hardware. */
377
+ static void paint_backdrop(void) {
378
+ u16 c, r;
379
+ /* Flat dark band behind the window HUD (rows 0-1). */
380
+ VDP_fillTileMapRect(BG_B, TILE_ATTR_FULL(PAL2, 0, 0, 0, T_HUDBAND),
381
+ 0, 0, 64, HUD_ROWS);
382
+ for (c = 0; c < 64; c++) {
383
+ u16 top = ridge_top[c & 15];
384
+ /* Dusk sky with deterministic clouds (PRNG would repaint different
385
+ * art after a console reset — fine, but determinism helps tests). */
386
+ for (r = HUD_ROWS; r < top; r++) {
387
+ u16 t = (((r * 5 + c * 11) & 31) == 0 && r >= 3) ? T_CLOUD : T_SKY;
388
+ VDP_setTileMapXY(BG_B, TILE_ATTR_FULL(PAL2, 0, 0, 0, t), c, r);
389
+ }
390
+ VDP_setTileMapXY(BG_B, TILE_ATTR_FULL(PAL2, 0, 0, 0, T_PEAK), c, top);
391
+ VDP_fillTileMapRect(BG_B, TILE_ATTR_FULL(PAL2, 0, 0, 0, T_MOUNT),
392
+ c, top + 1, 1, 32 - (top + 1));
393
+ }
394
+ }
395
+
396
+ static void paint_level(void) {
397
+ u16 c;
398
+ VDP_clearPlane(BG_A, TRUE);
399
+ for (c = 0; c < 64; c++) {
400
+ u8 g = ground_row[c];
401
+ if (g != NO_GROUND) {
402
+ VDP_setTileMapXY(BG_A, TILE_ATTR_FULL(PAL1, 0, 0, 0, T_GRASS), c, g);
403
+ VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL1, 0, 0, 0, T_DIRT),
404
+ c, g + 1, 1, 32 - (g + 1));
405
+ }
406
+ if (plat_row[c])
407
+ VDP_setTileMapXY(BG_A, TILE_ATTR_FULL(PAL1, 0, 0, 0, T_SLAB),
408
+ c, plat_row[c]);
68
409
  }
69
- return FALSE;
410
+ }
411
+
412
+ /* ── GAME LOGIC (clay) — the title screen (text on plane A, scroll 0) ── */
413
+ static void paint_title(void) {
414
+ VDP_clearPlane(BG_A, TRUE);
415
+ VDP_drawTextBG(BG_A, GAME_TITLE, (40 - (sizeof(GAME_TITLE) - 1)) / 2, 8);
416
+ VDP_drawTextBG(BG_A, "1P START - A", 14, 14);
417
+ VDP_drawTextBG(BG_A, "2P TURNS - B", 14, 16);
418
+ VDP_drawTextBG(BG_A, "JUMP THE PITS - GRAB THE COINS", 5, 21);
419
+ draw_hud_title();
420
+ }
421
+
422
+ /* ── GAME LOGIC (clay) — the game-over results screen ── */
423
+ static void paint_over(void) {
424
+ VDP_clearPlane(BG_A, TRUE);
425
+ VDP_drawTextBG(BG_A, "GAME OVER", 15, 8);
426
+ VDP_drawTextBG(BG_A, "P1", 13, 12);
427
+ draw_u16(BG_A, p_score[0], 17, 12);
428
+ if (two_player) {
429
+ VDP_drawTextBG(BG_A, "P2", 13, 14);
430
+ draw_u16(BG_A, p_score[1], 17, 14);
431
+ }
432
+ VDP_drawTextBG(BG_A, "HI", 13, 17);
433
+ draw_u16(BG_A, hiscore, 17, 17);
434
+ VDP_drawTextBG(BG_A, "START - TITLE", 13, 21);
435
+ }
436
+
437
+ /* ── GAME LOGIC (clay) — coins + spikes (sprite objects in the world) ── */
438
+ static const s16 coin_heights[4] = { 168, 144, 120, 152 };
439
+ static void respawn_coin(u16 i) {
440
+ coin_x[i] = SCREEN_W + 8 + (random8() & 31); /* enter at the right */
441
+ coin_y[i] = coin_heights[random8() & 3];
442
+ }
443
+
444
+ static void try_spawn_spike(u16 i) {
445
+ /* Anchor only over ground: an inactive spike rolls a low per-frame
446
+ * chance, and only spawns if the level column entering at the right
447
+ * edge has ground under it (never floats over a pit). */
448
+ u16 c = ((u16)(SCREEN_W + 8 + cam) >> 3) & 63;
449
+ if (ground_row[c] == NO_GROUND) return;
450
+ if (random8() > 4) return;
451
+ spike_x[i] = SCREEN_W + 8;
452
+ spike_active[i] = 1;
453
+ }
454
+
455
+ /* ── GAME LOGIC (clay) — start a turn / a run ── */
456
+ static void begin_turn(void) {
457
+ u16 i;
458
+ px = 24;
459
+ py_q44 = (u16)(GROUND_TOP - 8) << 4;
460
+ vy_q44 = 0;
461
+ on_ground = 1;
462
+ cam = 0;
463
+ dist_sub = 0;
464
+ coin_x[0] = 120; coin_y[0] = 168;
465
+ coin_x[1] = 200; coin_y[1] = 144;
466
+ coin_x[2] = 280; coin_y[2] = 120;
467
+ for (i = 0; i < NUM_SPIKES; i++) spike_active[i] = 0;
468
+ spike_x[0] = 232; spike_active[0] = 1; /* runway columns — always */
469
+ spike_x[1] = 304; spike_active[1] = 1; /* ground at cam 0 */
470
+ turn_pause = 48; /* "P1/P2 ready" breather */
471
+ prev_pad = 0xFFFF; /* swallow held buttons across*
472
+ * the turn change */
473
+ apply_camera();
474
+ draw_hud();
475
+ }
476
+
477
+ static void start_game(u8 players) {
478
+ two_player = players;
479
+ cur_player = 0;
480
+ p_score[0] = p_score[1] = 0;
481
+ p_lives[0] = START_LIVES;
482
+ p_lives[1] = players ? START_LIVES : 0;
483
+ paint_level();
484
+ begin_turn();
485
+ sfx_tone(0, 523, 10); /* start jingle (C5) */
486
+ state = ST_PLAY;
487
+ }
488
+
489
+ static void game_over(void) {
490
+ u16 best = p_score[0];
491
+ if (two_player && p_score[1] > best) best = p_score[1];
492
+ if (best > hiscore) {
493
+ hiscore = best;
494
+ hiscore_save(hiscore); /* battery SRAM — see the SRAM idiom */
495
+ }
496
+ state = ST_OVER;
497
+ cam = 0;
498
+ apply_camera();
499
+ draw_hud(); /* refresh the window HUD — HI may have just changed */
500
+ paint_over();
501
+ }
502
+
503
+ /* ── GAME LOGIC (clay) — death + alternating-turn handoff ── */
504
+ static void kill_player(void) {
505
+ u8 other;
506
+ sfx_noise(14);
507
+ if (p_lives[cur_player] > 0) --p_lives[cur_player];
508
+ if (two_player) {
509
+ other = cur_player ^ 1;
510
+ if (p_lives[other] > 0) cur_player = other; /* swap turns */
511
+ else if (p_lives[cur_player] == 0) { game_over(); return; }
512
+ } else if (p_lives[0] == 0) {
513
+ game_over();
514
+ return;
515
+ }
516
+ begin_turn();
517
+ }
518
+
519
+ /* ── GAME LOGIC (clay) — landing probe against the column map ──────────────
520
+ * One-way platforms, arcade-classic style: only catch the player while
521
+ * FALLING through a narrow window at the surface: top-1 (the standing snap
522
+ * parks feet exactly at top, and gravity's sub-pixel trickle doesn't move
523
+ * the integer y every frame — without the -1 slack the player "stands"
524
+ * with on_ground=0 most frames and jumps only register on lucky frames)
525
+ * through top+4 (so a 5 px/frame terminal fall can't step over it). */
526
+ static s16 land_top(u16 c, s16 feet) {
527
+ u8 r;
528
+ s16 top;
529
+ r = plat_row[c];
530
+ if (r) {
531
+ top = (s16)r << 3;
532
+ if (feet + 1 >= top && feet <= top + 4) return top;
533
+ }
534
+ r = ground_row[c];
535
+ if (r != NO_GROUND) {
536
+ top = (s16)r << 3;
537
+ if (feet + 1 >= top && feet <= top + 4) return top;
538
+ }
539
+ return 0;
540
+ }
541
+
542
+ /* ── GAME LOGIC (clay) — stage this frame's sprites ─────────────────────────
543
+ * Fixed SAT slots: 0 = player, 1-3 = coins, 4-5 = spikes. Hidden sprites
544
+ * park at y = -16 (above the screen). NEVER hide with x = -128..0 — a SAT
545
+ * x of 0 is the VDP's sprite-masking trigger and silently blanks every
546
+ * lower-priority sprite on those scanlines. */
547
+ #define HIDE_Y (-16)
548
+ static void stage_sprites(void) {
549
+ u16 i;
550
+ s16 player_y = (s16)(py_q44 >> 4);
551
+ if (state != ST_PLAY) /* no actors off the field */
552
+ VDP_setSprite(0, px, HIDE_Y, SPRITE_SIZE(1, 1),
553
+ TILE_ATTR_FULL(PAL0, 1, 0, 0, T_PLAYER));
554
+ else if (turn_pause == 0 || (turn_pause & 4)) /* blink on handoff */
555
+ VDP_setSprite(0, px, player_y, SPRITE_SIZE(1, 1),
556
+ TILE_ATTR_FULL(PAL0, 1, 0, 0, on_ground ? T_PLAYER : T_JUMP));
557
+ else
558
+ VDP_setSprite(0, px, HIDE_Y, SPRITE_SIZE(1, 1),
559
+ TILE_ATTR_FULL(PAL0, 1, 0, 0, T_PLAYER));
560
+ for (i = 0; i < NUM_COINS; i++) {
561
+ s16 cx = coin_x[i];
562
+ VDP_setSprite(1 + i, (state == ST_PLAY && cx < SCREEN_W) ? cx : (s16)SCREEN_W,
563
+ (state == ST_PLAY && cx < SCREEN_W) ? coin_y[i] : (s16)HIDE_Y,
564
+ SPRITE_SIZE(1, 1), TILE_ATTR_FULL(PAL0, 1, 0, 0, T_COIN));
565
+ }
566
+ for (i = 0; i < NUM_SPIKES; i++) {
567
+ u8 vis = (state == ST_PLAY) && spike_active[i] && spike_x[i] < SCREEN_W;
568
+ VDP_setSprite(4 + i, spike_x[i], vis ? (s16)SPIKE_Y : (s16)HIDE_Y,
569
+ SPRITE_SIZE(1, 1), TILE_ATTR_FULL(PAL0, 1, 0, 0, T_SPIKE));
570
+ }
571
+ /* ── HARDWARE IDIOM (load-bearing) — CHAIN the sprite list before
572
+ * uploading. VDP_setSprite does NOT set the SAT link byte, and link 0
573
+ * means "end of list": skip this and the VDP draws sprite 0 only.
574
+ * VDP_linkSprites(0, 6) links slots 0..5; the queued DMA flushes the
575
+ * 6 SAT entries during vblank. ── */
576
+ VDP_linkSprites(0, 6);
577
+ VDP_updateSprites(6, DMA_QUEUE);
70
578
  }
71
579
 
72
580
  int main(bool hard) {
581
+ u16 i, pad, fresh;
582
+ s16 delta, y, feet, top;
583
+ u16 c0, c1;
73
584
  (void)hard;
74
585
 
75
- PAL_setColor(0 + 2, 0x000E); /* player red */
76
- PAL_setColor(0 + 3, 0x00CC);
77
- PAL_setColor(16 + 1, 0x0888); /* platform light */
78
- PAL_setColor(16 + 2, 0x0666); /* platform dark */
79
- PAL_setColor(16 + 4, 0x0420); /* parallax bg */
80
-
81
- VDP_loadTileData(tile_blank, T_BLANK, 1, DMA);
82
- VDP_loadTileData(tile_platform, T_PLATFORM, 1, DMA);
83
- VDP_loadTileData(tile_player, T_PLAYER, 1, DMA);
84
- VDP_loadTileData(tile_bg, T_BG, 1, DMA);
85
-
86
- /* Plane A: the foreground world — platforms painted in world coords. */
87
- for (u16 i = 0; i < N_PLATFORMS; i++) {
88
- const Rect* p = &platforms[i];
89
- VDP_fillTileMapRect(BG_A,
90
- TILE_ATTR_FULL(PAL1, 0, 0, 0, T_PLATFORM),
91
- p->x >> 3, p->y >> 3, (p->w + 7) >> 3, (p->h + 7) >> 3);
92
- }
93
- /* Plane B: a sparse parallax backdrop — every 4th cell, upper area. */
94
- for (u16 cx = 0; cx < 64; cx += 4)
95
- for (u16 cy = 2; cy < 20; cy += 4)
96
- VDP_setTileMapXY(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, T_BG), cx, cy);
586
+ /* SRAM first before any VDP work. The save file then exists within
587
+ * the game's first frames of life, which is what lets a frontend (or
588
+ * a headless host) see a non-empty save_ram region as early as
589
+ * possible (see the SRAM idiom note on gpgx's size scan). */
590
+ hiscore_init();
97
591
 
98
- VDP_drawText("D-PAD MOVE A JUMP (scrolls)", 5, 1);
592
+ /* ── HARDWARE IDIOM (load-bearing see TROUBLESHOOTING) ──
593
+ * Init order: scrolling MODE before scroll VALUES, tiles + palettes
594
+ * before tilemaps that reference them, window size before window text.
595
+ * SGDK's boot already did the dangerous part (VDP regs, Z80, vblank
596
+ * int) — keep VDP_setScrollingMode FIRST here so every later
597
+ * apply_camera() writes the table layout the VDP is actually reading. */
598
+ VDP_setScrollingMode(HSCROLL_TILE, VSCROLL_PLANE);
599
+ hud_init();
99
600
 
100
- sfx_init();
601
+ /* Palettes: PAL0 sprites + HUD text, PAL1 plane A, PAL2 plane B.
602
+ * Colours are BGR, 3 bits per channel: 0x0BGR with E = full. */
603
+ PAL_setColor( 2, 0x028E); /* player ember orange */
604
+ PAL_setColor( 3, 0x0008); /* player eyes dark red */
605
+ PAL_setColor( 4, 0x02CE); /* coin gold */
606
+ PAL_setColor( 5, 0x008C); /* coin shading */
607
+ PAL_setColor( 6, 0x0888); /* spike steel */
608
+ PAL_setColor( 7, 0x0CCC); /* spike highlight */
609
+ PAL_setColor(15, 0x0EEE); /* font white (index 15 = SGDK font colour) */
610
+ PAL_setColor(16 + 1, 0x04A2); /* grass dusk green */
611
+ PAL_setColor(16 + 2, 0x0248); /* dirt brown */
612
+ PAL_setColor(16 + 3, 0x0136); /* dirt speckle */
613
+ PAL_setColor(16 + 4, 0x0666); /* slab stone */
614
+ PAL_setColor(16 + 5, 0x0AAA); /* slab lip */
615
+ PAL_setColor(32 + 1, 0x036C); /* dusk sky orange */
616
+ PAL_setColor(32 + 2, 0x08AE); /* cloud pink */
617
+ PAL_setColor(32 + 3, 0x0413); /* mountain dark purple */
618
+ PAL_setColor(32 + 4, 0x0635); /* mountain speckle */
619
+ PAL_setColor(32 + 5, 0x0202); /* HUD band near-black */
101
620
 
102
- s32 px = 32 << 4, py = 100 << 4; /* world subpixel coords */
103
- s32 vx = 0, vy = 0;
104
- s16 camX = 0;
621
+ VDP_loadTileData(tile_grass, T_GRASS, 1, DMA);
622
+ VDP_loadTileData(tile_dirt, T_DIRT, 1, DMA);
623
+ VDP_loadTileData(tile_slab, T_SLAB, 1, DMA);
624
+ VDP_loadTileData(tile_sky, T_SKY, 1, DMA);
625
+ VDP_loadTileData(tile_cloud, T_CLOUD, 1, DMA);
626
+ VDP_loadTileData(tile_peak, T_PEAK, 1, DMA);
627
+ VDP_loadTileData(tile_mount, T_MOUNT, 1, DMA);
628
+ VDP_loadTileData(tile_hudband, T_HUDBAND, 1, DMA);
629
+ VDP_loadTileData(tile_player, T_PLAYER, 1, DMA);
630
+ VDP_loadTileData(tile_jump, T_JUMP, 1, DMA);
631
+ VDP_loadTileData(tile_coin, T_COIN, 1, DMA);
632
+ VDP_loadTileData(tile_spike, T_SPIKE, 1, DMA);
105
633
 
106
- const s32 GRAVITY = 12, MOVE_SPEED = 24, JUMP_VEL = -200, MAX_FALL = 320;
107
- u16 prev = 0;
634
+ paint_backdrop(); /* plane B: painted once, scrolled forever */
635
+ sfx_init(); /* PSG: sfx channels + background melody */
636
+
637
+ state = ST_TITLE;
638
+ cam = 0;
639
+ apply_camera();
640
+ paint_title();
108
641
 
109
642
  while (TRUE) {
110
- u16 pad = JOY_readJoypad(JOY_1);
643
+ if (state == ST_TITLE) {
644
+ /* ── GAME LOGIC (clay) — title: A = 1P, B = 2P turns ──
645
+ * The camera drifts so the title sells the parallax: plane B
646
+ * slides at two speeds while the plane-A title text holds
647
+ * still (its strip values stay 0 — only B's get the drift). */
648
+ cam += 1;
649
+ for (i = 0; i < 28; i++) {
650
+ hsA[i] = 0;
651
+ hsB[i] = (i < SKY_STRIPS) ? -(s16)(cam >> 3) : -(s16)(cam >> 1);
652
+ }
653
+ VDP_setHorizontalScrollTile(BG_A, 0, hsA, 28, DMA_QUEUE);
654
+ VDP_setHorizontalScrollTile(BG_B, 0, hsB, 28, DMA_QUEUE);
655
+ stage_sprites();
656
+ pad = JOY_readJoypad(JOY_1);
657
+ fresh = pad & ~prev_pad;
658
+ if (fresh & (BUTTON_A | BUTTON_C | BUTTON_START)) start_game(0);
659
+ else if (fresh & BUTTON_B) start_game(1);
660
+ prev_pad = pad;
661
+ sfx_update();
662
+ SYS_doVBlankProcess();
663
+ continue;
664
+ }
111
665
 
112
- vx = 0;
113
- if (pad & BUTTON_LEFT) vx = -MOVE_SPEED;
114
- if (pad & BUTTON_RIGHT) vx = MOVE_SPEED;
666
+ if (state == ST_OVER) {
667
+ /* Results screen; START or A returns to the title. */
668
+ stage_sprites();
669
+ pad = JOY_readJoypad(JOY_1);
670
+ fresh = pad & ~prev_pad;
671
+ if (fresh & (BUTTON_START | BUTTON_A | BUTTON_C)) {
672
+ state = ST_TITLE;
673
+ cam = 0;
674
+ prev_pad = 0xFFFF; /* swallow the held START */
675
+ paint_title();
676
+ } else {
677
+ prev_pad = pad;
678
+ }
679
+ sfx_update();
680
+ SYS_doVBlankProcess();
681
+ continue;
682
+ }
683
+
684
+ /* ── ST_PLAY ──────────────────────────────────────────────────── */
685
+ stage_sprites();
686
+ apply_camera();
115
687
 
116
- s16 ipx = px >> 4, ipy = py >> 4;
117
- bool grounded = on_platform(ipx, ipy);
118
- if ((pad & BUTTON_A) && !(prev & BUTTON_A) && grounded) {
119
- vy = JUMP_VEL; sfx_tone(1, 300, 6);
688
+ if (turn_pause) { /* freeze gameplay, keep frames */
689
+ --turn_pause; /* honest (sprites/scroll staged) */
690
+ sfx_update();
691
+ SYS_doVBlankProcess();
692
+ continue;
120
693
  }
121
- prev = pad;
122
-
123
- vy += GRAVITY;
124
- if (vy > MAX_FALL) vy = MAX_FALL;
125
- if (grounded && vy > 0) vy = 0;
126
-
127
- /* Horizontal move, clamped to the world (no wrap now it scrolls). */
128
- px += vx;
129
- if (px < 0) px = 0;
130
- if (px > (WORLD_W - 8) << 4) px = (WORLD_W - 8) << 4;
131
-
132
- /* Vertical move with land-on-top snap. */
133
- s32 np = py + vy; s16 npy = np >> 4;
134
- bool landed = FALSE;
135
- if (vy > 0) {
136
- for (u16 i = 0; i < N_PLATFORMS; i++) {
137
- const Rect* p = &platforms[i];
138
- if (ipy + 8 <= p->y && npy + 8 >= p->y
139
- && ipx + 8 > p->x && ipx < p->x + p->w) {
140
- py = (p->y - 8) << 4; vy = 0; landed = TRUE;
141
- sfx_tone(2, 700, 3); break;
142
- }
143
- }
694
+
695
+ /* ── GAME LOGIC (clay) from here down ─────────────────────────────
696
+ * Input — the CURRENT player's controller (alternating turns: P2
697
+ * is on controller 2). Past SCROLL_WALL the world scrolls instead
698
+ * of the player (the camera never scrolls back the classic
699
+ * one-way runner camera). */
700
+ pad = JOY_readJoypad(cur_player ? JOY_2 : JOY_1);
701
+ delta = 0;
702
+ if (pad & BUTTON_RIGHT) {
703
+ if (px < SCROLL_WALL) px += MOVE_SPEED;
704
+ else { cam += MOVE_SPEED; delta = MOVE_SPEED; }
705
+ }
706
+ if ((pad & BUTTON_LEFT) && px > 8) px -= MOVE_SPEED;
707
+ if ((pad & BTN_JUMP) && !(prev_pad & BTN_JUMP) && on_ground) {
708
+ vy_q44 = JUMP_VEL_Q44;
709
+ on_ground = 0;
710
+ sfx_tone(0, 784, 8); /* jump whoop */
144
711
  }
145
- if (!landed) {
146
- py = np;
147
- if (py > 224 << 4) { py = 0; vy = 0; }
712
+ prev_pad = pad;
713
+
714
+ /* World objects drift left as the level scrolls (world-anchored). */
715
+ if (delta) {
716
+ dist_sub += delta;
717
+ if (dist_sub >= 64) { /* distance pay */
718
+ dist_sub -= 64;
719
+ ++p_score[cur_player];
720
+ draw_hud();
721
+ }
722
+ for (i = 0; i < NUM_COINS; i++) {
723
+ coin_x[i] -= delta;
724
+ if (coin_x[i] < 8) respawn_coin(i);
725
+ }
726
+ for (i = 0; i < NUM_SPIKES; i++) {
727
+ if (!spike_active[i]) continue;
728
+ spike_x[i] -= delta;
729
+ if (spike_x[i] < 8) spike_active[i] = 0;
730
+ }
148
731
  }
732
+ for (i = 0; i < NUM_SPIKES; i++)
733
+ if (!spike_active[i]) try_spawn_spike(i);
149
734
 
150
- /* Camera follows the player, centered, clamped to the world. */
151
- camX = (px >> 4) - (SCREEN_W / 2 - 4);
152
- if (camX < 0) camX = 0;
153
- if (camX > WORLD_W - SCREEN_W) camX = WORLD_W - SCREEN_W;
735
+ /* Physics: gravity + sub-pixel y. */
736
+ if (vy_q44 < MAX_VY_Q44) vy_q44 += GRAVITY_Q44;
737
+ py_q44 += vy_q44;
738
+ y = (s16)(py_q44 >> 4);
154
739
 
155
- /* Scroll: Plane A with the world, Plane B at half rate (parallax).
156
- * The VDP scroll value moves the plane LEFT for positive camX, so
157
- * we write the negative camera offset. */
158
- VDP_setHorizontalScroll(BG_A, -camX);
159
- VDP_setHorizontalScroll(BG_B, -(camX >> 1));
740
+ /* Fell into a pit (below the screen) lose the turn. */
741
+ if (y >= 224) {
742
+ kill_player();
743
+ sfx_update();
744
+ SYS_doVBlankProcess();
745
+ continue;
746
+ }
160
747
 
161
- /* Player drawn in SCREEN space = world - camera. */
162
- VDP_setSprite(0, (px >> 4) - camX, py >> 4, SPRITE_SIZE(1, 1),
163
- TILE_ATTR_FULL(PAL0, 1, 0, 0, T_PLAYER));
164
- VDP_updateSprites(1, DMA);
748
+ /* Landing probe the two level columns under the player's feet. */
749
+ if (vy_q44 >= 0) {
750
+ feet = y + 8;
751
+ c0 = ((u16)(px + cam) >> 3) & 63;
752
+ c1 = ((u16)(px + cam + 7) >> 3) & 63;
753
+ top = land_top(c0, feet);
754
+ if (top == 0) top = land_top(c1, feet);
755
+ if (top) {
756
+ py_q44 = (u16)(top - 8) << 4;
757
+ vy_q44 = 0;
758
+ if (!on_ground) sfx_tone(1, 196, 4); /* landing thud */
759
+ on_ground = 1;
760
+ } else {
761
+ on_ground = 0; /* walked off */
762
+ }
763
+ }
764
+
765
+ /* Coins (collect) + spikes (death). AABB on 8x8 sprites. */
766
+ for (i = 0; i < NUM_COINS; i++) {
767
+ if (coin_x[i] < px + 8 && coin_x[i] + 8 > px &&
768
+ coin_y[i] < y + 8 && coin_y[i] + 8 > y) {
769
+ p_score[cur_player] += 10;
770
+ sfx_tone(0, 1047, 6); /* coin ping */
771
+ draw_hud();
772
+ respawn_coin(i);
773
+ }
774
+ }
775
+ for (i = 0; i < NUM_SPIKES; i++) {
776
+ if (!spike_active[i]) continue;
777
+ if (spike_x[i] < px + 7 && spike_x[i] + 7 > px &&
778
+ SPIKE_Y < y + 7 && SPIKE_Y + 7 > y) {
779
+ kill_player();
780
+ break;
781
+ }
782
+ }
165
783
 
166
784
  sfx_update();
167
785
  SYS_doVBlankProcess();