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,4 +1,54 @@
1
- // ── racing.c — Atari Lynx 3-lane top-down racer ──────────────────────
1
+ /* ── racing.c — Atari Lynx 1P top-down road racer (complete example game) ─────
2
+ *
3
+ * A COMPLETE, working game — DEPTH DODGE, a top-down vertical road racer fit to
4
+ * the Lynx's tiny 160x102 screen: title screen, a 1P endless run with speed
5
+ * control and a steerable car, a best-distance record, MIKEY music + SFX, AND
6
+ * the Lynx's signature party trick: HARDWARE SPRITE SCALING used for PSEUDO-3D
7
+ * DEPTH. Obstacle cars are Suzy scalable sprites that ENTER tiny at the far
8
+ * horizon and SWELL as they rush toward you — an OutRun-ish "coming at you"
9
+ * read built from real hardware scaling, not Mode-7 (the Lynx has no affine
10
+ * background; this is honest sprite scaling, see the HARDWARE IDIOM note).
11
+ *
12
+ * The game: you drive the YELLOW car along the bottom of a vertically-scrolling
13
+ * road. LEFT/RIGHT hop between three lanes; UP accelerates, DOWN brakes
14
+ * (speed 1-5). Faster = more distance banked but obstacles close quicker.
15
+ * Obstacle cars spawn at the horizon and grow as they approach; a same-lane
16
+ * collision when one reaches you is a CRASH (3 crashes ends the run). The run's
17
+ * DISTANCE is the score; your best DISTANCE this power-on is shown on the title.
18
+ *
19
+ * THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
20
+ * very different one. The markers tell you what's what:
21
+ * HARDWARE IDIOM (load-bearing) — dodges a documented Lynx footgun;
22
+ * reshape your gameplay around it (see TROUBLESHOOTING before changing).
23
+ * GAME LOGIC (clay) — road art, traffic patterns, speeds, scoring rules:
24
+ * reshape freely.
25
+ *
26
+ * What depends on what:
27
+ * lynx_sfx.{h,c} — MIKEY 4-voice audio (voice 0 = steer/crash SFX, voice 1 =
28
+ * background melody, voice 2 = engine/checkpoint blips, voice 3 = noise).
29
+ * vendor/cc65/libsrc/lynx/ — the FULL cc65 Lynx driver source shipped into
30
+ * your project. The TGI driver (tgi/lynx-160-102-16.s) is REQUIRED
31
+ * reading when graphics misbehave: every TGI call is itself a Suzy
32
+ * sprite, and our scaled obstacle cars ride the same engine via
33
+ * tgi_ioctl(0).
34
+ *
35
+ * NO HARDWARE TILEMAP (read this — it is the platform's biggest "where's the
36
+ * road renderer?" surprise): the Lynx has NO background tilemap and NO
37
+ * hardware scroll. Suzy is a SPRITE BLITTER, not a tile engine. So the road
38
+ * is drawn the honest way: the full-redraw TGI loop repaints the WHOLE track
39
+ * every frame as a stack of tgi_bar fills + tgi_line markings, and the road
40
+ * "scrolls" by animating the lane-dash phase each frame — cheap on a 160x102
41
+ * screen, and it falls out of the canonical full-redraw loop for free.
42
+ *
43
+ * PLAYERS: 1. This is a handheld — head-to-head on real hardware is ComLynx,
44
+ * a cable between TWO physical Lynx units. A single emulator instance has
45
+ * nobody on the other end of the cable, so this example is honestly a 1P
46
+ * endless racer (no fake "P2 VERSUS" that could never work here — contrast
47
+ * the NES racing donor, which has a real simultaneous-2P split-road mode).
48
+ *
49
+ * SCREEN: 160x102. The system font is 8x8, so a full row of text is 20
50
+ * characters — the road + HUD are kept compact to fit.
51
+ */
2
52
 
3
53
  #include <tgi.h>
4
54
  #include <joystick.h>
@@ -6,122 +56,508 @@
6
56
  #include <stdint.h>
7
57
  #include "lynx_sfx.h"
8
58
 
9
- #define MAX_OBS 4
10
- #define LANE0 32
11
- #define LANE1 76
12
- #define LANE2 120
59
+ /* The title screen renders this — examples({op:'fork'}) stamps your game's
60
+ * name here automatically. Keep it <=16 chars of A-Z 0-9 space dash. */
61
+ #define GAME_TITLE "DEPTH DODGE"
13
62
 
14
- static const uint8_t lane_x[3] = { LANE0, LANE1, LANE2 };
63
+ /* ── GAME LOGIC (clay reshape freely) road geometry (fits 160x102) ───────
64
+ * A vertical road down the centre with grass shoulders. ROAD_L/ROAD_R bound
65
+ * the tarmac; three lane centres sit inside it. The player rides near the
66
+ * bottom; obstacles travel the road from HORIZON_Y (top) downward. */
67
+ #define ROAD_L 28 /* left tarmac edge */
68
+ #define ROAD_R 131 /* right tarmac edge */
69
+ #define HORIZON_Y 14 /* top of the playfield (far distance) */
70
+ #define PLAYER_Y 88 /* player car centre row (near the foot) */
71
+ #define CRASH_Y 82 /* y at/after which an obstacle "reaches" */
72
+ #define LANES 3
73
+ #define START_LIVES 3
74
+ #define MAX_OBS 4 /* obstacle pool size */
75
+ static const int16_t lane_x[LANES] = { 51, 79, 108 }; /* lane centres */
15
76
 
16
- typedef struct { int16_t x, y, alive; } Car;
77
+ /* Game states the shell every example shares: title → play → over. */
78
+ #define ST_TITLE 0
79
+ #define ST_PLAY 1
80
+ #define ST_OVER 2
81
+ static uint8_t state;
17
82
 
18
- void main(void) {
19
- uint8_t player_lane = 1;
20
- Car player = { LANE1, 90, 1 };
21
- Car obs[MAX_OBS];
22
- uint8_t spawn = 0, prev = 0;
23
- uint8_t game_over = 0;
24
- uint8_t joy, i;
25
- uint32_t rng = 1;
26
- uint8_t scroll = 0; /* animates the road dashes so the track moves */
83
+ /* ── GAME LOGIC (clay) — run state ── */
84
+ static uint8_t player_lane; /* 0..2 */
85
+ static uint8_t speed; /* 1..5 road px/frame */
86
+ static uint16_t dist; /* run distance (the score) */
87
+ static uint8_t dist_frac;
88
+ static uint16_t best; /* in-session best distance — see below */
89
+ static uint8_t lives;
90
+ static uint8_t invuln; /* post-crash blink/no-collide frames */
91
+ static uint8_t spawn_timer;
92
+ static uint8_t road_phase; /* lane-dash scroll phase (0..11) */
93
+ static uint8_t prev_joy;
94
+ static uint8_t new_record; /* result screen shows NEW RECORD */
95
+
96
+ /* The result SCALE POP: when >0 the result glyph draws swollen for a few
97
+ * frames (the SCALING signature), counting back to the resting 1.0x. */
98
+ static uint8_t pop_timer;
99
+ #define POP_FRAMES 12
100
+
101
+ /* Obstacle pool (fixed slots, no allocation). y travels HORIZON_Y..CRASH_Y. */
102
+ static uint8_t obs_alive[MAX_OBS];
103
+ static uint8_t obs_lane[MAX_OBS];
104
+ static int16_t obs_y[MAX_OBS];
105
+
106
+ /* ── GAME LOGIC (clay) — xorshift16 PRNG (~tens of cycles per call).
107
+ * Picks obstacle lanes; without a noise source the spawn pattern would be a
108
+ * fixed loop. rand8() is also ticked once per play frame so identical game
109
+ * states a few seconds apart still diverge. */
110
+ static uint16_t rng = 0xC0A7;
111
+ static uint8_t rand8(void) {
112
+ uint16_t r = rng;
113
+ r ^= r << 7;
114
+ r ^= r >> 9;
115
+ r ^= r << 8;
116
+ rng = r;
117
+ return (uint8_t)r;
118
+ }
119
+
120
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
121
+ * SUZY HARDWARE SPRITE SCALING — the Lynx signature, used here for PSEUDO-3D
122
+ * DEPTH. Suzy renders every sprite through a Sprite Control Block (SCB) it
123
+ * walks in cart/work RAM. Two SCB fields, HSIZE and VSIZE, are 8.8 fixed-point
124
+ * scale factors ($0100 = 1.0): the SAME 8x8 source pixels render at any size,
125
+ * every frame, for free. This game uses it to fake DEPTH:
126
+ * - each OBSTACLE car is a Suzy sprite whose 8.8 scale is computed from its
127
+ * screen Y — small at the far HORIZON, swelling toward 1.0x+ as it nears
128
+ * the player — recomputed every frame, zero CPU pixel cost (Suzy scales
129
+ * while it blits). A car that "rushes at you" is the hardware doing the
130
+ * perspective, not a pre-scaled sprite sheet.
131
+ * - the player car and the RESULT POP (the glyph swells then eases back when
132
+ * a run ends) ride the same scaling SCB path.
133
+ * This is NOT Mode-7 / affine backgrounds (the Lynx has none): it is honest
134
+ * SPRITE scaling, and the hitbox below TRACKS the live hardware size so the
135
+ * collision reads what you SEE.
136
+ *
137
+ * The SCB, field by field (this is cc65's SCB_REHV_PAL from <_suzy.h>):
138
+ * sprctl0 bits 7-6 = bits per pixel (11 = 4bpp), bits 2-0 = sprite TYPE.
139
+ * TYPE_NORMAL (4) draws pens 1-15 and treats pen 0 as
140
+ * TRANSPARENT — that's how the car shape sits over the road.
141
+ * sprctl1 bit 7 LITERAL (raw nybbles, no RLE) + bits 5-4 reload depth:
142
+ * REHV means "this SCB carries HPOS, VPOS, HSIZE, VSIZE". The
143
+ * reload bits ARE the struct layout — mismatch them and Suzy reads
144
+ * palette bytes as size words.
145
+ * sprcoll $20 = NO_COLLIDE. Car/obstacle collision is done in C on the road
146
+ * coordinates (the collision buffer knows nothing about gameplay).
147
+ * next pointer to the next SCB, 0 = end of chain (one blit per call).
148
+ * data sprite pixel data (LITERAL 4bpp format below).
149
+ * hpos/vpos signed SCREEN position of the sprite's top-left corner.
150
+ * hsize/vsize 8.8 scale — THE party trick, rewritten per draw.
151
+ * penpal[8] 16 nybbles mapping pixel values 0-15 → palette pens. We RECOLOUR
152
+ * the sprite per draw here (one 8x8 art block, any pen) by pointing
153
+ * the art's pixel value 1 at the wanted pen — no extra art.
154
+ *
155
+ * LITERAL 4bpp data format (hand-encodable): each sprite LINE is
156
+ * [offset byte][width/2 bytes of raw nybble pixels]
157
+ * where offset = 1 + bytes of pixel data; a final offset of 0 ends the sprite.
158
+ * 8 px @ 4bpp = 4 data bytes, so every line starts with 5.
159
+ *
160
+ * Drawing: tgi_sprite(&scb) → tgi_ioctl(0, &scb) — the TGI driver's
161
+ * documented escape hatch (see CONTROL in vendor/cc65/libsrc/lynx/tgi/
162
+ * lynx-160-102-16.s). It points Suzy's SCBNEXT at your SCB, aims VIDBAS at
163
+ * TGI's current DRAW page (so scaled sprites land in the same double-buffered
164
+ * frame as tgi_bar/tgi_outtextxy), fires SPRGO, and sleeps the CPU until
165
+ * SPRSYS reports the blit done.
166
+ *
167
+ * Requires: the cc65 crt0 Suzy init (already done before main()), and calls
168
+ * only between the tgi_busy() wait and tgi_updatedisplay() — i.e. while
169
+ * TGI's draw buffer is the blit target. Draw order = paint order: road fills
170
+ * first, scaled obstacle/player cars after, HUD text last.
171
+ */
172
+ static SCB_REHV_PAL scb = {
173
+ BPP_4 | TYPE_NORMAL, /* sprctl0: 4bpp, pen 0 transparent */
174
+ LITERAL | REHV, /* sprctl1: literal data, HV+size SCB */
175
+ 0x20, /* sprcoll: NO_COLLIDE */
176
+ 0, /* next: single-SCB chain */
177
+ 0, /* data: set per draw */
178
+ 0, 0, /* hpos, vpos */
179
+ 0x0100, 0x0100, /* hsize, vsize (8.8) */
180
+ { 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF } /* identity pens */
181
+ };
182
+
183
+ /* ── GAME LOGIC (clay) — 8x8 4bpp literal sprite art ────────────────────────
184
+ * A nose-up car in pixel value 1 (plus value $F = white windshield glint).
185
+ * draw_sprite() recolours value 1 → the wanted pen via the SCB penpal, so one
186
+ * art block paints any colour (player = yellow, obstacles = red). Each line:
187
+ * 5, then 4 nybble bytes; a final 0 byte ends the sprite. */
188
+ static unsigned char spr_car[] = {
189
+ 5, 0x01, 0x11, 0x11, 0x10, /* . 1 1 1 1 1 1 . roof */
190
+ 5, 0x01, 0x1F, 0xF1, 0x10, /* . 1 1 F F 1 1 . windshield glint */
191
+ 5, 0x11, 0x11, 0x11, 0x11, /* 1 1 1 1 1 1 1 1 cabin */
192
+ 5, 0x01, 0x11, 0x11, 0x10, /* . 1 1 1 1 1 1 . */
193
+ 5, 0x11, 0x11, 0x11, 0x11, /* 1 1 1 1 1 1 1 1 body */
194
+ 5, 0x11, 0x11, 0x11, 0x11, /* 1 1 1 1 1 1 1 1 */
195
+ 5, 0x10, 0x11, 0x11, 0x01, /* 1 . 1 1 1 1 . 1 wheels */
196
+ 5, 0x10, 0x00, 0x00, 0x01, /* 1 . . . . . . 1 */
197
+ 0
198
+ };
199
+ /* A chunky trophy/cup glyph for the result pop (pixel value 1 = body). */
200
+ static unsigned char spr_cup[] = {
201
+ 5, 0x01, 0x11, 0x11, 0x10, /* . 1 1 1 1 1 1 . cup bowl */
202
+ 5, 0x01, 0x11, 0x11, 0x10, /* . 1 1 1 1 1 1 . */
203
+ 5, 0x01, 0x11, 0x11, 0x10, /* . 1 1 1 1 1 1 . */
204
+ 5, 0x00, 0x11, 0x11, 0x00, /* . . 1 1 1 1 . . taper */
205
+ 5, 0x00, 0x01, 0x10, 0x00, /* . . . 1 1 . . . stem */
206
+ 5, 0x00, 0x01, 0x10, 0x00, /* . . . 1 1 . . . */
207
+ 5, 0x00, 0x11, 0x11, 0x00, /* . . 1 1 1 1 . . base */
208
+ 5, 0x01, 0x11, 0x11, 0x10, /* . 1 1 1 1 1 1 . */
209
+ 0
210
+ };
211
+
212
+ /* Draw an 8x8 literal sprite CENTERED on (cx,cy) at the given 8.8 scale,
213
+ * recoloured so art pixel value 1 paints `pen`. Centering matters: hpos/vpos
214
+ * are the TOP-LEFT, so a sprite scaled around its corner would slide as it
215
+ * grows — anchoring the centre keeps a growing obstacle reading as "coming at
216
+ * you" along its lane, and the result pop as a uniform swell. */
217
+ static void draw_sprite(unsigned char *data, int cx, int cy, uint8_t pen, unsigned scale) {
218
+ unsigned w = (8u * scale) >> 8;
219
+ if (w == 0) w = 1;
220
+ scb.penpal[0] = (uint8_t)((0u << 4) | pen); /* val0=transparent, val1=pen */
221
+ scb.data = data;
222
+ scb.hsize = scale;
223
+ scb.vsize = scale;
224
+ scb.hpos = cx - (int)(w >> 1);
225
+ scb.vpos = cy - (int)(w >> 1);
226
+ tgi_sprite(&scb);
227
+ }
228
+
229
+ /* ── HARDWARE IDIOM (load-bearing) — DEPTH→SCALE mapping ─────────────────────
230
+ * The pseudo-3D read lives here: an obstacle's 8.8 scale is a function of its
231
+ * screen Y. At the HORIZON it is tiny (~0.5x); as it travels down to the
232
+ * player it swells to ~1.5x — so a car genuinely LOOMS as it nears. The same
233
+ * function feeds the on-screen footprint AND the collision box (obs_px below),
234
+ * so the hardware size and the hitbox never disagree. Tune the 0x0080 floor /
235
+ * 0x0140 span to make traffic loom harder or gentler. */
236
+ #define OBS_SCALE_MIN 0x0080u /* 0.5x at the far horizon */
237
+ #define OBS_SCALE_SPAN 0x0140u /* +1.25x by the time it reaches you */
238
+ static unsigned obs_scale(int16_t y) {
239
+ /* progress 0..256 as y goes HORIZON_Y..PLAYER_Y */
240
+ int16_t num = y - HORIZON_Y;
241
+ int16_t den = PLAYER_Y - HORIZON_Y;
242
+ unsigned prog;
243
+ if (num < 0) num = 0;
244
+ if (num > den) num = den;
245
+ prog = (unsigned)((long)num * 256 / den);
246
+ return OBS_SCALE_MIN + (OBS_SCALE_SPAN * prog) / 256u;
247
+ }
248
+ /* The obstacle's current on-screen pixel footprint (8 px * scale), used for
249
+ * the same-lane "did it reach me" overlap so collision matches what's drawn. */
250
+ static unsigned obs_px(int16_t y) {
251
+ unsigned p = (8u * obs_scale(y)) >> 8;
252
+ return p ? p : 1;
253
+ }
254
+
255
+ /* Current result-pop scale: 1.0x at rest, swelling to ~2.0x at the peak and
256
+ * easing back. POP drives the SCALING idiom on the result screen. */
257
+ #define POP_SCALE_PEAK 0x0200u /* 2.0x */
258
+ static unsigned pop_scale(void) {
259
+ if (pop_timer == 0) return 0x0100u;
260
+ return 0x0100u + ((unsigned)pop_timer * (POP_SCALE_PEAK - 0x0100u)) / POP_FRAMES;
261
+ }
262
+
263
+ /* ── GAME LOGIC (clay) — number text (no sprintf: it drags in ~6KB) ── */
264
+ static char numbuf[6];
265
+ static char *fmt5(unsigned v) {
266
+ uint8_t i;
267
+ for (i = 0; i < 5; i++) { numbuf[4 - i] = (char)('0' + v % 10); v /= 10; }
268
+ numbuf[5] = 0;
269
+ return numbuf;
270
+ }
271
+
272
+ /* ── GAME LOGIC (clay) — paint the road (full redraw, every frame) ──────────
273
+ * No hardware tilemap, so the road is bars + lines: grass fill, tarmac, solid
274
+ * white edges, dashed lane dividers whose phase scrolls each frame (the road
275
+ * "moves"), and a darker horizon band for depth. Layered tones keep any one
276
+ * colour comfortably under the render-health blank threshold (>=92% one colour
277
+ * reads as "blank"). */
278
+ static void draw_road(void) {
27
279
  int16_t y;
280
+ /* grass + rumble shoulders */
281
+ tgi_setcolor(COLOR_GREEN);
282
+ tgi_bar(0, 0, 159, 101);
283
+ tgi_setcolor(COLOR_LIGHTGREEN);
284
+ for (y = (int16_t)road_phase - 8; y < 102; y += 16) {
285
+ tgi_bar(0, (unsigned)(y < 0 ? 0 : y), 6, (unsigned)(y + 6 > 101 ? 101 : y + 6));
286
+ tgi_bar(153, (unsigned)(y < 0 ? 0 : y), 159, (unsigned)(y + 6 > 101 ? 101 : y + 6));
287
+ }
288
+ /* tarmac + a darker far band for depth */
289
+ tgi_setcolor(COLOR_GREY);
290
+ tgi_bar(ROAD_L, HORIZON_Y, ROAD_R, 101);
291
+ tgi_setcolor(COLOR_DARKGREY);
292
+ tgi_bar(ROAD_L, HORIZON_Y, ROAD_R, HORIZON_Y + 10); /* horizon haze */
293
+ tgi_bar(0, 0, 159, HORIZON_Y - 1); /* top HUD band */
294
+ /* solid road edges */
295
+ tgi_setcolor(COLOR_WHITE);
296
+ tgi_line(ROAD_L, HORIZON_Y, ROAD_L, 101);
297
+ tgi_line(ROAD_R, HORIZON_Y, ROAD_R, 101);
298
+ tgi_line(0, HORIZON_Y, 159, HORIZON_Y);
299
+ /* dashed lane dividers between the 3 lanes, scrolling downward */
300
+ for (y = (int16_t)road_phase - 12; y < 102; y += 12) {
301
+ int16_t y0 = y < HORIZON_Y ? HORIZON_Y : y;
302
+ int16_t y1 = y + 6 > 101 ? 101 : y + 6;
303
+ if (y1 <= y0) continue;
304
+ tgi_bar(65, (unsigned)y0, 66, (unsigned)y1);
305
+ tgi_bar(93, (unsigned)y0, 94, (unsigned)y1);
306
+ }
307
+ }
308
+
309
+ /* ── GAME LOGIC (clay) — HUD: distance + lives across the top band ── */
310
+ static void draw_hud(void) {
311
+ tgi_setcolor(COLOR_YELLOW);
312
+ tgi_outtextxy(2, 2, "D");
313
+ tgi_outtextxy(12, 2, fmt5(dist));
314
+ tgi_setcolor(COLOR_RED);
315
+ tgi_outtextxy(120, 2, "CAR");
316
+ numbuf[0] = (char)('0' + lives); numbuf[1] = 0;
317
+ tgi_outtextxy(148, 2, numbuf);
318
+ }
319
+
320
+ /* ── GAME LOGIC (clay) — obstacle pool ── */
321
+ static void spawn_obstacle(void) {
322
+ uint8_t i;
323
+ for (i = 0; i < MAX_OBS; i++) {
324
+ if (!obs_alive[i]) {
325
+ obs_alive[i] = 1;
326
+ obs_lane[i] = (uint8_t)(rand8() % LANES);
327
+ obs_y[i] = HORIZON_Y;
328
+ return;
329
+ }
330
+ }
331
+ }
332
+
333
+ /* ── GAME LOGIC (clay) — start a run ── */
334
+ static void start_run(void) {
335
+ uint8_t i;
336
+ for (i = 0; i < MAX_OBS; i++) obs_alive[i] = 0;
337
+ player_lane = 1;
338
+ speed = 1;
339
+ dist = 0; dist_frac = 0;
340
+ lives = START_LIVES;
341
+ invuln = 0;
342
+ spawn_timer = 0;
343
+ new_record = 0;
344
+ prev_joy = 0xFF; /* the button that started the run shouldn't
345
+ * also count as the first frame's input */
346
+ sfx_tone(0, 80, 8); /* start chirp */
347
+ state = ST_PLAY;
348
+ }
349
+
350
+ /* ── GAME LOGIC (clay) — run over: result + record bookkeeping.
351
+ * Persistence choice: best DISTANCE this power-on. ── */
352
+ static void end_run(void) {
353
+ if (dist > best) {
354
+ /* ── In-session record ONLY — and here's the honest why. Real Lynx
355
+ * carts persist via a 93Cxx serial EEPROM on the cart PCB (cc65 even
356
+ * ships lynx_eeprom_read/write for it; see vendor/cc65/libsrc/lynx/
357
+ * eeprom.s). PROBED: the bundled handy core emulates CEEPROM internally
358
+ * but its libretro build exposes NO save path — retro_get_memory(
359
+ * SAVE_RAM) returns NULL/size 0, so nothing survives host.hardReset()
360
+ * and a bit-banged round-trip reads back garbage under the WASM build.
361
+ * Wiring the EEPROM to SAVE_RAM is a future core round; until then a
362
+ * fake "save" would be lying. The best DOES survive title↔play cycles
363
+ * within one power-on. ── */
364
+ best = dist;
365
+ new_record = 1;
366
+ sfx_tone(0, 60, 16); /* record fanfare */
367
+ } else {
368
+ sfx_tone(2, 220, 18); /* low defeat thump */
369
+ }
370
+ sfx_noise(14); /* crash debris */
371
+ pop_timer = POP_FRAMES; /* trigger the result SCALE POP */
372
+ state = ST_OVER;
373
+ }
374
+
375
+ /* ── GAME LOGIC (clay) — a crash ── */
376
+ static void crash(void) {
377
+ sfx_noise(12);
378
+ invuln = 45; /* blink + no-collide grace */
379
+ speed = 1; /* a wreck kills your momentum */
380
+ if (lives > 0) --lives;
381
+ if (lives == 0) end_run();
382
+ }
383
+
384
+ /* ── GAME LOGIC (clay) — per-state frames. Each runs INSIDE the canonical
385
+ * loop below: road already painted, tgi_updatedisplay not yet called. ── */
386
+
387
+ static unsigned attract_phase;
388
+
389
+ static void frame_title(uint8_t joy) {
390
+ /* attract: a lone obstacle car in the title's clear zone "approaches" via
391
+ * the SCALING idiom — the same swell traffic uses in play, shown off on the
392
+ * menu by sweeping its scale small↔large. */
393
+ unsigned t = attract_phase < 64 ? attract_phase : (127 - attract_phase);
394
+ unsigned s = OBS_SCALE_MIN + (t * (0x0220u - OBS_SCALE_MIN)) / 63u; /* small↔big */
395
+ attract_phase = (attract_phase + 2) & 127;
396
+ draw_sprite(spr_car, 79, 40, COLOR_RED, s); /* approaching car */
397
+
398
+ tgi_setcolor(COLOR_WHITE);
399
+ tgi_outtextxy(8, 20, GAME_TITLE);
400
+ tgi_setcolor(COLOR_YELLOW);
401
+ tgi_outtextxy(48, 56, "PRESS A");
402
+ tgi_setcolor(COLOR_LIGHTGREY);
403
+ tgi_outtextxy(28, 70, "BEST ");
404
+ tgi_outtextxy(68, 70, fmt5(best));
405
+ tgi_outtextxy(36, 84, "1P RACE"); /* handheld honesty */
406
+
407
+ if (JOY_BTN_1(joy) && !JOY_BTN_1(prev_joy)) start_run();
408
+ }
409
+
410
+ static void frame_over(uint8_t joy) {
411
+ unsigned ps = pop_scale();
412
+ /* the SCALE POP: the result glyph swells then eases back to 1.0x */
413
+ if (new_record) draw_sprite(spr_cup, 80, 38, COLOR_YELLOW, ps);
414
+ else draw_sprite(spr_car, 80, 38, COLOR_RED, ps);
415
+ if (pop_timer) pop_timer--;
416
+
417
+ tgi_setcolor(COLOR_DARKGREY);
418
+ tgi_bar(24, 52, 135, 98);
419
+ tgi_setcolor(COLOR_WHITE);
420
+ tgi_outtextxy(48, 56, "WRECKED");
421
+ tgi_setcolor(COLOR_LIGHTGREY);
422
+ tgi_outtextxy(28, 68, "DIST ");
423
+ tgi_outtextxy(68, 68, fmt5(dist));
424
+ if (new_record) { tgi_setcolor(COLOR_YELLOW); tgi_outtextxy(32, 80, "NEW RECORD"); }
425
+ else { tgi_setcolor(COLOR_LIGHTGREY); tgi_outtextxy(44, 80, "A = TITLE"); }
426
+ tgi_setcolor(COLOR_LIGHTGREY);
427
+ tgi_outtextxy(28, 90, "BEST ");
428
+ tgi_outtextxy(68, 90, fmt5(best));
429
+
430
+ if (JOY_BTN_1(joy) && !JOY_BTN_1(prev_joy)) state = ST_TITLE;
431
+ }
432
+
433
+ static void frame_play(uint8_t joy) {
434
+ uint8_t i;
435
+
436
+ /* ── draw: obstacle cars (SCALED by depth), the player car, HUD ──
437
+ * Draw obstacles FAR-FIRST (smallest at the horizon) then the player on
438
+ * top, so a near obstacle that overlaps the player paints over it correctly.
439
+ * Each obstacle's hardware scale is obs_scale(y) — the pseudo-3D loom. */
440
+ for (i = 0; i < MAX_OBS; i++) {
441
+ if (!obs_alive[i]) continue;
442
+ draw_sprite(spr_car, (int)lane_x[obs_lane[i]], (int)obs_y[i],
443
+ COLOR_RED, obs_scale(obs_y[i]));
444
+ }
445
+ if (!(invuln & 2)) /* crash blink: skip the player on odd frames */
446
+ draw_sprite(spr_car, (int)lane_x[player_lane], PLAYER_Y, COLOR_YELLOW, 0x0100u);
447
+ draw_hud();
448
+
449
+ /* ── update ── */
450
+ rand8(); /* tick the noise source every play frame */
451
+
452
+ /* steer: LEFT/RIGHT hop lanes (edge-detected so a held d-pad doesn't
453
+ * machine-gun across the road). */
454
+ if ((joy & JOY_LEFT_MASK) && !(prev_joy & JOY_LEFT_MASK) && player_lane > 0) {
455
+ --player_lane; sfx_tone(0, 90, 3);
456
+ }
457
+ if ((joy & JOY_RIGHT_MASK) && !(prev_joy & JOY_RIGHT_MASK) && player_lane < LANES - 1) {
458
+ ++player_lane; sfx_tone(0, 90, 3);
459
+ }
460
+ /* speed: UP accelerates, DOWN brakes (edge-detected, 1..5) */
461
+ if ((joy & JOY_UP_MASK) && !(prev_joy & JOY_UP_MASK) && speed < 5) {
462
+ ++speed; sfx_tone(2, (uint8_t)(120 - speed * 12), 4); /* engine rev */
463
+ }
464
+ if ((joy & JOY_DOWN_MASK) && !(prev_joy & JOY_DOWN_MASK) && speed > 1) {
465
+ --speed; sfx_tone(2, 180, 3); /* brake blip */
466
+ }
467
+
468
+ if (invuln > 0) --invuln;
469
+
470
+ /* distance: 1 unit per 4 scrolled "road units"; a chime every 256 units. */
471
+ dist_frac = (uint8_t)(dist_frac + speed);
472
+ if (dist_frac >= 4) {
473
+ dist_frac -= 4;
474
+ if (dist < 65535u) ++dist;
475
+ if (dist != 0 && (dist & 0xFF) == 0) sfx_tone(2, 110, 8); /* checkpoint */
476
+ }
477
+
478
+ /* scroll the road (animate the dash + rumble phase) */
479
+ road_phase = (uint8_t)(road_phase + speed);
480
+ while (road_phase >= 12) road_phase -= 12;
481
+
482
+ /* obstacles travel from the horizon toward you at road speed; despawn past
483
+ * the bottom with a pass tick. A same-lane obstacle that REACHES the player
484
+ * (its near edge overlaps PLAYER_Y) while you share its lane is a crash. */
485
+ for (i = 0; i < MAX_OBS; i++) {
486
+ if (!obs_alive[i]) continue;
487
+ obs_y[i] += speed;
488
+ if (obs_y[i] >= 104) {
489
+ obs_alive[i] = 0;
490
+ sfx_tone(2, 70, 2); /* whoosh past */
491
+ continue;
492
+ }
493
+ if (!invuln && obs_lane[i] == player_lane) {
494
+ /* the obstacle's live (scaled) footprint reaches the player row */
495
+ unsigned half = obs_px(obs_y[i]) >> 1;
496
+ if (obs_y[i] + (int)half >= CRASH_Y && obs_y[i] - (int)half <= PLAYER_Y + 4) {
497
+ obs_alive[i] = 0;
498
+ crash();
499
+ if (state != ST_PLAY) return;
500
+ }
501
+ }
502
+ }
503
+
504
+ /* spawn cadence: faster speed spawns slightly more often (denser traffic
505
+ * the quicker you push). */
506
+ if (++spawn_timer >= (uint8_t)(48 - speed * 4)) {
507
+ spawn_timer = 0;
508
+ spawn_obstacle();
509
+ }
510
+ }
511
+
512
+ void main(void) {
513
+ uint8_t joy;
28
514
 
29
515
  tgi_install(&lynx_160_102_16_tgi);
30
516
  tgi_init();
31
517
  joy_install(&lynx_stdjoy_joy);
32
- sfx_init();
33
- for (i = 0; i < MAX_OBS; i++) obs[i].alive = 0;
518
+ sfx_init(); /* MIKEY up; background melody starts on voice 1 */
519
+
520
+ state = ST_TITLE;
521
+ prev_joy = 0;
522
+ attract_phase = 0;
523
+ best = 0;
524
+ road_phase = 0;
34
525
 
35
526
  for (;;) {
36
- /* Lynx frame loop: WAIT for the blitter, then clear with a full-screen
37
- * tgi_bar (NOT tgi_clear, which leaves the back page stale on this core)
38
- * — drawing while the blitter is mid-flight loses the frame black.
39
- * (Copied from the shmup scaffold, the LYNX-1 fix.) */
527
+ /* ── HARDWARE IDIOM (load-bearing reshape gameplay around this; see TROUBLESHOOTING) ──
528
+ * CANONICAL LYNX GAME LOOP full-redraw every frame, in this order:
529
+ * 1. while (tgi_busy()) { } — WAIT for the previous frame's page flip.
530
+ * Skipping this is the #1 "Lynx screen stays blank" trap: drawing
531
+ * while the swap is pending loses the frame.
532
+ * 2. Repaint the WHOLE scene with tgi_bar/tgi_line fills — NOT
533
+ * tgi_clear() (which can leave the framebuffer stale on this
534
+ * toolchain+emulator path). TGI double-buffers; the back buffer holds
535
+ * the frame from two flips ago, so partial redraws ghost. With no
536
+ * hardware tilemap, the ROAD is repainted every frame (and the
537
+ * lane-dash phase animation IS the scroll).
538
+ * 3. Draw every object (every TGI call and every tgi_sprite() is a
539
+ * synchronous Suzy blit into the SAME draw page) — obstacles SCALED
540
+ * by depth, then the player car, then HUD text.
541
+ * 4. tgi_updatedisplay() — request the page flip at next VBL.
542
+ * 5. sfx_update() IMMEDIATELY after — MIKEY voice writes must land in
543
+ * vblank: handy reschedules its timer sweep on the spot when a voice
544
+ * CTL bit-3 write lands, and mid-frame that sweep can preempt an
545
+ * in-flight Suzy blit and eat sprites (the R57 bug — history in
546
+ * lynx_sfx.c). sfx_tone()/sfx_noise() only STAGE; sfx_update() is
547
+ * the hardware flush. */
40
548
  while (tgi_busy()) { }
41
549
 
42
- /* ── Background scene (drawn every frame). Without it the track is a
43
- * near-flat single colour and the render-health audit flags the
44
- * screen as blank. A full road with grass shoulders + animated lane
45
- * dashes keeps several distinct colours well under the threshold:
46
- * - green grass shoulders on both sides
47
- * - mid-grey tarmac with darker-grey lane bands
48
- * - white scrolling centre dashes + solid edge lines. */
49
- tgi_setcolor(COLOR_GREEN);
50
- tgi_bar(0, 0, tgi_getmaxx(), tgi_getmaxy()); /* grass base */
51
- tgi_setcolor(COLOR_GREY);
52
- tgi_bar(20, 0, 148, 101); /* tarmac */
53
- /* darker lane bands so the road isn't one flat grey */
54
- tgi_setcolor(COLOR_DARKGREY);
55
- tgi_bar(20, 0, 53, 101);
56
- tgi_bar(96, 0, 128, 101);
57
- /* solid road edges */
58
- tgi_setcolor(COLOR_WHITE);
59
- tgi_line(20, 0, 20, 101);
60
- tgi_line(148, 0, 148, 101);
61
- /* animated dashed lane dividers (scroll downward) */
62
- for (y = (int16_t)scroll - 12; y < 102; y += 12) {
63
- tgi_bar(53, (unsigned)(y < 0 ? 0 : y), 55, (unsigned)(y + 6 > 101 ? 101 : y + 6));
64
- tgi_bar(96, (unsigned)(y < 0 ? 0 : y), 98, (unsigned)(y + 6 > 101 ? 101 : y + 6));
65
- }
66
- /* grass rumble strips for extra colour texture */
67
- tgi_setcolor(COLOR_LIGHTGREEN);
68
- for (y = (int16_t)scroll - 8; y < 102; y += 16) {
69
- tgi_bar(0, (unsigned)(y < 0 ? 0 : y), 6, (unsigned)(y + 6 > 101 ? 101 : y + 6));
70
- tgi_bar(153, (unsigned)(y < 0 ? 0 : y), 159, (unsigned)(y + 6 > 101 ? 101 : y + 6));
71
- }
550
+ draw_road();
72
551
 
73
- tgi_setcolor(COLOR_YELLOW);
74
- tgi_bar((unsigned)player.x - 4, (unsigned)player.y - 4, (unsigned)player.x + 4, (unsigned)player.y + 4);
75
- tgi_setcolor(COLOR_RED);
76
- for (i = 0; i < MAX_OBS; i++) {
77
- if (obs[i].alive) tgi_bar((unsigned)obs[i].x - 4, (unsigned)obs[i].y - 4, (unsigned)obs[i].x + 4, (unsigned)obs[i].y + 4);
78
- }
79
- tgi_updatedisplay();
80
- sfx_update();
552
+ joy = joy_read(JOY_1);
81
553
 
82
- scroll += 2; if (scroll >= 12) scroll -= 12; /* advance road dashes */
554
+ if (state == ST_TITLE) frame_title(joy);
555
+ else if (state == ST_PLAY) frame_play(joy);
556
+ else frame_over(joy);
83
557
 
84
- if (game_over > 0) {
85
- game_over--;
86
- if (game_over == 0) {
87
- for (i = 0; i < MAX_OBS; i++) obs[i].alive = 0;
88
- player_lane = 1; player.x = LANE1;
89
- }
90
- continue;
91
- }
558
+ tgi_updatedisplay();
559
+ sfx_update();
92
560
 
93
- joy = joy_read(JOY_1);
94
- if (JOY_LEFT(joy) && !(prev & 4) && player_lane > 0) { player_lane--; sfx_tone(1, 70, 2); }
95
- if (JOY_RIGHT(joy) && !(prev & 8) && player_lane < 2) { player_lane++; sfx_tone(1, 70, 2); }
96
- player.x = lane_x[player_lane];
97
- prev = (JOY_LEFT(joy) ? 4 : 0) | (JOY_RIGHT(joy) ? 8 : 0);
98
-
99
- for (i = 0; i < MAX_OBS; i++) {
100
- if (!obs[i].alive) continue;
101
- obs[i].y += 2;
102
- if (obs[i].y >= 110) obs[i].alive = 0;
103
- }
104
- spawn++;
105
- if (spawn >= 30) {
106
- spawn = 0;
107
- for (i = 0; i < MAX_OBS; i++) {
108
- if (!obs[i].alive) {
109
- rng = rng * 1103515245u + 12345u;
110
- obs[i].x = lane_x[(rng >> 16) % 3];
111
- obs[i].y = 0;
112
- obs[i].alive = 1;
113
- break;
114
- }
115
- }
116
- }
117
- for (i = 0; i < MAX_OBS; i++) {
118
- if (obs[i].alive
119
- && obs[i].x > player.x - 8 && obs[i].x < player.x + 8
120
- && obs[i].y > player.y - 8 && obs[i].y < player.y + 8) {
121
- game_over = 60;
122
- sfx_noise(30);
123
- break;
124
- }
125
- }
561
+ prev_joy = joy;
126
562
  }
127
563
  }