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,7 +1,47 @@
1
- // ── platformer.c — Atari Lynx single-screen platformer ──────────────
2
- //
3
- // Subpixel gravity + jump + land-on-top collision. Static platforms
4
- // drawn as rectangles each frame. cc65 tgi + lynx joystick.
1
+ /* ── platformer.c — Atari Lynx side-scrolling platformer (complete example) ───
2
+ *
3
+ * A COMPLETE, working game title screen, lives + score, in-session
4
+ * hi-score, MIKEY music + SFX, gravity/jump physics, one-way platforms,
5
+ * pits, spikes, coins, a scrolling level, AND the Lynx's signature party
6
+ * trick: HARDWARE SPRITE SCALING. The hero is a Suzy-scaled sprite, and
7
+ * collectible GEMS breathe (pulse big↔small) every frame purely by
8
+ * rewriting two 8.8 fixed-point fields in a Sprite Control Block — no CPU
9
+ * pixel work at all. That pulse is the bait: the bigger the gem reads, the
10
+ * easier it is to grab, and the hardware does every frame of the animation.
11
+ *
12
+ * THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
13
+ * very different one. The markers tell you what's what:
14
+ * HARDWARE IDIOM (load-bearing) — dodges a documented Lynx footgun;
15
+ * reshape your gameplay around it (see TROUBLESHOOTING before changing).
16
+ * GAME LOGIC (clay) — level layout, physics tuning, scoring, art: reshape
17
+ * freely.
18
+ *
19
+ * What depends on what:
20
+ * lynx_sfx.{h,c} — MIKEY 4-voice audio (voice 0 = jump/coin SFX, voice 1 =
21
+ * background melody, voice 2 = land/hurt SFX, voice 3 = noise/death).
22
+ * vendor/cc65/libsrc/lynx/ — the FULL cc65 Lynx driver source shipped into
23
+ * your project. The TGI driver (tgi/lynx-160-102-16.s) is REQUIRED
24
+ * reading when graphics misbehave: every TGI call is itself a Suzy
25
+ * sprite, and our scaled sprites ride the same engine via tgi_ioctl(0).
26
+ *
27
+ * SCROLLING ON THE LYNX (read this — it is the platform's biggest "where's
28
+ * the hardware feature?" surprise): the Lynx has NO hardware tilemap and NO
29
+ * background scroll register. Suzy is a SPRITE BLITTER, not a tile engine.
30
+ * So we scroll the level the honest way: keep a software camera (cam_x) and
31
+ * REDRAW the visible slice of the world every frame, painting each ground/
32
+ * platform column at its on-screen position (world_x - cam_x). The full-
33
+ * redraw TGI loop (below) makes that cheap enough — the whole 160-px window
34
+ * is a handful of tgi_bar fills. The camera is one-way (never scrolls back),
35
+ * the classic runner camera. See draw_level().
36
+ *
37
+ * PLAYERS: 1. This is a handheld — multiplayer on real hardware is ComLynx,
38
+ * a cable between TWO Lynx units. A single emulator instance has nobody on
39
+ * the other end of the cable, so this example is honestly single-player
40
+ * (no fake "P2" that could never work).
41
+ *
42
+ * SCREEN: 160x102. The system font is 8x8, so a full row of text is 20
43
+ * characters — keep the HUD line short and the layout compact.
44
+ */
5
45
 
6
46
  #include <tgi.h>
7
47
  #include <joystick.h>
@@ -9,89 +49,584 @@
9
49
  #include <stdint.h>
10
50
  #include "lynx_sfx.h"
11
51
 
12
- typedef struct { int16_t x, y, w, h; } Rect;
52
+ /* The title screen renders this examples({op:'fork'}) stamps your game's
53
+ * name here automatically. Keep it <=16 chars of A-Z 0-9 space dash. */
54
+ #define GAME_TITLE "RIDGE ROMP"
13
55
 
14
- static const Rect platforms[] = {
15
- { 0, 94, 160, 8 }, /* floor */
16
- { 16, 76, 32, 4 },
17
- { 64, 64, 32, 4 },
18
- { 112, 52, 32, 4 },
19
- { 32, 40, 24, 4 },
20
- { 88, 28, 40, 4 },
56
+ /* ── GAME LOGIC (clay — reshape freely) — screen + world geometry ─────────── */
57
+ #define SCRW 160
58
+ #define SCRH 102
59
+ #define HUD_H 9 /* HUD bar height (keep it compact) */
60
+ #define GROUND_Y 90 /* default ground surface (screen Y) */
61
+ #define PLAYER_W 8 /* art is 8x8; SCALE keeps that 1:1 */
62
+
63
+ /* The level is a column map, 8 px per column. world_x of column c = c*8.
64
+ * ground_y[c] — screen-Y of the ground surface, 0xFF = pit (no floor).
65
+ * plat_y[c] — screen-Y of a one-way floating platform, 0 = none.
66
+ * COL_COUNT columns × 8 px = the level length; the run loops when the camera
67
+ * passes the end (we wrap cam_x back to 0 — the seam is a flat runway). */
68
+ #define NO_GROUND 0xFF
69
+ #define COL_COUNT 48 /* 48 * 8 = 384 px of level */
70
+ static const uint8_t ground_y[COL_COUNT] = {
71
+ 90, 90, 90, 90, 90, 90, /* start runway */
72
+ 90, 90, NO_GROUND, NO_GROUND, 90, 90, /* pit 1 (16 px) */
73
+ 82, 82, 82, 90, 90, 90, /* a raised step */
74
+ 90, NO_GROUND, NO_GROUND, NO_GROUND, 90, 90, /* pit 2 (24 px) */
75
+ 90, 90, 74, 74, 74, 90, /* high mesa */
76
+ 90, 90, 90, NO_GROUND, NO_GROUND, 90, /* pit 3 (16 px) */
77
+ 90, 90, 90, 90, 90, 90,
78
+ 90, 90, 90, 90, 90, 90, /* end runway (loop seam) */
79
+ };
80
+ static const uint8_t plat_y[COL_COUNT] = {
81
+ 0, 0, 0, 0, 70, 70, /* slab over start */
82
+ 0, 0, 0, 0, 0, 0,
83
+ 0, 0, 0, 0, 58, 58, /* high slab */
84
+ 58, 0, 0, 0, 0, 0,
85
+ 0, 0, 0, 0, 0, 0,
86
+ 0, 0, 64, 64, 64, 0, /* slab across pit 3 */
87
+ 0, 0, 0, 0, 0, 0,
88
+ 0, 0, 70, 70, 0, 0,
89
+ };
90
+
91
+ /* ── GAME LOGIC (clay) — physics tuning (all Q4.4: 16 = 1.0 px) ───────────── */
92
+ #define GRAVITY_Q44 6 /* +0.375 px/frame/frame */
93
+ #define JUMP_VEL_Q44 (-58) /* launch vy → ~7 px apex, ~6 tiles */
94
+ #define MAX_VY_Q44 56 /* terminal fall = 3.5 px/frame — *
95
+ * MUST stay under 4: the landing *
96
+ * window is 4 px (tunnelling else) */
97
+ #define MOVE_SPEED 2 /* px/frame walk + scroll speed */
98
+ #define SCROLL_WALL 72 /* past this the world scrolls (cam) */
99
+ #define START_LIVES 3
100
+ #define N_COINS 3
101
+ #define N_SPIKES 2
102
+ #define N_GEMS 2 /* the SCALING collectibles */
103
+
104
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
105
+ * SUZY HARDWARE SPRITE SCALING — the Lynx signature. Suzy renders every
106
+ * sprite through a Sprite Control Block (SCB) it walks in cart/work RAM.
107
+ * Two SCB fields, HSIZE and VSIZE, are 8.8 fixed-point scale factors
108
+ * ($0100 = 1.0): the SAME 8x8 source pixels render at any size, every
109
+ * frame, for free. We use it three ways here:
110
+ * - the HERO renders at a fixed 1.0x via the SAME SCB path (so forking in
111
+ * a depth/power-up scale is a one-line change);
112
+ * - the GEMS breathe — HSIZE/VSIZE sweep 0.75x↔1.75x every frame, a pure
113
+ * hardware animation that doubles as a difficulty tell (a fat gem is an
114
+ * easy grab, the collision box tracks the live hardware size);
115
+ * - it costs zero extra CPU vs. a fixed sprite — Suzy scales while it blits.
116
+ *
117
+ * The SCB, field by field (this is cc65's SCB_REHV_PAL from <_suzy.h>):
118
+ * sprctl0 bits 7-6 = bits per pixel (11 = 4bpp), bits 2-0 = sprite TYPE.
119
+ * TYPE_NORMAL (4) draws pens 1-15 and treats pen 0 as
120
+ * TRANSPARENT — that's how shaped sprites sit over the level.
121
+ * sprctl1 bit 7 LITERAL (raw nybbles, no RLE) + bits 5-4 reload depth:
122
+ * REHV means "this SCB carries HPOS, VPOS, HSIZE, VSIZE". The
123
+ * reload bits ARE the struct layout — mismatch them and Suzy
124
+ * reads palette bytes as size words.
125
+ * sprcoll $20 = NO_COLLIDE. Gameplay collision is done in C (in screen
126
+ * coordinates the collision buffer knows nothing about).
127
+ * next pointer to the next SCB, 0 = end of chain (one blit per call).
128
+ * data sprite pixel data (LITERAL 4bpp format below).
129
+ * hpos/vpos signed SCREEN position of the sprite's top-left corner.
130
+ * hsize/vsize 8.8 scale — THE party trick, rewritten per draw.
131
+ * penpal[8] 16 nybbles mapping pixel values 0-15 → palette pens.
132
+ *
133
+ * LITERAL 4bpp data format (hand-encodable): each sprite LINE is
134
+ * [offset byte][width/2 bytes of raw nybble pixels]
135
+ * where offset = 1 + bytes of pixel data; a final offset of 0 ends the
136
+ * sprite. 8 px @ 4bpp = 4 data bytes, so every line starts with 5.
137
+ *
138
+ * Drawing: tgi_sprite(&scb) → tgi_ioctl(0, &scb) — the TGI driver's
139
+ * documented escape hatch (see CONTROL in vendor/cc65/libsrc/lynx/tgi/
140
+ * lynx-160-102-16.s). It points Suzy's SCBNEXT at your SCB, aims VIDBAS at
141
+ * TGI's current DRAW page (so scaled sprites land in the same double-
142
+ * buffered frame as tgi_bar/tgi_outtextxy), fires SPRGO, and sleeps the CPU
143
+ * until SPRSYS reports the blit done.
144
+ *
145
+ * Requires: the cc65 crt0 Suzy init (already done before main()), and calls
146
+ * only between the tgi_busy() wait and tgi_updatedisplay() — i.e. while
147
+ * TGI's draw buffer is the blit target. Draw order = paint order: level
148
+ * fills first, scaled sprites after, HUD text last.
149
+ */
150
+ static SCB_REHV_PAL scb = {
151
+ BPP_4 | TYPE_NORMAL, /* sprctl0: 4bpp, pen 0 transparent */
152
+ LITERAL | REHV, /* sprctl1: literal data, HV+size SCB */
153
+ 0x20, /* sprcoll: NO_COLLIDE */
154
+ 0, /* next: single-SCB chain */
155
+ 0, /* data: set per draw */
156
+ 0, 0, /* hpos, vpos */
157
+ 0x0100, 0x0100, /* hsize, vsize (8.8) */
158
+ { 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF } /* identity pens */
159
+ };
160
+
161
+ /* Draw an 8x8 literal sprite at the given 8.8 scale, anchored at the
162
+ * top-left (x,y). The hero stays 1.0x; gems pulse. */
163
+ static void draw_scaled(unsigned char *data, int x, int y, unsigned scale) {
164
+ scb.data = data;
165
+ scb.hsize = scale;
166
+ scb.vsize = scale;
167
+ scb.hpos = x;
168
+ scb.vpos = y;
169
+ tgi_sprite(&scb);
170
+ }
171
+
172
+ /* ── GAME LOGIC (clay) — 8x8 4bpp literal sprite art ────────────────────────
173
+ * Pens use the TGI default palette (cc65 lynx.h COLOR_* indices): 2 = red,
174
+ * 9 = yellow, $D = blue, $E = light-blue, $F = white, 0 = transparent. Each
175
+ * line: 5, then 4 nybble bytes; a final 0 byte ends the sprite. */
176
+ static unsigned char spr_hero[] = {
177
+ 5, 0x00, 0x0E, 0xE0, 0x00, /* . . . E E . . . cyan runner, eyes */
178
+ 5, 0x00, 0xEF, 0xFE, 0x00, /* . . E F F E . . */
179
+ 5, 0x00, 0xE9, 0x9E, 0x00, /* . . E 9 9 E . . (eyes) */
180
+ 5, 0x0E, 0xEE, 0xEE, 0xE0, /* . E E E E E E . body */
181
+ 5, 0xEE, 0xEE, 0xEE, 0xEE, /* E E E E E E E E */
182
+ 5, 0x0E, 0x0E, 0xE0, 0xE0, /* . E . E E . E . legs */
183
+ 5, 0x0E, 0x00, 0x00, 0xE0, /* . E . . . . E . */
184
+ 5, 0x0F, 0x00, 0x00, 0xF0, /* . F . . . . F . feet */
185
+ 0
186
+ };
187
+ static unsigned char spr_gem[] = {
188
+ 5, 0x00, 0x09, 0x90, 0x00, /* . . . 9 9 . . . yellow gem, white shine */
189
+ 5, 0x00, 0x9F, 0xF9, 0x00, /* . . 9 F F 9 . . */
190
+ 5, 0x09, 0xFD, 0x9F, 0x90, /* . 9 F D 9 F 9 . (blue facet) */
191
+ 5, 0x9F, 0x99, 0x99, 0xF9, /* 9 F 9 9 9 9 F 9 */
192
+ 5, 0x9F, 0x99, 0x99, 0xF9, /* 9 F 9 9 9 9 F 9 */
193
+ 5, 0x09, 0x99, 0x99, 0x90, /* . 9 9 9 9 9 9 . */
194
+ 5, 0x00, 0x99, 0x99, 0x00, /* . . 9 9 9 9 . . */
195
+ 5, 0x00, 0x09, 0x90, 0x00, /* . . . 9 9 . . . */
196
+ 0
21
197
  };
22
- #define N_PLATFORMS (sizeof(platforms) / sizeof(platforms[0]))
23
198
 
24
- static uint8_t on_platform(int16_t px, int16_t py) {
199
+ /* ── GAME LOGIC (clay) gem pulse (the SCALING signature) ──────────────────
200
+ * One shared phase drives every gem's HSIZE/VSIZE. The 8.8 scale sweeps
201
+ * SCALE_MIN..SCALE_MAX and back; gem_scale() returns the current value and
202
+ * gem_half() the matching on-screen half-width so the grab box tracks the
203
+ * hardware size exactly. */
204
+ #define SCALE_MIN 0x00C0u /* 0.75x → 6 px */
205
+ #define SCALE_MAX 0x01C0u /* 1.75x → 14 px */
206
+ static unsigned gem_phase; /* 0..255 triangle wave */
207
+ static unsigned gem_scale(void) {
208
+ unsigned t = gem_phase < 128 ? gem_phase : (255 - gem_phase); /* 0..127 */
209
+ return SCALE_MIN + (t * (SCALE_MAX - SCALE_MIN)) / 127u;
210
+ }
211
+ static uint8_t gem_half(void) {
212
+ return (uint8_t)((gem_scale() * 8u) >> 9); /* (8*scale>>8)/2 */
213
+ }
214
+
215
+ typedef struct { uint8_t alive; int16_t wx; uint8_t y; } Coin; /* world-x */
216
+ typedef struct { uint8_t alive; int16_t wx; uint8_t y; } Spike;
217
+ typedef struct { uint8_t alive; int16_t wx; uint8_t y; } Gem;
218
+
219
+ static Coin coins[N_COINS];
220
+ static Spike spikes[N_SPIKES];
221
+ static Gem gems[N_GEMS];
222
+
223
+ /* Player state. px is SCREEN x (camera holds it at SCROLL_WALL while
224
+ * scrolling); world x = px + cam_x. py is Q4.4 for sub-pixel gravity. */
225
+ static uint8_t px;
226
+ static int16_t py_q44;
227
+ static int8_t vy_q44;
228
+ static uint8_t on_ground;
229
+ static unsigned cam_x; /* software camera (one-way) */
230
+ static uint8_t lives;
231
+ static unsigned score, hiscore; /* hiscore: in-session only (see below) */
232
+ static uint8_t dist_sub; /* 64 px scrolled = +1 distance point */
233
+ static uint8_t hurt_timer;
234
+ static uint8_t prev_joy;
235
+
236
+ /* Game states — the shell every example shares: title → play → game over. */
237
+ #define ST_TITLE 0
238
+ #define ST_PLAY 1
239
+ #define ST_OVER 2
240
+ static uint8_t state;
241
+ static uint8_t over_new_hi;
242
+
243
+ /* ── GAME LOGIC (clay) — Galois LFSR (taps $B8), period 255 ── */
244
+ static uint8_t rng_state = 0x5A;
245
+ static uint8_t rand8(void) {
246
+ uint8_t lsb = (uint8_t)(rng_state & 1);
247
+ rng_state >>= 1;
248
+ if (lsb) rng_state ^= 0xB8;
249
+ return rng_state;
250
+ }
251
+
252
+ /* ── GAME LOGIC (clay) — score text (no sprintf: it drags in ~6KB) ── */
253
+ static char numbuf[6];
254
+ static char *fmt5(unsigned v) {
25
255
  uint8_t i;
26
- for (i = 0; i < N_PLATFORMS; i++) {
27
- if (py + 6 == platforms[i].y
28
- && px + 6 > platforms[i].x
29
- && px < platforms[i].x + platforms[i].w) return 1;
256
+ for (i = 0; i < 5; i++) { numbuf[4 - i] = (char)('0' + v % 10); v /= 10; }
257
+ numbuf[5] = 0;
258
+ return numbuf;
259
+ }
260
+ static uint8_t udist(uint8_t a, uint8_t b) { return a > b ? a - b : b - a; }
261
+
262
+ /* ── GAME LOGIC (clay) — column-map lookups (world x → column) ──────────────
263
+ * The level loops: world x wraps at COL_COUNT*8 so the run is endless. */
264
+ #define LEVEL_LEN ((unsigned)COL_COUNT * 8u)
265
+ static uint8_t col_of(unsigned wx) { return (uint8_t)((wx % LEVEL_LEN) >> 3); }
266
+
267
+ /* ── GAME LOGIC (clay) — draw the scrolling level (SOFTWARE camera) ─────────
268
+ * No hardware scroll on the Lynx (see header). We paint the visible window
269
+ * column by column: for each on-screen column, look up the world column at
270
+ * (cam_x + screenX) and fill its ground body + grass cap + any platform
271
+ * slab. Per-column tgi_bar fills keep the code legible — the whole strip is
272
+ * well under the frame budget. */
273
+ static void draw_level(void) {
274
+ int sx;
275
+ uint8_t c, gy, pgy;
276
+ for (sx = 0; sx < SCRW; sx += 8) {
277
+ c = col_of(cam_x + (unsigned)sx);
278
+ gy = ground_y[c];
279
+ /* ground column: grass cap (green) over a dirt body (brown) */
280
+ if (gy != NO_GROUND) {
281
+ tgi_setcolor(COLOR_BROWN);
282
+ tgi_bar(sx, gy + 2, sx + 7, SCRH - 1);
283
+ tgi_setcolor(COLOR_LIGHTGREEN);
284
+ tgi_bar(sx, gy, sx + 7, gy + 1);
285
+ }
286
+ /* one-way platform slab (grey ledge) */
287
+ pgy = plat_y[c];
288
+ if (pgy) {
289
+ tgi_setcolor(COLOR_GREY);
290
+ tgi_bar(sx, pgy, sx + 7, pgy + 2);
291
+ }
292
+ }
293
+ }
294
+
295
+ /* ── GAME LOGIC (clay) — shared scene painter (runs every frame) ────────────
296
+ * Full-redraw, painter's order: sky, far parallax hills, HUD bar, then the
297
+ * caller layers the level + sprites + text on top. Layered bands keep any
298
+ * one colour comfortably under the render-health blank threshold. */
299
+ static const unsigned char hill_x[6] = { 8, 44, 78, 112, 138, 156 };
300
+ static void draw_scene(void) {
301
+ uint8_t i, hx;
302
+ tgi_setcolor(COLOR_BLUE);
303
+ tgi_bar(0, 0, SCRW - 1, SCRH - 1); /* sky */
304
+ /* far parallax hills (drift slower than the camera → depth) */
305
+ tgi_setcolor(COLOR_PURPLE);
306
+ for (i = 0; i < 6; i++) {
307
+ hx = (uint8_t)((hill_x[i] + SCRW - (uint8_t)((cam_x >> 2) % SCRW)) % SCRW);
308
+ tgi_bar(hx, 62, (hx + 26 < SCRW ? hx + 26 : SCRW - 1), 89);
309
+ }
310
+ tgi_setcolor(COLOR_DARKGREY);
311
+ tgi_bar(0, 0, SCRW - 1, HUD_H - 1); /* HUD bar */
312
+ }
313
+
314
+ /* ── GAME LOGIC (clay) — place world objects across the level ── */
315
+ static void place_objects(void) {
316
+ uint8_t i, c;
317
+ for (i = 0; i < N_COINS; i++) {
318
+ coins[i].alive = 1;
319
+ coins[i].wx = (int16_t)(40 + i * 110);
320
+ c = col_of(coins[i].wx);
321
+ /* hover a little above the surface, with a touch of LFSR jitter so the
322
+ * pickup arc isn't a flat line */
323
+ coins[i].y = (uint8_t)((ground_y[c] == NO_GROUND ? 60 : ground_y[c] - 18)
324
+ - (rand8() & 7));
325
+ }
326
+ for (i = 0; i < N_SPIKES; i++) {
327
+ spikes[i].wx = (int16_t)(96 + i * 150);
328
+ c = col_of(spikes[i].wx);
329
+ spikes[i].alive = ground_y[c] != NO_GROUND;
330
+ spikes[i].y = (uint8_t)((ground_y[c] == NO_GROUND ? GROUND_Y : ground_y[c]) - 6);
331
+ }
332
+ /* gems sit higher (a reach jump) and pulse via the scaling idiom */
333
+ for (i = 0; i < N_GEMS; i++) {
334
+ gems[i].alive = 1;
335
+ gems[i].wx = (int16_t)(150 + i * 130);
336
+ gems[i].y = (uint8_t)(48 + (i & 1) * 8);
337
+ }
338
+ }
339
+
340
+ /* ── GAME LOGIC (clay) — start a run ── */
341
+ static void start_game(void) {
342
+ px = 24;
343
+ py_q44 = (int16_t)((GROUND_Y - PLAYER_W) << 4);
344
+ vy_q44 = 0;
345
+ on_ground = 1;
346
+ cam_x = 0;
347
+ dist_sub = 0;
348
+ lives = START_LIVES;
349
+ score = 0;
350
+ hurt_timer = 0;
351
+ gem_phase = 0;
352
+ place_objects();
353
+ sfx_tone(0, 80, 8); /* start chirp */
354
+ state = ST_PLAY;
355
+ }
356
+
357
+ static void game_over(void) {
358
+ over_new_hi = 0;
359
+ if (score > hiscore) {
360
+ /* ── In-session hi-score ONLY — and here's the honest why. Real Lynx
361
+ * carts persist via a 93Cxx serial EEPROM on the cart PCB (cc65 even
362
+ * ships lynx_eeprom_read/write for it; see vendor/cc65/libsrc/lynx/
363
+ * eeprom.s). PROBED: the bundled handy core emulates CEEPROM internally
364
+ * but its libretro build exposes NO save path — retro_get_memory(
365
+ * SAVE_RAM) returns NULL/size 0, so nothing survives host.hardReset()
366
+ * and a bit-banged round-trip reads back garbage under the WASM build.
367
+ * Wiring the EEPROM to SAVE_RAM is a future core round; until then a
368
+ * fake "save" would be lying. The hi-score DOES survive title↔play
369
+ * cycles within one power-on. ── */
370
+ hiscore = score;
371
+ over_new_hi = 1;
30
372
  }
373
+ sfx_tone(2, 240, 24); /* voice 2: low game-over drone */
374
+ sfx_noise(16); /* voice 3: crunch */
375
+ state = ST_OVER;
376
+ }
377
+
378
+ /* ── GAME LOGIC (clay) — death + respawn at the run start ── */
379
+ static void lose_life(void) {
380
+ sfx_noise(14); /* voice 3: splat */
381
+ if (lives) lives--;
382
+ if (lives == 0) { game_over(); return; }
383
+ /* respawn at a safe runway tile, keep the camera (one-way run) */
384
+ px = 24;
385
+ py_q44 = (int16_t)((GROUND_Y - PLAYER_W) << 4);
386
+ vy_q44 = 0;
387
+ on_ground = 1;
388
+ hurt_timer = 45;
389
+ prev_joy = 0xFF; /* swallow held jump across the respawn */
390
+ }
391
+
392
+ /* ── GAME LOGIC (clay) — landing probe against the column map ───────────────
393
+ * One-way platforms: only catch the player while FALLING through a narrow
394
+ * 4-px window at a surface's top. Probe both columns under the 8-px-wide
395
+ * feet so a foot half-off a ledge still lands. Returns the surface Y to snap
396
+ * to, or 0 for "no floor here". */
397
+ static uint8_t land_top(uint8_t feet) {
398
+ uint8_t c0, c1, gy, pgy;
399
+ unsigned wx = cam_x + px;
400
+ c0 = col_of(wx);
401
+ c1 = col_of(wx + 7);
402
+ /* platform slabs first (they sit above the ground) */
403
+ pgy = plat_y[c0]; if (!pgy) pgy = plat_y[c1];
404
+ if (pgy && (uint8_t)(feet + 1) >= pgy && feet <= (uint8_t)(pgy + 4)) return pgy;
405
+ /* then the ground surface */
406
+ gy = ground_y[c0];
407
+ if (gy == NO_GROUND) gy = ground_y[c1];
408
+ if (gy != NO_GROUND && (uint8_t)(feet + 1) >= gy && feet <= (uint8_t)(gy + 4))
409
+ return gy;
31
410
  return 0;
32
411
  }
33
412
 
413
+ /* ── GAME LOGIC (clay) — per-state frames. Each runs INSIDE the canonical
414
+ * loop below: scene already painted, tgi_updatedisplay not yet called. ── */
415
+
416
+ static unsigned attract_cam;
417
+
418
+ static void frame_title(uint8_t joy) {
419
+ cam_x = attract_cam; /* attract: the level drifts by */
420
+ draw_level();
421
+ /* a lone breathing gem sells the scaling idiom on the title screen —
422
+ * parked in a clear top-right zone (away from all the text) so the pulse
423
+ * reads cleanly. */
424
+ draw_scaled(spr_gem, 132, 14, gem_scale());
425
+ draw_scaled(spr_hero, 28, GROUND_Y - PLAYER_W, 0x0100);
426
+ attract_cam++;
427
+ if (attract_cam >= LEVEL_LEN) attract_cam -= LEVEL_LEN;
428
+
429
+ tgi_setcolor(COLOR_WHITE);
430
+ tgi_outtextxy(40, 1, GAME_TITLE); /* on the HUD bar */
431
+ tgi_setcolor(COLOR_YELLOW);
432
+ tgi_outtextxy(52, 30, "PRESS A");
433
+ tgi_setcolor(COLOR_LIGHTGREY);
434
+ tgi_outtextxy(32, 44, "HI ");
435
+ tgi_outtextxy(56, 44, fmt5(hiscore));
436
+ tgi_outtextxy(24, 56, "1 PLAYER GAME"); /* handheld honesty */
437
+
438
+ if (JOY_BTN_1(joy) && !JOY_BTN_1(prev_joy)) start_game();
439
+ }
440
+
441
+ static void frame_over(uint8_t joy) {
442
+ tgi_setcolor(COLOR_DARKGREY);
443
+ tgi_bar(20, 28, 139, 66);
444
+ tgi_setcolor(COLOR_WHITE);
445
+ tgi_outtextxy(44, 32, "GAME OVER");
446
+ tgi_setcolor(COLOR_YELLOW);
447
+ tgi_outtextxy(36, 42, "SCORE ");
448
+ tgi_outtextxy(84, 42, fmt5(score));
449
+ if (over_new_hi) { tgi_setcolor(COLOR_LIGHTGREEN); tgi_outtextxy(32, 54, "NEW HI SCORE"); }
450
+ else { tgi_setcolor(COLOR_LIGHTGREY); tgi_outtextxy(40, 54, "A = TITLE"); }
451
+ if (JOY_BTN_1(joy) && !JOY_BTN_1(prev_joy)) state = ST_TITLE;
452
+ }
453
+
454
+ /* clamp+test that a world object is on-screen; returns its screen-x or -1 */
455
+ static int obj_sx(int16_t wx) {
456
+ int16_t s = wx - (int16_t)cam_x;
457
+ if (s < 0 || s >= SCRW) return -1;
458
+ return (int)s;
459
+ }
460
+
461
+ static void frame_play(uint8_t joy) {
462
+ uint8_t i, py8, feet, top, gh;
463
+ int s;
464
+ uint8_t moved = 0;
465
+
466
+ /* ── draw: level, world objects (camera-relative), hero, HUD ── */
467
+ draw_level();
468
+
469
+ for (i = 0; i < N_COINS; i++) {
470
+ if (!coins[i].alive) continue;
471
+ s = obj_sx(coins[i].wx); if (s < 0) continue;
472
+ tgi_setcolor(COLOR_YELLOW);
473
+ tgi_bar(s, coins[i].y, s + 5, coins[i].y + 5);
474
+ tgi_setcolor(COLOR_BROWN);
475
+ tgi_bar(s + 2, coins[i].y + 2, s + 3, coins[i].y + 3);
476
+ }
477
+ for (i = 0; i < N_SPIKES; i++) {
478
+ if (!spikes[i].alive) continue;
479
+ s = obj_sx(spikes[i].wx); if (s < 0) continue;
480
+ tgi_setcolor(COLOR_RED);
481
+ tgi_bar(s, spikes[i].y, s + 1, spikes[i].y + 5);
482
+ tgi_bar(s + 2, spikes[i].y - 2, s + 3, spikes[i].y + 5);
483
+ tgi_bar(s + 4, spikes[i].y, s + 5, spikes[i].y + 5);
484
+ }
485
+ /* gems — drawn via the SCALING SCB (pulse this frame's hardware size) */
486
+ for (i = 0; i < N_GEMS; i++) {
487
+ if (!gems[i].alive) continue;
488
+ s = obj_sx(gems[i].wx); if (s < 0) continue;
489
+ draw_scaled(spr_gem, s, gems[i].y, gem_scale());
490
+ }
491
+ /* hero (blink while hurt) at a fixed 1.0x through the same SCB path */
492
+ py8 = (uint8_t)(py_q44 >> 4);
493
+ if (hurt_timer == 0 || (hurt_timer & 4))
494
+ draw_scaled(spr_hero, (int)px, py8, 0x0100);
495
+
496
+ tgi_setcolor(COLOR_WHITE);
497
+ tgi_outtextxy(2, 1, "SC");
498
+ tgi_outtextxy(20, 1, fmt5(score));
499
+ tgi_setcolor(COLOR_LIGHTGREY);
500
+ tgi_outtextxy(72, 1, "HI");
501
+ tgi_outtextxy(90, 1, fmt5(hiscore));
502
+ tgi_setcolor(COLOR_YELLOW);
503
+ tgi_outtextxy(140, 1, "L");
504
+ numbuf[0] = (char)('0' + lives); numbuf[1] = 0;
505
+ tgi_outtextxy(148, 1, numbuf);
506
+
507
+ /* ── update: input ── */
508
+ if (joy & JOY_RIGHT_MASK) {
509
+ if (px < SCROLL_WALL) px += MOVE_SPEED;
510
+ else { cam_x += MOVE_SPEED; moved = MOVE_SPEED; }
511
+ }
512
+ if ((joy & JOY_LEFT_MASK) && px > 8) px -= MOVE_SPEED;
513
+ if (JOY_BTN_1(joy) && !JOY_BTN_1(prev_joy) && on_ground) {
514
+ vy_q44 = JUMP_VEL_Q44;
515
+ on_ground = 0;
516
+ sfx_tone(0, 110, 6); /* voice 0: jump whoop */
517
+ }
518
+ if (hurt_timer) hurt_timer--;
519
+ if (cam_x >= LEVEL_LEN) cam_x -= LEVEL_LEN; /* loop the run */
520
+
521
+ /* distance scoring */
522
+ if (moved) {
523
+ dist_sub += moved;
524
+ if (dist_sub >= 64) { dist_sub -= 64; score++; }
525
+ }
526
+
527
+ /* ── physics: gravity + sub-pixel Y ── */
528
+ if (vy_q44 < MAX_VY_Q44) vy_q44 += GRAVITY_Q44;
529
+ py_q44 += vy_q44;
530
+ py8 = (uint8_t)(py_q44 >> 4);
531
+
532
+ /* fell below the screen (into a pit) → lose a life */
533
+ if (py_q44 < 0 || py8 >= SCRH - 2) { lose_life(); return; }
534
+
535
+ /* landing probe (only while falling) */
536
+ if (vy_q44 >= 0) {
537
+ feet = py8 + PLAYER_W;
538
+ top = land_top(feet);
539
+ if (top) {
540
+ py_q44 = (int16_t)((top - PLAYER_W) << 4);
541
+ vy_q44 = 0;
542
+ if (!on_ground) sfx_tone(2, 180, 3); /* voice 2: land thud */
543
+ on_ground = 1;
544
+ } else {
545
+ on_ground = 0;
546
+ }
547
+ }
548
+
549
+ /* ── collisions (screen space; gem box tracks the live hardware size) ── */
550
+ gh = gem_half();
551
+ for (i = 0; i < N_GEMS; i++) {
552
+ if (!gems[i].alive) continue;
553
+ s = obj_sx(gems[i].wx); if (s < 0) continue;
554
+ if (udist((uint8_t)(s + 4), (uint8_t)(px + 4)) < gh + 4
555
+ && udist((uint8_t)(gems[i].y + 4), (uint8_t)(py8 + 4)) < gh + 4) {
556
+ gems[i].alive = 0;
557
+ score += 25; /* fat gem = fat points */
558
+ sfx_tone(0, 60, 6); /* voice 0: sparkle */
559
+ }
560
+ }
561
+ for (i = 0; i < N_COINS; i++) {
562
+ if (!coins[i].alive) continue;
563
+ s = obj_sx(coins[i].wx); if (s < 0) continue;
564
+ if (udist((uint8_t)(s + 3), (uint8_t)(px + 4)) < 8
565
+ && udist((uint8_t)(coins[i].y + 3), (uint8_t)(py8 + 4)) < 8) {
566
+ coins[i].alive = 0;
567
+ score += 10;
568
+ sfx_tone(0, 70, 5); /* voice 0: coin ping */
569
+ }
570
+ }
571
+ if (hurt_timer == 0) {
572
+ for (i = 0; i < N_SPIKES; i++) {
573
+ if (!spikes[i].alive) continue;
574
+ s = obj_sx(spikes[i].wx); if (s < 0) continue;
575
+ if (udist((uint8_t)(s + 3), (uint8_t)(px + 4)) < 7
576
+ && udist((uint8_t)(spikes[i].y + 3), (uint8_t)(py8 + 4)) < 7) {
577
+ lose_life();
578
+ return;
579
+ }
580
+ }
581
+ }
582
+ }
583
+
34
584
  void main(void) {
35
- int16_t px = 80, py = 88;
36
- int8_t vy = 0;
37
- uint8_t joy, prev = 0, btn, grounded;
38
- uint8_t i;
585
+ uint8_t joy;
39
586
 
40
587
  tgi_install(&lynx_160_102_16_tgi);
41
588
  tgi_init();
42
589
  joy_install(&lynx_stdjoy_joy);
43
- sfx_init();
590
+ sfx_init(); /* MIKEY up; background melody starts on voice 1 */
591
+
592
+ state = ST_TITLE;
593
+ prev_joy = 0;
594
+ attract_cam = 0;
595
+ hiscore = 0;
44
596
 
45
597
  for (;;) {
46
- /* Lynx frame loop: WAIT for the blitter, then clear with a full-screen
47
- * tgi_bar (NOT tgi_clear, which leaves the back page stale on this core)
48
- * — drawing while the blitter is mid-flight loses the frame → black.
49
- * (Copied from the shmup scaffold, the LYNX-1 fix.) */
598
+ /* ── HARDWARE IDIOM (load-bearing reshape gameplay around this; see TROUBLESHOOTING) ──
599
+ * CANONICAL LYNX GAME LOOP full-redraw every frame, in this order:
600
+ * 1. while (tgi_busy()) { } — WAIT for the previous frame's page
601
+ * flip. Skipping this is the #1 "Lynx screen stays blank" trap:
602
+ * drawing while the swap is pending loses the frame.
603
+ * 2. Repaint the WHOLE scene with tgi_bar fills — NOT tgi_clear()
604
+ * (which can leave the framebuffer stale on this toolchain+
605
+ * emulator path). TGI double-buffers; the back buffer holds the
606
+ * frame from two flips ago, so partial redraws ghost. The SOFTWARE
607
+ * camera (header) means scrolling = redrawing the visible slice.
608
+ * 3. Draw every object (every TGI call and every tgi_sprite() is a
609
+ * synchronous Suzy blit into the SAME draw page).
610
+ * 4. tgi_updatedisplay() — request the page flip at next VBL.
611
+ * 5. sfx_update() IMMEDIATELY after — MIKEY voice writes must land in
612
+ * vblank: handy reschedules its timer sweep on the spot when a
613
+ * voice CTL bit-3 write lands, and mid-frame that sweep can preempt
614
+ * an in-flight Suzy blit and eat sprites (the R57 bug — history in
615
+ * lynx_sfx.c). sfx_tone()/sfx_noise() only STAGE; sfx_update() is
616
+ * the hardware flush. */
50
617
  while (tgi_busy()) { }
51
- tgi_setcolor(COLOR_BLACK);
52
- tgi_bar(0, 0, tgi_getmaxx(), tgi_getmaxy());
53
- tgi_setcolor(COLOR_GREY);
54
- for (i = 0; i < N_PLATFORMS; i++) {
55
- tgi_bar(platforms[i].x, platforms[i].y, platforms[i].x + platforms[i].w - 1, platforms[i].y + platforms[i].h - 1);
56
- }
57
- tgi_setcolor(COLOR_YELLOW);
58
- tgi_bar((unsigned)px, (unsigned)py, (unsigned)(px + 6), (unsigned)(py + 6));
618
+
619
+ draw_scene();
620
+ joy = joy_read(JOY_1);
621
+
622
+ if (state == ST_TITLE) frame_title(joy);
623
+ else if (state == ST_PLAY) frame_play(joy);
624
+ else frame_over(joy);
625
+
59
626
  tgi_updatedisplay();
60
627
  sfx_update();
61
628
 
62
- joy = joy_read(JOY_1);
63
- btn = JOY_BTN_1(joy) ? 1 : 0;
64
- grounded = on_platform(px, py);
65
-
66
- if (JOY_LEFT(joy) && px > 0) px--;
67
- if (JOY_RIGHT(joy) && px < 154) px++;
68
- if (btn && !prev && grounded) { vy = -6; sfx_tone(0, 100, 6); }
69
- prev = btn;
70
-
71
- {
72
- /* Land-on-top via a CROSSING test. The old check demanded
73
- * py+6 == platform.y EXACTLY after the move — falls step up to
74
- * 4px/frame, so the exact value was usually skipped (fall-through),
75
- * and the `py & 0xFC` snap then broke the equality for the next
76
- * frame's grounded test (couldn't jump from floating platforms). */
77
- int16_t old_py = py;
78
- uint8_t i;
79
- vy++;
80
- if (vy > 4) vy = 4;
81
- py += vy;
82
- if (vy > 0) {
83
- for (i = 0; i < N_PLATFORMS; i++) {
84
- if (old_py + 6 <= platforms[i].y && py + 6 >= platforms[i].y
85
- && px + 6 > platforms[i].x
86
- && px < platforms[i].x + platforms[i].w) {
87
- py = platforms[i].y - 6;
88
- vy = 0;
89
- break;
90
- }
91
- }
92
- }
93
- if (py < 0) py = 0;
94
- if (py > 96) py = 96;
95
- }
629
+ gem_phase = (gem_phase + 4) & 255; /* advance the shared scaling pulse */
630
+ prev_joy = joy;
96
631
  }
97
632
  }