romdevtools 0.28.0 → 0.30.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.
- package/AGENTS.md +53 -43
- package/CHANGELOG.md +91 -0
- package/README.md +3 -3
- package/examples/README.md +7 -7
- package/examples/atari2600/templates/platformer.asm +1225 -332
- package/examples/atari2600/templates/puzzle.asm +1056 -0
- package/examples/atari2600/templates/racing.asm +906 -275
- package/examples/atari2600/templates/shmup.asm +1031 -239
- package/examples/atari2600/templates/sports.asm +1135 -253
- package/examples/atari7800/templates/platformer.c +991 -156
- package/examples/atari7800/templates/puzzle.c +1091 -148
- package/examples/atari7800/templates/racing.c +952 -124
- package/examples/atari7800/templates/shmup.c +812 -134
- package/examples/atari7800/templates/sports.c +820 -184
- package/examples/c64/templates/platformer.c +879 -164
- package/examples/c64/templates/puzzle.c +855 -178
- package/examples/c64/templates/racing.c +873 -97
- package/examples/c64/templates/shmup.c +757 -161
- package/examples/c64/templates/sports.c +755 -100
- package/examples/gb/templates/platformer.c +841 -179
- package/examples/gb/templates/puzzle.c +986 -246
- package/examples/gb/templates/racing.c +754 -174
- package/examples/gb/templates/shmup.c +673 -175
- package/examples/gb/templates/sports.c +790 -159
- package/examples/gba/templates/platformer.c +626 -165
- package/examples/gba/templates/puzzle.c +519 -269
- package/examples/gba/templates/racing.c +511 -206
- package/examples/gba/templates/shmup.c +564 -179
- package/examples/gba/templates/sports.c +454 -174
- package/examples/gbc/templates/platformer.c +944 -180
- package/examples/gbc/templates/puzzle.c +363 -109
- package/examples/gbc/templates/racing.c +884 -180
- package/examples/gbc/templates/shmup.c +821 -185
- package/examples/gbc/templates/sports.c +870 -162
- package/examples/genesis/templates/platformer.c +747 -129
- package/examples/genesis/templates/puzzle.c +694 -261
- package/examples/genesis/templates/racing.c +726 -203
- package/examples/genesis/templates/shmup.c +535 -142
- package/examples/genesis/templates/sports.c +495 -158
- package/examples/gg/templates/platformer.c +880 -215
- package/examples/gg/templates/puzzle.c +875 -216
- package/examples/gg/templates/racing.c +915 -172
- package/examples/gg/templates/shmup.c +714 -191
- package/examples/gg/templates/sports.c +732 -129
- package/examples/lynx/templates/platformer.c +604 -69
- package/examples/lynx/templates/puzzle.c +498 -158
- package/examples/lynx/templates/racing.c +538 -102
- package/examples/lynx/templates/shmup.c +458 -131
- package/examples/lynx/templates/sports.c +496 -72
- package/examples/msx/platformer/main.c +649 -162
- package/examples/msx/puzzle/main.c +742 -240
- package/examples/msx/racing/main.c +669 -178
- package/examples/msx/shmup/main.c +460 -178
- package/examples/msx/sports/main.c +592 -126
- package/examples/nes/templates/platformer.c +589 -171
- package/examples/nes/templates/puzzle.c +563 -242
- package/examples/nes/templates/racing.c +502 -208
- package/examples/nes/templates/shmup.c +339 -145
- package/examples/nes/templates/sports.c +341 -183
- package/examples/pce/platformer/main.c +874 -205
- package/examples/pce/puzzle/main.c +802 -287
- package/examples/pce/racing/main.c +783 -208
- package/examples/pce/shmup/main.c +638 -212
- package/examples/pce/sports/main.c +586 -169
- package/examples/porting-across-platforms/README.md +1 -1
- package/examples/sms/templates/platformer.c +762 -177
- package/examples/sms/templates/puzzle.c +752 -212
- package/examples/sms/templates/racing.c +808 -145
- package/examples/sms/templates/shmup.c +599 -162
- package/examples/sms/templates/sports.c +630 -122
- package/examples/snes/templates/music_demo.c +7 -0
- package/examples/snes/templates/platformer-data.asm +123 -24
- package/examples/snes/templates/platformer-hdr.asm +57 -0
- package/examples/snes/templates/platformer.c +586 -165
- package/examples/snes/templates/puzzle-data.asm +116 -21
- package/examples/snes/templates/puzzle-hdr.asm +57 -0
- package/examples/snes/templates/puzzle.c +614 -235
- package/examples/snes/templates/racing-data.asm +390 -32
- package/examples/snes/templates/racing-hdr.asm +57 -0
- package/examples/snes/templates/racing.c +807 -196
- package/examples/snes/templates/shmup-data.asm +87 -29
- package/examples/snes/templates/shmup-hdr.asm +57 -0
- package/examples/snes/templates/shmup.c +459 -198
- package/examples/snes/templates/sports-data.asm +48 -2
- package/examples/snes/templates/sports-hdr.asm +57 -0
- package/examples/snes/templates/sports.c +414 -163
- package/package.json +12 -12
- package/src/cores/wasm/bluemsx_libretro.js +1 -1
- package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
- package/src/cores/wasm/fceumm_libretro.js +1 -1
- package/src/cores/wasm/fceumm_libretro.wasm +0 -0
- package/src/cores/wasm/gambatte_libretro.js +1 -1
- package/src/cores/wasm/gambatte_libretro.wasm +0 -0
- package/src/cores/wasm/geargrafx_libretro.js +1 -1
- package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
- package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
- package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
- package/src/cores/wasm/handy_libretro.js +1 -1
- package/src/cores/wasm/handy_libretro.wasm +0 -0
- package/src/cores/wasm/mgba_libretro.js +1 -1
- package/src/cores/wasm/mgba_libretro.wasm +0 -0
- package/src/cores/wasm/prosystem_libretro.js +1 -1
- package/src/cores/wasm/prosystem_libretro.wasm +0 -0
- package/src/cores/wasm/snes9x_libretro.js +1 -1
- package/src/cores/wasm/snes9x_libretro.wasm +0 -0
- package/src/cores/wasm/stella2014_libretro.js +1 -1
- package/src/cores/wasm/stella2014_libretro.wasm +0 -0
- package/src/cores/wasm/vice_x64_libretro.js +1 -1
- package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
- package/src/host/LibretroHost.js +84 -8
- package/src/http/tool-registry.js +11 -11
- package/src/mcp/tools/cheats.js +2 -1
- package/src/mcp/tools/frame.js +3 -2
- package/src/mcp/tools/index.js +3 -3
- package/src/mcp/tools/input.js +5 -4
- package/src/mcp/tools/lifecycle.js +6 -4
- package/src/mcp/tools/memory.js +131 -24
- package/src/mcp/tools/platform-docs.js +1 -1
- package/src/mcp/tools/preview-tile.js +6 -2
- package/src/mcp/tools/project.js +1098 -130
- package/src/mcp/tools/record.js +6 -7
- package/src/mcp/tools/rom-id.js +5 -1
- package/src/mcp/tools/run-until.js +12 -4
- package/src/mcp/tools/snippets.js +6 -6
- package/src/mcp/tools/sprite-pipeline.js +14 -2
- package/src/mcp/tools/state.js +2 -1
- package/src/mcp/tools/tile-inspect.js +8 -1
- package/src/mcp/tools/toolchain.js +12 -1
- package/src/mcp/tools/watch-memory.js +53 -10
- package/src/observer/bus.js +73 -0
- package/src/observer/livestream.html +4 -2
- package/src/observer/tool-wrap.js +17 -14
- package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +32 -3
- package/src/platforms/atari7800/MENTAL_MODEL.md +5 -5
- package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
- package/src/platforms/c64/MENTAL_MODEL.md +11 -4
- package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
- package/src/platforms/gb/MENTAL_MODEL.md +3 -3
- package/src/platforms/gb/TROUBLESHOOTING.md +61 -8
- package/src/platforms/gb/lib/c/README.md +10 -11
- package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
- package/src/platforms/gb/lib/c/patch-header.js +13 -3
- package/src/platforms/gba/MENTAL_MODEL.md +4 -4
- package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
- package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
- package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
- package/src/platforms/gbc/MENTAL_MODEL.md +4 -4
- package/src/platforms/gbc/TROUBLESHOOTING.md +4 -4
- package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gbc/lib/c/README.md +10 -11
- package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
- package/src/platforms/gbc/lib/c/patch-header.js +13 -3
- package/src/platforms/genesis/MENTAL_MODEL.md +3 -3
- package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
- package/src/platforms/gg/MENTAL_MODEL.md +4 -4
- package/src/platforms/gg/TROUBLESHOOTING.md +3 -3
- package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gg/lib/c/joypad_read.c +29 -0
- package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
- package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
- package/src/platforms/msx/MENTAL_MODEL.md +5 -5
- package/src/platforms/msx/TROUBLESHOOTING.md +2 -2
- package/src/platforms/msx/lib/c/msx_hw.h +1 -0
- package/src/platforms/msx/lib/c/msx_vdp.c +25 -0
- package/src/platforms/nes/MENTAL_MODEL.md +2 -2
- package/src/platforms/nes/lib/c/nes_runtime.c +149 -34
- package/src/platforms/nes/lib/c/nes_runtime.h +34 -1
- package/src/platforms/pce/MENTAL_MODEL.md +5 -5
- package/src/platforms/pce/TROUBLESHOOTING.md +1 -1
- package/src/platforms/pce/lib/c/pce_hw.h +11 -0
- package/src/platforms/pce/lib/c/pce_video.c +32 -0
- package/src/platforms/sms/MENTAL_MODEL.md +6 -6
- package/src/platforms/snes/MENTAL_MODEL.md +2 -2
- package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
- package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
- package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
- package/src/toolchains/index.js +27 -11
|
@@ -1,4 +1,54 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
#define
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
/*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
}
|