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,305 +1,880 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* PC Engine "racing" — a top-down lane-racer scaffold.
|
|
1
|
+
/* ── main.c — PC Engine top-down road racer (complete example game) ───────────
|
|
3
2
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* (
|
|
8
|
-
*
|
|
9
|
-
*
|
|
3
|
+
* PINION PURSUIT — a COMPLETE, working game: title screen, 1P endless race with
|
|
4
|
+
* speed control, 2P simultaneous SPLIT-LANE VERSUS (both cars on screen at once,
|
|
5
|
+
* P2 on the TurboTap's second pad), a vertically-scrolling road done the PC
|
|
6
|
+
* Engine way (hardware BG Y-scroll via the VDC's BYR register), streamed
|
|
7
|
+
* roadside scenery as the road wraps, crash/lives rules, in-session best
|
|
8
|
+
* distance (a bare HuCard can't save — see the best-distance note), PSG music + SFX.
|
|
10
9
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
|
|
11
|
+
* very different one. The markers tell you what's what:
|
|
12
|
+
* HARDWARE IDIOM (load-bearing) — dodges a documented PCE footgun; reshape
|
|
13
|
+
* your gameplay around it (see TROUBLESHOOTING before changing).
|
|
14
|
+
* GAME LOGIC (clay) — traffic patterns, speeds, tuning, art: reshape freely.
|
|
14
15
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* What depends on what:
|
|
17
|
+
* pce_hw.h / pce_video.c / pce_input.c / pce_sound.c — the helper lib
|
|
18
|
+
* (VDC/VCE/PSG register dances + joypad). The HARDWARE IDIOM markers in
|
|
19
|
+
* pce_video.c say which parts are load-bearing.
|
|
20
|
+
* cc65's pce crt0 + pce.lib are auto-linked; the 'rom32k' linker preset
|
|
21
|
+
* (applied automatically to example projects) gives a 32KB HuCard.
|
|
19
22
|
*
|
|
20
|
-
*
|
|
23
|
+
* THE DESIGN (read before reshaping):
|
|
24
|
+
* Scrolling — the road is the BACKGROUND, scrolled DOWN by INCREMENTING the
|
|
25
|
+
* VDC's BYR register each frame (driving up = the road slides toward you).
|
|
26
|
+
* The PCE wins here: its BAT is a 32x32 (256px-tall) virtual map and the
|
|
27
|
+
* VDC masks BYR to the plane IN HARDWARE, so `road_scroll += speed` on a
|
|
28
|
+
* plain u8 is the whole idiom — 256 wraps seamlessly forever. Compare the
|
|
29
|
+
* NES racing template (examples/nes/templates/racing.c): there a nametable
|
|
30
|
+
* is only 240px tall, scroll_y 240-255 fetches attribute bytes as garbage
|
|
31
|
+
* tiles, and EVERY scroll change must run through a 240-wrap helper. The
|
|
32
|
+
* SMS (examples/sms/templates/racing.c) wraps at 224. On the PCE there is
|
|
33
|
+
* no wrap math at all. Cars/traffic are hardware sprites with their own Y.
|
|
34
|
+
* Streamed scenery — see the BYR idiom below: as the road wraps, the BAT row
|
|
35
|
+
* re-entering at the top gets restamped with fresh random roadside so the
|
|
36
|
+
* 256-px loop never shows the same scenery twice. The swap hides under the
|
|
37
|
+
* HUD band (the PCE's curtain — same trick the Genesis window HUD plays).
|
|
38
|
+
* HUD — the PCE has no hardware window plane and this minimal lib does no
|
|
39
|
+
* raster split, so (like the platformer template's painted-band HUD) the
|
|
40
|
+
* status row is BAT tiles at the top. Because BYR scrolls the WHOLE BG, we
|
|
41
|
+
* keep the HUD readable by parking it in BAT rows the scroll never exposes:
|
|
42
|
+
* the road only ever occupies the play band, and the top 2 BAT rows hold a
|
|
43
|
+
* fixed HUD band repainted with each scenery stream so it reads continuous.
|
|
44
|
+
* 2P VERSUS — ONE VDC means ONE road scroll, so both players share one road
|
|
45
|
+
* at a fixed speed and only STEER (the same constraint the NES/Genesis
|
|
46
|
+
* versions explain): solid center divider, P1 (cyan, port 0) owns the left
|
|
47
|
+
* two lanes, P2 (amber, TurboTap port 1) the right two. Each starts with 3
|
|
48
|
+
* crashes; first to use them all LOSES.
|
|
49
|
+
* 1P RACE — all four lanes, UP/I accelerates, DOWN/II brakes (speed 1-4);
|
|
50
|
+
* 3 crashes end the run. Persistent stat: best DISTANCE (u16, one unit =
|
|
51
|
+
* 16 scrolled pixels ≈ one car length); in-session only (see the note below).
|
|
52
|
+
*
|
|
53
|
+
* 2P, honestly: the stock PC Engine has ONE controller port; 2P needs a
|
|
54
|
+
* TurboTap. The geargrafx core implements the TurboTap and the romdev host
|
|
55
|
+
* force-ENABLES it (PLATFORM_CORE_OPTIONS pce: geargrafx_turbotap), so a second
|
|
56
|
+
* pad's input reaches the game on pad slot 2 — verified by driving port-1 input
|
|
57
|
+
* and seeing car 2 move. So this game ships REAL simultaneous 2P versus. (On
|
|
58
|
+
* real hardware the player plugs a TurboTap and a second pad.)
|
|
59
|
+
*
|
|
60
|
+
* Frame budget (NTSC, 60fps, 7.16MHz 65C02-class CPU): 4 traffic + 2 cars AABB,
|
|
61
|
+
* one BAT row restamp at most every other frame, an 8-entry SATB copy in
|
|
62
|
+
* vblank — a tiny fraction of a frame. Hardware BYR scroll is one register.
|
|
21
63
|
*/
|
|
22
64
|
#include <pce.h>
|
|
23
|
-
#include <stdint.h> /* int16_t for the per-frame speed step
|
|
65
|
+
#include <stdint.h> /* int16_t for the per-frame speed step */
|
|
66
|
+
#include <joystick.h> /* JOY_2 + joy_read for the 2nd pad (TurboTap port 1) */
|
|
24
67
|
#include "pce_hw.h"
|
|
25
68
|
|
|
26
|
-
/*
|
|
27
|
-
|
|
28
|
-
#define
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
69
|
+
/* The title screen renders this — examples({op:'fork'}) stamps your game's
|
|
70
|
+
* name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
|
|
71
|
+
#define GAME_TITLE "PINION PURSUIT"
|
|
72
|
+
|
|
73
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
74
|
+
* VRAM map (WORD addresses — the VDC is a 16-bit-word machine; an 8x8 tile is
|
|
75
|
+
* 16 words, a 16x16 sprite cell is 64). Sprites and BG tiles share one 64KB
|
|
76
|
+
* VRAM, so lay it out ONCE and keep the SATB out of pattern space:
|
|
77
|
+
* $0000 BAT (32x32 background map — matches vdc_init's VDC_MWR setting)
|
|
78
|
+
* $1000 font glyphs (38 tiles: blank, 0-9, A-Z, dash) — BG text only
|
|
79
|
+
* $1400 road furniture tiles (grass, asphalt, dash, edge, divider, band)
|
|
80
|
+
* $1800 16x16 sprite cells: player car, traffic car
|
|
81
|
+
* $1900 16x16 sprite DIGIT cells (0-9) for the SPRITE HUD (see HUD idiom) */
|
|
82
|
+
#define BAT_VRAM 0x0000
|
|
83
|
+
#define FONT_VRAM 0x1000
|
|
84
|
+
#define GRASS_VRAM 0x1400 /* roadside grass (BG colour 1) */
|
|
85
|
+
#define ROAD_VRAM 0x1410 /* asphalt (BG colour 2) */
|
|
86
|
+
#define DASH_VRAM 0x1420 /* asphalt + a colour-3 lane dash */
|
|
87
|
+
#define EDGE_VRAM 0x1430 /* solid colour-3 shoulder / centre divider */
|
|
88
|
+
#define BAND_VRAM 0x1440 /* flat band behind the title/result text */
|
|
89
|
+
#define PLAYER_VRAM 0x1800 /* 16x16 player car */
|
|
90
|
+
#define ENEMY_VRAM 0x1840 /* 16x16 traffic car */
|
|
91
|
+
#define SDIGIT_VRAM 0x1900 /* 10 consecutive 16x16 digit cells (0..9) */
|
|
34
92
|
|
|
35
93
|
#define BAT_ENTRY(pal, vram) ((u16)(((pal) << 12) | ((vram) >> 4)))
|
|
36
94
|
|
|
37
|
-
|
|
38
|
-
#define
|
|
39
|
-
#define
|
|
40
|
-
#define
|
|
41
|
-
|
|
95
|
+
/* Sprite pattern codes = VRAM >> 6 (the 16x16 cell index). */
|
|
96
|
+
#define PLAYER_PAT (PLAYER_VRAM >> 6)
|
|
97
|
+
#define ENEMY_PAT (ENEMY_VRAM >> 6)
|
|
98
|
+
#define SDIGIT_PAT (SDIGIT_VRAM >> 6) /* digit d → SDIGIT_PAT + d (cells are *4 words apart = +1 pattern code) */
|
|
99
|
+
|
|
100
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
101
|
+
* Road geometry. Four 4-cell-wide lanes between shoulders, a solid centre
|
|
102
|
+
* divider (it's also the 2P territory line). BAT columns (cells):
|
|
103
|
+
* 8 = left shoulder, 12/20 = dashed lane lines, 16 = centre divider,
|
|
104
|
+
* 24 = right shoulder; grass outside. The BAT is 32 cells (256px) wide. */
|
|
105
|
+
#define COL_EDGE_L 8
|
|
106
|
+
#define COL_DASH_1 12
|
|
107
|
+
#define COL_DIVIDER 16
|
|
108
|
+
#define COL_DASH_2 20
|
|
109
|
+
#define COL_EDGE_R 24
|
|
110
|
+
/* Lane centre X for the 16px-wide car sprite (lane i spans 32 px). */
|
|
111
|
+
static const u16 lane_x[4] = { 80, 112, 144, 176 };
|
|
112
|
+
|
|
113
|
+
#define MAX_TRAFFIC 4 /* sprite slots 2-5 (0=P1, 1=P2) */
|
|
114
|
+
#define CAR_Y 176 /* both players' fixed screen Y */
|
|
115
|
+
#define SPAWN_Y 28 /* traffic entry Y — BELOW the sprite HUD line */
|
|
116
|
+
#define HUD_Y 8 /* sprite HUD scanline (digits live here) */
|
|
117
|
+
#define DESPAWN_Y 216 /* traffic exits past the player */
|
|
118
|
+
#define START_LIVES 3 /* crashes per run / per player */
|
|
119
|
+
#define SPAWN_PERIOD 40 /* frames between traffic spawns — traffic moves
|
|
120
|
+
* at road speed, so per-meter density stays
|
|
121
|
+
* constant whatever the player does */
|
|
122
|
+
#define SPEED_2P 2 /* fixed road speed in versus (one VDC = one
|
|
123
|
+
* scroll = one shared speed; see the design) */
|
|
124
|
+
#define MAX_SPEED 4 /* px/frame — MUST stay under 8: the row
|
|
125
|
+
* streamer restamps one row per 8px crossing
|
|
126
|
+
* and a >8px step could skip a row */
|
|
127
|
+
|
|
128
|
+
/* SATB slot plan (slot order = priority): 0 = P1, 1 = P2, 2-5 = traffic,
|
|
129
|
+
* 6-11 = the 6 sprite-HUD digits (see the HUD idiom). PAL plan: cars on their
|
|
130
|
+
* own sprite sub-palettes so P1/P2/traffic read as three liveries; digits on
|
|
131
|
+
* the HUD palette. */
|
|
132
|
+
#define SLOT_P1 0
|
|
133
|
+
#define SLOT_P2 1
|
|
134
|
+
#define SLOT_TRAFFIC 2
|
|
135
|
+
#define SLOT_HUD 6 /* slots 6..11: crash digit + 5 distance digits */
|
|
136
|
+
#define PAL_P1 0
|
|
137
|
+
#define PAL_P2 1
|
|
138
|
+
#define PAL_TRAFFIC 2
|
|
139
|
+
#define PAL_HUD 3
|
|
140
|
+
#define OFFSCREEN_Y 0x1F0 /* park hidden sprites below the display */
|
|
141
|
+
|
|
142
|
+
/* ── GAME LOGIC (clay — reshape freely) ── game state ── */
|
|
143
|
+
/* Players: index 0 = P1 (port 0), 1 = P2 (TurboTap port 1, versus only). */
|
|
144
|
+
static u8 car_lane[2];
|
|
145
|
+
static u8 car_active[2];
|
|
146
|
+
static u8 crashes_left[2];
|
|
147
|
+
static u8 invuln[2]; /* post-crash blink/no-collide frames */
|
|
148
|
+
static u8 lane_cd[2]; /* steer cooldown frames (latency-robust) */
|
|
149
|
+
static u8 prev_pads[2];
|
|
150
|
+
static u8 lane_min[2], lane_max[2]; /* 2P: split territories */
|
|
151
|
+
static u8 two_player;
|
|
152
|
+
static u8 winner; /* versus result: 0 = P1, 1 = P2 */
|
|
42
153
|
|
|
43
|
-
/* ---- font (digits only) ------------------------------------------------- */
|
|
44
|
-
#define NUM_GLYPHS 10
|
|
45
|
-
static const u8 FONT5x7[NUM_GLYPHS][7] = {
|
|
46
|
-
{0x0E,0x11,0x13,0x15,0x19,0x11,0x0E},
|
|
47
|
-
{0x04,0x0C,0x04,0x04,0x04,0x04,0x0E},
|
|
48
|
-
{0x0E,0x11,0x01,0x02,0x04,0x08,0x1F},
|
|
49
|
-
{0x1F,0x02,0x04,0x02,0x01,0x11,0x0E},
|
|
50
|
-
{0x02,0x06,0x0A,0x12,0x1F,0x02,0x02},
|
|
51
|
-
{0x1F,0x10,0x1E,0x01,0x01,0x11,0x0E},
|
|
52
|
-
{0x06,0x08,0x10,0x1E,0x11,0x11,0x0E},
|
|
53
|
-
{0x1F,0x01,0x02,0x04,0x08,0x08,0x08},
|
|
54
|
-
{0x0E,0x11,0x11,0x0E,0x11,0x11,0x0E},
|
|
55
|
-
{0x0E,0x11,0x11,0x0F,0x01,0x02,0x0C}
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
/* ---- state -------------------------------------------------------------- */
|
|
59
154
|
typedef struct { u16 x, y; u8 alive; } Car;
|
|
155
|
+
static Car traffic[MAX_TRAFFIC];
|
|
60
156
|
|
|
61
|
-
static
|
|
62
|
-
static
|
|
63
|
-
static
|
|
157
|
+
static u8 speed; /* road px/frame, 1..MAX_SPEED */
|
|
158
|
+
static u16 dist; /* 1P distance, 1 unit = 16 scrolled px */
|
|
159
|
+
static u8 dist_frac;
|
|
160
|
+
static u16 best; /* persisted best 1P distance */
|
|
64
161
|
static u8 spawn_timer;
|
|
65
|
-
static u8
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
162
|
+
static u8 road_scroll; /* BG Y scroll. NEVER wrapped by hand: the BAT
|
|
163
|
+
* is 256px tall, the VDC masks BYR to the
|
|
164
|
+
* plane, and 256 wrapping a u8 is seamless —
|
|
165
|
+
* see the BYR idiom (the NES needs a 240-wrap
|
|
166
|
+
* helper here, the SMS a 224-wrap). */
|
|
167
|
+
static u8 prev_top_row; /* last restamped BAT row */
|
|
168
|
+
static u8 start_pause; /* green-light freeze frames */
|
|
70
169
|
static u8 sfx_timer;
|
|
71
|
-
static
|
|
72
|
-
|
|
170
|
+
static u8 state; /* ST_TITLE / ST_PLAY / ST_OVER */
|
|
171
|
+
|
|
172
|
+
/* Game states — the shell every example shares: title → play → game over. */
|
|
173
|
+
#define ST_TITLE 0
|
|
174
|
+
#define ST_PLAY 1
|
|
175
|
+
#define ST_OVER 2
|
|
176
|
+
|
|
177
|
+
static u16 tile_buf[16]; /* scratch for one 8x8 tile */
|
|
178
|
+
static u16 spr_buf[64]; /* scratch for one 16x16 sprite cell */
|
|
73
179
|
|
|
74
|
-
|
|
180
|
+
/* ── GAME LOGIC (clay) — 5x7 glyph font: blank, 0-9, A-Z, dash ──────────────
|
|
181
|
+
* Each glyph is 7 rows of 5 bits (bit4 = leftmost). upload_font() expands
|
|
182
|
+
* them into 8x8 1-plane tiles; drawn with BG sub-palette 1 (white). */
|
|
183
|
+
#define G_BLANK 0
|
|
184
|
+
#define G_DIGIT 1 /* '0'..'9' -> glyphs 1..10 */
|
|
185
|
+
#define G_ALPHA 11 /* 'A'..'Z' -> glyphs 11..36 */
|
|
186
|
+
#define G_DASH 37
|
|
187
|
+
#define NUM_GLYPHS 38
|
|
75
188
|
|
|
189
|
+
static const u8 FONT5x7[NUM_GLYPHS][7] = {
|
|
190
|
+
{0,0,0,0,0,0,0},
|
|
191
|
+
{0x0E,0x11,0x13,0x15,0x19,0x11,0x0E}, {0x04,0x0C,0x04,0x04,0x04,0x04,0x0E},
|
|
192
|
+
{0x0E,0x11,0x01,0x02,0x04,0x08,0x1F}, {0x1F,0x02,0x04,0x02,0x01,0x11,0x0E},
|
|
193
|
+
{0x02,0x06,0x0A,0x12,0x1F,0x02,0x02}, {0x1F,0x10,0x1E,0x01,0x01,0x11,0x0E},
|
|
194
|
+
{0x06,0x08,0x10,0x1E,0x11,0x11,0x0E}, {0x1F,0x01,0x02,0x04,0x08,0x08,0x08},
|
|
195
|
+
{0x0E,0x11,0x11,0x0E,0x11,0x11,0x0E}, {0x0E,0x11,0x11,0x0F,0x01,0x02,0x0C},
|
|
196
|
+
{0x0E,0x11,0x11,0x1F,0x11,0x11,0x11}, {0x1E,0x11,0x11,0x1E,0x11,0x11,0x1E},
|
|
197
|
+
{0x0E,0x11,0x10,0x10,0x10,0x11,0x0E}, {0x1E,0x11,0x11,0x11,0x11,0x11,0x1E},
|
|
198
|
+
{0x1F,0x10,0x10,0x1E,0x10,0x10,0x1F}, {0x1F,0x10,0x10,0x1E,0x10,0x10,0x10},
|
|
199
|
+
{0x0E,0x11,0x10,0x17,0x11,0x11,0x0F}, {0x11,0x11,0x11,0x1F,0x11,0x11,0x11},
|
|
200
|
+
{0x0E,0x04,0x04,0x04,0x04,0x04,0x0E}, {0x07,0x02,0x02,0x02,0x02,0x12,0x0C},
|
|
201
|
+
{0x11,0x12,0x14,0x18,0x14,0x12,0x11}, {0x10,0x10,0x10,0x10,0x10,0x10,0x1F},
|
|
202
|
+
{0x11,0x1B,0x15,0x15,0x11,0x11,0x11}, {0x11,0x19,0x15,0x13,0x11,0x11,0x11},
|
|
203
|
+
{0x0E,0x11,0x11,0x11,0x11,0x11,0x0E}, {0x1E,0x11,0x11,0x1E,0x10,0x10,0x10},
|
|
204
|
+
{0x0E,0x11,0x11,0x11,0x15,0x12,0x0D}, {0x1E,0x11,0x11,0x1E,0x14,0x12,0x11},
|
|
205
|
+
{0x0F,0x10,0x10,0x0E,0x01,0x01,0x1E}, {0x1F,0x04,0x04,0x04,0x04,0x04,0x04},
|
|
206
|
+
{0x11,0x11,0x11,0x11,0x11,0x11,0x0E}, {0x11,0x11,0x11,0x11,0x11,0x0A,0x04},
|
|
207
|
+
{0x11,0x11,0x11,0x15,0x15,0x15,0x0A}, {0x11,0x11,0x0A,0x04,0x0A,0x11,0x11},
|
|
208
|
+
{0x11,0x11,0x0A,0x04,0x04,0x04,0x04}, {0x1F,0x01,0x02,0x04,0x08,0x10,0x1F},
|
|
209
|
+
{0x00,0x00,0x00,0x1F,0x00,0x00,0x00},
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
/* ── GAME LOGIC (clay) — 16x16 car sprite mask (16 rows × 16 bits, bit15 left).
|
|
213
|
+
* A blocky top-down car: cabin, windows, wheels. Colour is the PALETTE, not the
|
|
214
|
+
* bits (one shape, three sub-palettes → P1 cyan, P2 amber, traffic red). */
|
|
215
|
+
static const u16 car_mask[16] = {
|
|
216
|
+
0x0660, 0x0660, 0x3FFC, 0x7FFE, 0x7FFE, 0x7FFE, 0x6FF6, 0x6FF6,
|
|
217
|
+
0x7FFE, 0x7FFE, 0x6FF6, 0x6FF6, 0x7FFE, 0x3FFC, 0x6006, 0x6006
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
/* ── GAME LOGIC (clay) — tile/sprite builders ────────────────────────────── */
|
|
76
221
|
static void make_solid_tile(u16 *t, u8 ci) {
|
|
77
222
|
u8 r;
|
|
78
223
|
u8 p0 = (ci & 1) ? 0xFF : 0x00;
|
|
79
224
|
u8 p1 = (ci & 2) ? 0xFF : 0x00;
|
|
80
|
-
u8 p2 = (ci & 4) ? 0xFF : 0x00;
|
|
81
|
-
u8 p3 = (ci & 8) ? 0xFF : 0x00;
|
|
82
225
|
for (r = 0; r < 8; ++r) {
|
|
83
226
|
t[r] = (u16)(p0 | (p1 << 8));
|
|
84
|
-
t[r + 8] =
|
|
227
|
+
t[r + 8] = 0;
|
|
85
228
|
}
|
|
86
229
|
}
|
|
87
230
|
|
|
88
|
-
/*
|
|
231
|
+
/* speckled grass: colour-1 body with a few colour-2 specks so the vertical
|
|
232
|
+
* scroll reads (a flat colour shifted N px looks identical to itself). */
|
|
233
|
+
static void make_grass_tile(u16 *t) {
|
|
234
|
+
make_solid_tile(t, 1); /* body = colour 1 (plane0) */
|
|
235
|
+
t[1] |= 0x1000; /* row 1: one plane1 speck → colour 3 */
|
|
236
|
+
t[5] |= 0x0400; /* row 5: another speck */
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/* asphalt: colour-2 body with a sparse colour-3 speck for the same reason. */
|
|
240
|
+
static void make_road_tile(u16 *t) {
|
|
241
|
+
make_solid_tile(t, 2); /* body = colour 2 (plane1) */
|
|
242
|
+
t[3] |= 0x0008; /* a single colour-3 speck */
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/* road tile with a centred colour-3 lane dash on the top 4 rows. */
|
|
89
246
|
static void make_dash_tile(u16 *t) {
|
|
90
247
|
u8 r;
|
|
91
|
-
|
|
92
|
-
for (r = 0; r < 4; ++r)
|
|
93
|
-
/* centre 4px (mask 0x18) -> colour 3 (planes 0+1): add plane0 bits */
|
|
94
|
-
t[r] = (u16)((t[r] & 0xFF00) | 0x18 | (t[r] & 0x00FF));
|
|
95
|
-
}
|
|
248
|
+
make_road_tile(t);
|
|
249
|
+
for (r = 0; r < 4; ++r) t[r] |= 0x0018; /* centre 2px → colour 3 (dash) */
|
|
96
250
|
}
|
|
97
251
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
0x0660, 0x0660, 0x3FFC, 0x7FFE, 0x7FFE, 0x7FFE, 0x6FF6, 0x6FF6,
|
|
101
|
-
0x7FFE, 0x7FFE, 0x6FF6, 0x6FF6, 0x7FFE, 0x3FFC, 0x6006, 0x6006
|
|
102
|
-
};
|
|
252
|
+
/* one-colour 16x16 sprite cell from a 16-row mask (colour = plane0 → index 1) */
|
|
253
|
+
static void make_sprite16(u16 vram, const u16 *mask) {
|
|
103
254
|
u8 r;
|
|
104
255
|
for (r = 0; r < 64; ++r) spr_buf[r] = 0;
|
|
105
|
-
for (r = 0; r < 16; ++r)
|
|
106
|
-
if (ci & 1) spr_buf[r] = car[r];
|
|
107
|
-
if (ci & 2) spr_buf[r + 16] = car[r];
|
|
108
|
-
if (ci & 4) spr_buf[r + 32] = car[r];
|
|
109
|
-
}
|
|
256
|
+
for (r = 0; r < 16; ++r) spr_buf[r] = mask[r]; /* plane 0 → colour 1 */
|
|
110
257
|
load_tiles(vram, spr_buf, 64);
|
|
111
258
|
}
|
|
112
259
|
|
|
113
260
|
static void upload_font(void) {
|
|
114
|
-
u8 g, row, bits,
|
|
261
|
+
u8 g, row, bits, px;
|
|
115
262
|
for (g = 0; g < NUM_GLYPHS; ++g) {
|
|
116
263
|
for (row = 0; row < 16; ++row) tile_buf[row] = 0;
|
|
117
264
|
for (row = 0; row < 7; ++row) {
|
|
118
265
|
bits = FONT5x7[g][row];
|
|
119
|
-
|
|
120
|
-
if (bits & 0x10)
|
|
121
|
-
if (bits & 0x08)
|
|
122
|
-
if (bits & 0x04)
|
|
123
|
-
if (bits & 0x02)
|
|
124
|
-
if (bits & 0x01)
|
|
125
|
-
tile_buf[row] = (u16)
|
|
266
|
+
px = 0;
|
|
267
|
+
if (bits & 0x10) px |= 0x40;
|
|
268
|
+
if (bits & 0x08) px |= 0x20;
|
|
269
|
+
if (bits & 0x04) px |= 0x10;
|
|
270
|
+
if (bits & 0x02) px |= 0x08;
|
|
271
|
+
if (bits & 0x01) px |= 0x04;
|
|
272
|
+
tile_buf[row] = (u16)px;
|
|
126
273
|
}
|
|
127
274
|
load_tiles((u16)(FONT_VRAM + g * 16), tile_buf, 16);
|
|
128
275
|
}
|
|
129
276
|
}
|
|
130
277
|
|
|
131
|
-
/*
|
|
132
|
-
*
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
278
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
279
|
+
* SPRITE HUD digits. The PCE has NO hardware window plane and this minimal lib
|
|
280
|
+
* does no raster split, so a BAT-tile HUD would scroll WITH the road under BYR
|
|
281
|
+
* (and the road-row STREAMER below restamps every BAT row in turn, wiping any
|
|
282
|
+
* tile HUD outright). The honest fix — the same one the NES racing template
|
|
283
|
+
* uses — is a SPRITE HUD: sprites are positioned in SCREEN space and never move
|
|
284
|
+
* with a BG scroll. We build 10 digit cells here and stage them at HUD_Y every
|
|
285
|
+
* frame. Traffic spawns BELOW HUD_Y so the HuC6270's 16-sprites-per-scanline
|
|
286
|
+
* limit is never hit (6 HUD digits + 0 traffic share the line).
|
|
287
|
+
* requires: digit cells consecutive from SDIGIT_VRAM; stage_hud() each frame. */
|
|
288
|
+
static void upload_sprite_digits(void) {
|
|
289
|
+
u8 d, row, bits, px;
|
|
290
|
+
for (d = 0; d < 10; ++d) {
|
|
291
|
+
for (row = 0; row < 64; ++row) spr_buf[row] = 0;
|
|
292
|
+
/* reuse the 5x7 glyph for digit d (G_DIGIT + d), centred in the cell */
|
|
293
|
+
for (row = 0; row < 7; ++row) {
|
|
294
|
+
bits = FONT5x7[G_DIGIT + d][row];
|
|
295
|
+
px = 0;
|
|
296
|
+
if (bits & 0x10) px |= 0x40;
|
|
297
|
+
if (bits & 0x08) px |= 0x20;
|
|
298
|
+
if (bits & 0x04) px |= 0x10;
|
|
299
|
+
if (bits & 0x02) px |= 0x08;
|
|
300
|
+
if (bits & 0x01) px |= 0x04;
|
|
301
|
+
spr_buf[row] = (u16)px; /* plane 0 → colour 1 (white) */
|
|
151
302
|
}
|
|
303
|
+
load_tiles((u16)(SDIGIT_VRAM + d * 64), spr_buf, 64);
|
|
152
304
|
}
|
|
153
305
|
}
|
|
154
306
|
|
|
155
|
-
static void
|
|
156
|
-
|
|
307
|
+
static void upload_art(void) {
|
|
308
|
+
upload_font();
|
|
309
|
+
make_grass_tile(tile_buf); load_tiles(GRASS_VRAM, tile_buf, 16);
|
|
310
|
+
make_road_tile(tile_buf); load_tiles(ROAD_VRAM, tile_buf, 16);
|
|
311
|
+
make_dash_tile(tile_buf); load_tiles(DASH_VRAM, tile_buf, 16);
|
|
312
|
+
make_solid_tile(tile_buf, 3); load_tiles(EDGE_VRAM, tile_buf, 16);
|
|
313
|
+
make_solid_tile(tile_buf, 2); load_tiles(BAND_VRAM, tile_buf, 16);
|
|
314
|
+
make_sprite16(PLAYER_VRAM, car_mask);
|
|
315
|
+
make_sprite16(ENEMY_VRAM, car_mask);
|
|
316
|
+
upload_sprite_digits();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/* ── GAME LOGIC (clay) — BAT text helpers ────────────────────────────────── */
|
|
320
|
+
static void put_glyph(u8 col, u8 row, u8 glyph) {
|
|
321
|
+
u16 e = BAT_ENTRY(1, (u16)(FONT_VRAM + glyph * 16)); /* pal 1 = white */
|
|
157
322
|
vram_set_write_addr((u16)(BAT_VRAM + row * 32 + col));
|
|
158
323
|
VDC_DATA_LO = (u8)(e & 0xFF);
|
|
159
324
|
VDC_DATA_HI = (u8)(e >> 8);
|
|
160
325
|
}
|
|
161
326
|
|
|
162
|
-
static void
|
|
163
|
-
u16
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
d2 = (u8)(v % 10); v /= 10;
|
|
167
|
-
d1 = (u8)(v % 10); v /= 10;
|
|
168
|
-
d0 = (u8)(v % 10);
|
|
169
|
-
put_glyph(1, 1, d0);
|
|
170
|
-
put_glyph(2, 1, d1);
|
|
171
|
-
put_glyph(3, 1, d2);
|
|
172
|
-
put_glyph(4, 1, d3);
|
|
327
|
+
static void put_tile(u8 col, u8 row, u16 e) {
|
|
328
|
+
vram_set_write_addr((u16)(BAT_VRAM + row * 32 + col));
|
|
329
|
+
VDC_DATA_LO = (u8)(e & 0xFF);
|
|
330
|
+
VDC_DATA_HI = (u8)(e >> 8);
|
|
173
331
|
}
|
|
174
332
|
|
|
175
|
-
static
|
|
176
|
-
|
|
177
|
-
|
|
333
|
+
static void draw_text(u8 col, u8 row, const char *s) {
|
|
334
|
+
u8 c;
|
|
335
|
+
while ((c = (u8)*s++) != 0) {
|
|
336
|
+
u8 g = G_BLANK;
|
|
337
|
+
if (c >= '0' && c <= '9') g = (u8)(G_DIGIT + c - '0');
|
|
338
|
+
else if (c >= 'A' && c <= 'Z') g = (u8)(G_ALPHA + c - 'A');
|
|
339
|
+
else if (c == '-') g = G_DASH;
|
|
340
|
+
put_glyph(col++, row, g);
|
|
341
|
+
}
|
|
178
342
|
}
|
|
179
343
|
|
|
180
|
-
static u16
|
|
181
|
-
|
|
182
|
-
|
|
344
|
+
static void draw_num5(u8 col, u8 row, u16 v) {
|
|
345
|
+
u8 i, d[5];
|
|
346
|
+
for (i = 0; i < 5; ++i) { d[i] = (u8)(v % 10); v /= 10; }
|
|
347
|
+
for (i = 0; i < 5; ++i) put_glyph((u8)(col + i), row, (u8)(G_DIGIT + d[4 - i]));
|
|
183
348
|
}
|
|
184
349
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
350
|
+
/* ── GAME LOGIC (clay) — xorshift16 PRNG ───────────────────────────────────── */
|
|
351
|
+
static u16 rng = 0xBEEF;
|
|
352
|
+
static u8 random8(void) {
|
|
353
|
+
u16 r = rng;
|
|
354
|
+
r ^= r << 7;
|
|
355
|
+
r ^= r >> 9;
|
|
356
|
+
r ^= r << 8;
|
|
357
|
+
rng = r;
|
|
358
|
+
return (u8)r;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
362
|
+
* HARDWARE BG Y-SCROLL via BYR + STREAMED ROWS — the PCE's road. The BAT is a
|
|
363
|
+
* 32x32 (256px-tall) virtual map and the VDC's R8 (BYR) shifts the whole
|
|
364
|
+
* background vertically with ZERO CPU per pixel. Screen line y shows plane line
|
|
365
|
+
* (BYR + y) & 255, so DECREMENTING road_scroll slides the road DOWN — the
|
|
366
|
+
* driving-up illusion — for one register write per frame. The BAT is 256px tall
|
|
367
|
+
* and the VDC masks BYR to it IN HARDWARE, so a plain u8 wraps at 256 seamlessly
|
|
368
|
+
* forever — the NES racing template (examples/nes/templates/racing.c) needs a
|
|
369
|
+
* 240-wrap helper here (a nametable is 240px tall; scroll_y 240-255 fetches
|
|
370
|
+
* attribute bytes as garbage tiles), and the SMS a 224-wrap. On the PCE there
|
|
371
|
+
* is no wrap math at all.
|
|
372
|
+
*
|
|
373
|
+
* The 32 BAT rows recycle as road_scroll moves: the row crossing into the top
|
|
374
|
+
* of the screen is BAT row (road_scroll >> 3) & 31. The moment it changes we
|
|
375
|
+
* restamp that ONE row with fresh random roadside, so the 256-px loop never
|
|
376
|
+
* shows the same scenery twice. Two rules:
|
|
377
|
+
* 1. Restamp with the address latch armed by vram_set_write_addr() — a row
|
|
378
|
+
* is 32 contiguous BAT words, so one latch + 32 word writes does it.
|
|
379
|
+
* 2. Road speed stays under 8 px/frame (MAX_SPEED) so a frame never skips a
|
|
380
|
+
* whole row crossing (the streamer restamps one row per crossing).
|
|
381
|
+
* The HUD is SPRITES (see upload_sprite_digits), so unlike the NES (overscan
|
|
382
|
+
* band) or Genesis (window plane) there's no BG "curtain" to hide a restamp —
|
|
383
|
+
* the restamp lands at the very top edge and the dashes/edges are identical
|
|
384
|
+
* tiles row to row, so the only thing that changes is the random grass speckle,
|
|
385
|
+
* which reads as roadside texture, not a pop.
|
|
386
|
+
*
|
|
387
|
+
* requires: BYR written every frame (we do, in the loop); each BAT row painted
|
|
388
|
+
* when it enters; the BAT 32x32 (vdc_init's MWR). */
|
|
389
|
+
static u16 road_cell(u8 c) {
|
|
390
|
+
if (c == COL_EDGE_L || c == COL_EDGE_R || c == COL_DIVIDER)
|
|
391
|
+
return BAT_ENTRY(0, EDGE_VRAM); /* shoulders + divider */
|
|
392
|
+
if (c == COL_DASH_1 || c == COL_DASH_2)
|
|
393
|
+
return BAT_ENTRY(0, DASH_VRAM); /* dashed lane lines */
|
|
394
|
+
if (c > COL_EDGE_L && c < COL_EDGE_R)
|
|
395
|
+
return BAT_ENTRY(0, ROAD_VRAM); /* asphalt */
|
|
396
|
+
return BAT_ENTRY(0, GRASS_VRAM); /* roadside grass */
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/* Restamp one BAT row with fresh roadside (the dashes/edges are fixed; only the
|
|
400
|
+
* grass speckle phase changes per row via the road_cell tiles themselves). */
|
|
401
|
+
static void paint_road_row(u8 row) {
|
|
402
|
+
u8 c;
|
|
403
|
+
vram_set_write_addr((u16)(BAT_VRAM + row * 32));
|
|
404
|
+
for (c = 0; c < 32; ++c) {
|
|
405
|
+
u16 e = road_cell(c);
|
|
406
|
+
VDC_DATA_LO = (u8)(e & 0xFF);
|
|
407
|
+
VDC_DATA_HI = (u8)(e >> 8);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/* Initial full road paint (all 32 rows) — used on (re)entering the race. */
|
|
412
|
+
static void paint_road(void) {
|
|
413
|
+
u8 r;
|
|
414
|
+
for (r = 0; r < 32; ++r) paint_road_row(r);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/* Advance the road by `px` pixels: one BYR write + at most one row restamp.
|
|
418
|
+
* DECREMENT so the road slides DOWN (driving up); the u8 wraps at 256 — idiom. */
|
|
419
|
+
static void advance_road(u8 px) {
|
|
420
|
+
u8 top_row;
|
|
421
|
+
road_scroll = (u8)(road_scroll - px); /* hardware wraps at 256 — idiom */
|
|
422
|
+
vdc_set_reg(VDC_BYR, (u16)road_scroll);
|
|
423
|
+
top_row = (u8)((road_scroll >> 3) & 31);
|
|
424
|
+
if (top_row != prev_top_row) {
|
|
425
|
+
prev_top_row = top_row;
|
|
426
|
+
paint_road_row(top_row);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/* ── HARDWARE TRUTH: a bare HuCard CANNOT save the best distance (in-session) ──
|
|
431
|
+
* This was researched and corrected: earlier versions wrote the best distance
|
|
432
|
+
* to BRAM ("backup RAM", bank $F7) and claimed it persisted across power
|
|
433
|
+
* cycles. That is NOT honest for a HuCard game. On REAL hardware a plain HuCard
|
|
434
|
+
* plugged into a base PC Engine / TurboGrafx-16 has NO backup RAM at all — BRAM
|
|
435
|
+
* exists ONLY when a peripheral is attached: the CD-ROM² System (2KB kept by a
|
|
436
|
+
* supercapacitor), the Tennokoe Bank HuCard, or the Memory Base 128. No
|
|
437
|
+
* commercial HuCard self-saved; they used PASSWORDS. (The often-cited Populous
|
|
438
|
+
* "ROMRAM" SRAM was the game's own working RAM, not a battery save.) An
|
|
439
|
+
* emulator like geargrafx exposes BRAM unconditionally, so the old code
|
|
440
|
+
* "worked" in emulation in a way the real machine never would.
|
|
441
|
+
*
|
|
442
|
+
* So this game keeps an IN-SESSION best only (like the honest 2600/Lynx
|
|
443
|
+
* examples) — it survives across runs within a power-on, resets to 0 on a cold
|
|
444
|
+
* boot. To ACTUALLY persist on real hardware you would target a peripheral
|
|
445
|
+
* (BRAM behind a detect, or a CD-ROM² build) — a real-hardware feature, not a
|
|
446
|
+
* property of the cartridge. */
|
|
447
|
+
static u16 best_load(void) {
|
|
448
|
+
return 0; /* cold boot: no persistence on a bare HuCard */
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
static void best_save(u16 v) {
|
|
452
|
+
(void)v; /* in-session only — nowhere to persist on real HW */
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
static void best_init(void) {
|
|
456
|
+
best = best_load(); /* always 0 — in-session best starts fresh each boot */
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/* ── GAME LOGIC (clay) — music: a 2-channel tune ticked once per frame ──────
|
|
460
|
+
* PSG channel plan: 5 = melody, 4 = bass, 0-3 = SFX (tones cut by sfx_timer).
|
|
461
|
+
* PCE frequency regs are DIVIDERS: pitch ≈ 3.58MHz / (32 × value), so a
|
|
462
|
+
* BIGGER number is a LOWER note. Note indices into NOTE_DIV below. */
|
|
463
|
+
enum { R = 0, A2N, C3, F3, G3, A3, B3, C4, D4, E4, F4, G4, A4, B4, C5, D5, E5 };
|
|
464
|
+
static const u16 NOTE_DIV[17] = {
|
|
465
|
+
0, 1017, 854, 641, 571, 508, 453, 427, 381, 339, 320, 285, 254, 226, 214, 190, 170
|
|
466
|
+
};
|
|
467
|
+
/* 16 melody steps + 8 bass steps (one bass note per 2 melody steps) */
|
|
468
|
+
static const u8 MEL_TITLE[16] = { C4,E4,G4,C5, B4,G4,E4,G4, A4,C5,E5,C5, D5,B4,G4,E4 };
|
|
469
|
+
static const u8 BAS_TITLE[8] = { C3,C3, G3,G3, A2N,A2N, G3,G3 };
|
|
470
|
+
static const u8 MEL_PLAY[16] = { E4,G4,A4,G4, E4,D4,E4,G4, C5,B4,A4,G4, A4,G4,E4,R };
|
|
471
|
+
static const u8 BAS_PLAY[8] = { A2N,A2N, C3,C3, G3,G3, F3,F3 };
|
|
472
|
+
static const u8 MEL_OVER[16] = { C5,R,G4,R, E4,R,D4,R, C4,R,A2N,R, A2N,R,R,R };
|
|
473
|
+
|
|
474
|
+
static u8 music_song; /* reuses the ST_* ids */
|
|
475
|
+
static u8 music_step, music_timer, music_done;
|
|
476
|
+
|
|
477
|
+
static void music_set(u8 song) {
|
|
478
|
+
music_song = song;
|
|
479
|
+
music_step = 0;
|
|
480
|
+
music_timer = 0;
|
|
481
|
+
music_done = 0;
|
|
482
|
+
psg_off(4);
|
|
483
|
+
psg_off(5);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
static void music_tick(void) {
|
|
487
|
+
const u8 *mel;
|
|
488
|
+
u8 n;
|
|
489
|
+
if (music_done) return;
|
|
490
|
+
if (music_timer == 0) {
|
|
491
|
+
mel = (music_song == ST_PLAY) ? MEL_PLAY
|
|
492
|
+
: (music_song == ST_OVER) ? MEL_OVER : MEL_TITLE;
|
|
493
|
+
n = mel[music_step & 15];
|
|
494
|
+
if (n != R) psg_tone(5, NOTE_DIV[n], 26);
|
|
495
|
+
else psg_off(5);
|
|
496
|
+
if (music_song != ST_OVER) { /* the wreck jingle has no bass */
|
|
497
|
+
n = ((music_step & 1) == 0)
|
|
498
|
+
? ((music_song == ST_PLAY) ? BAS_PLAY[(music_step >> 1) & 7]
|
|
499
|
+
: BAS_TITLE[(music_step >> 1) & 7])
|
|
500
|
+
: R;
|
|
501
|
+
if (n != R) psg_tone(4, NOTE_DIV[n], 20);
|
|
502
|
+
}
|
|
503
|
+
++music_step;
|
|
504
|
+
if (music_song == ST_OVER && music_step >= 16) { /* play once, stop */
|
|
505
|
+
music_done = 1;
|
|
506
|
+
psg_off(4);
|
|
507
|
+
psg_off(5);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
++music_timer;
|
|
511
|
+
if (music_timer >= 9) music_timer = 0;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/* short SFX on channels 0-3, auto-cut by sfx_timer */
|
|
515
|
+
static void sfx(u8 chan, u16 freq, u8 frames) {
|
|
516
|
+
psg_tone(chan, freq, 31);
|
|
517
|
+
if (frames > sfx_timer) sfx_timer = frames;
|
|
195
518
|
}
|
|
196
519
|
|
|
197
|
-
|
|
520
|
+
/* ── GAME LOGIC (clay) — AABB, both boxes 14x14 (16px cars, slight slack). ── */
|
|
521
|
+
static u8 hits(u16 ax, u16 ay, u16 bx, u16 by) {
|
|
522
|
+
return (u8)(ax < bx + 14 && ax + 14 > bx && ay < by + 14 && ay + 14 > by);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/* ── GAME LOGIC (clay) — traffic pool (fixed slots, no allocation) ── */
|
|
526
|
+
static void spawn_traffic(void) {
|
|
198
527
|
u8 i;
|
|
199
|
-
for (i = 0; i <
|
|
200
|
-
if (!
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
528
|
+
for (i = 0; i < MAX_TRAFFIC; ++i) {
|
|
529
|
+
if (!traffic[i].alive) {
|
|
530
|
+
traffic[i].x = lane_x[random8() & 3];
|
|
531
|
+
traffic[i].y = SPAWN_Y;
|
|
532
|
+
traffic[i].alive = 1;
|
|
204
533
|
return;
|
|
205
534
|
}
|
|
206
535
|
}
|
|
207
536
|
}
|
|
208
537
|
|
|
209
|
-
|
|
538
|
+
/* ── GAME LOGIC (clay) — stage the SPRITE HUD digits at HUD_Y ────────────────
|
|
539
|
+
* 1P: crashes-left digit (left) + 5-digit distance (right) = 6 sprites on the
|
|
540
|
+
* HUD scanline. 2P: one crashes-left digit per player = 2 sprites. Unused HUD
|
|
541
|
+
* slots park off-screen. Sprites are SCREEN-space, so the HUD holds steady over
|
|
542
|
+
* the scrolling road (see the SPRITE-HUD idiom on upload_sprite_digits). */
|
|
543
|
+
static void put_digit(u8 slot, u16 x, u8 d) {
|
|
544
|
+
set_sprite(slot, x, (u16)HUD_Y, (u16)(SDIGIT_PAT + d), PAL_HUD);
|
|
545
|
+
}
|
|
546
|
+
static void hide_hud_slot(u8 slot) {
|
|
547
|
+
set_sprite(slot, 0, OFFSCREEN_Y, SDIGIT_PAT, PAL_HUD);
|
|
548
|
+
}
|
|
549
|
+
static void stage_hud(void) {
|
|
210
550
|
u8 i;
|
|
551
|
+
if (state != ST_PLAY) { for (i = 0; i < 6; ++i) hide_hud_slot((u8)(SLOT_HUD + i)); return; }
|
|
552
|
+
if (two_player) {
|
|
553
|
+
put_digit((u8)(SLOT_HUD + 0), 24, crashes_left[0]); /* P1 left */
|
|
554
|
+
put_digit((u8)(SLOT_HUD + 1), 216, crashes_left[1]); /* P2 right */
|
|
555
|
+
for (i = 2; i < 6; ++i) hide_hud_slot((u8)(SLOT_HUD + i));
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
put_digit((u8)(SLOT_HUD + 0), 16, crashes_left[0]); /* crashes */
|
|
559
|
+
{ /* distance */
|
|
560
|
+
u16 v = dist;
|
|
561
|
+
u8 d[5];
|
|
562
|
+
for (i = 0; i < 5; ++i) { d[i] = (u8)(v % 10); v /= 10; }
|
|
563
|
+
for (i = 0; i < 5; ++i) put_digit((u8)(SLOT_HUD + 1 + i), (u16)(176 + i * 14), d[4 - i]);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
211
566
|
|
|
212
|
-
|
|
567
|
+
/* ── GAME LOGIC (clay) — flat band behind title/result text (BAT tiles) ──────
|
|
568
|
+
* The title and result screens DON'T scroll (BYR held at 0, no streaming), so
|
|
569
|
+
* their text safely lives in the BAT. A dark band sits behind the text rows. */
|
|
570
|
+
static void paint_band_rows(u8 r0, u8 r1) {
|
|
571
|
+
u8 c, r;
|
|
572
|
+
for (r = r0; r <= r1; ++r)
|
|
573
|
+
for (c = 0; c < 32; ++c) put_tile(c, r, BAT_ENTRY(0, BAND_VRAM));
|
|
574
|
+
}
|
|
213
575
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
576
|
+
/* ── GAME LOGIC (clay) — screen painters (full BAT repaint per state change) ──
|
|
577
|
+
* Title/result paint the road as a STATIC backdrop (so the scene reads as a
|
|
578
|
+
* road, not a blank card) then lay text over a dark band. Only ST_PLAY scrolls. */
|
|
579
|
+
static void paint_title(void) {
|
|
580
|
+
paint_road();
|
|
581
|
+
paint_band_rows(6, 23);
|
|
582
|
+
draw_text((u8)((32 - (sizeof(GAME_TITLE) - 1)) / 2), 8, GAME_TITLE);
|
|
583
|
+
draw_text(10, 13, "1P RACE - I");
|
|
584
|
+
draw_text(10, 15, "2P VERSUS - II");
|
|
585
|
+
draw_text(11, 18, "BEST");
|
|
586
|
+
draw_num5(16, 18, best);
|
|
587
|
+
draw_text(4, 22, "STEER L R - GAS I - BRAKE II");
|
|
588
|
+
}
|
|
223
589
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
590
|
+
static void paint_play(void) {
|
|
591
|
+
paint_road(); /* fresh 32-row road; the sprite HUD floats */
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
static void paint_over(void) {
|
|
595
|
+
paint_road();
|
|
596
|
+
paint_band_rows(7, 22);
|
|
597
|
+
if (two_player) {
|
|
598
|
+
draw_text(13, 8, winner ? "P2 WINS" : "P1 WINS");
|
|
599
|
+
draw_text(10, 11, "RIVAL WRECKED");
|
|
600
|
+
} else {
|
|
601
|
+
draw_text(13, 8, "WRECKED");
|
|
602
|
+
draw_text(11, 11, "DIST");
|
|
603
|
+
draw_num5(16, 11, dist);
|
|
604
|
+
draw_text(11, 13, "BEST");
|
|
605
|
+
draw_num5(16, 13, best);
|
|
606
|
+
}
|
|
607
|
+
draw_text(9, 21, "RUN - TITLE");
|
|
608
|
+
}
|
|
230
609
|
|
|
231
|
-
|
|
610
|
+
/* ── GAME LOGIC (clay) — start a run ── */
|
|
611
|
+
static void start_game(u8 versus) {
|
|
612
|
+
u8 i;
|
|
613
|
+
two_player = versus;
|
|
614
|
+
for (i = 0; i < MAX_TRAFFIC; ++i) traffic[i].alive = 0;
|
|
615
|
+
for (i = 0; i < 2; ++i) {
|
|
616
|
+
crashes_left[i] = START_LIVES;
|
|
617
|
+
invuln[i] = 0;
|
|
618
|
+
lane_cd[i] = 0;
|
|
619
|
+
/* prev_pads = the CURRENTLY-held pad so a button held across the
|
|
620
|
+
* title→play transition (the start press) isn't read as a fresh edge,
|
|
621
|
+
* WITHOUT swallowing the player's first deliberate steer. (0xFF here
|
|
622
|
+
* would mask the first press of every direction until released once.) */
|
|
623
|
+
prev_pads[i] = 0;
|
|
624
|
+
}
|
|
625
|
+
prev_pads[0] = pce_joy_read(); /* swallow the held start button (pad 1) */
|
|
626
|
+
if (versus) {
|
|
627
|
+
car_active[0] = 1; car_active[1] = 1;
|
|
628
|
+
lane_min[0] = 0; lane_max[0] = 1; car_lane[0] = 0; /* P1: left half */
|
|
629
|
+
lane_min[1] = 2; lane_max[1] = 3; car_lane[1] = 3; /* P2: right half */
|
|
630
|
+
speed = SPEED_2P; /* shared road, fixed speed (see design) */
|
|
631
|
+
} else {
|
|
632
|
+
car_active[0] = 1; car_active[1] = 0;
|
|
633
|
+
lane_min[0] = 0; lane_max[0] = 3; car_lane[0] = 1; /* whole road */
|
|
634
|
+
speed = 1;
|
|
635
|
+
}
|
|
636
|
+
dist = 0; dist_frac = 0;
|
|
637
|
+
spawn_timer = 0;
|
|
638
|
+
start_pause = 30; /* green-light breather */
|
|
639
|
+
road_scroll = 0;
|
|
640
|
+
prev_top_row = 0;
|
|
641
|
+
state = ST_PLAY;
|
|
642
|
+
paint_play();
|
|
643
|
+
vdc_set_reg(VDC_BYR, 0);
|
|
644
|
+
music_set(ST_PLAY);
|
|
645
|
+
sfx(2, 0x180, 6); /* start blip */
|
|
646
|
+
}
|
|
232
647
|
|
|
233
|
-
|
|
648
|
+
static void game_over(void) {
|
|
649
|
+
if (!two_player && dist > best) {
|
|
650
|
+
best = dist;
|
|
651
|
+
best_save(best); /* in-session only (no save on a bare HuCard) */
|
|
652
|
+
}
|
|
653
|
+
state = ST_OVER;
|
|
654
|
+
prev_pads[0] = pce_joy_read(); /* swallow only the held button, not the
|
|
655
|
+
* player's next deliberate press */
|
|
234
656
|
road_scroll = 0;
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
657
|
+
vdc_set_reg(VDC_BYR, 0);
|
|
658
|
+
paint_over();
|
|
659
|
+
music_set(ST_OVER);
|
|
660
|
+
sfx(3, 0x500, 16); /* wreck rumble */
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/* ── GAME LOGIC (clay) — crash rules ── */
|
|
664
|
+
static void crash(u8 p) {
|
|
665
|
+
sfx(3, 0x080, 16); /* crash buzz */
|
|
666
|
+
invuln[p] = 60; /* blink + no-collide grace */
|
|
667
|
+
if (!two_player) speed = 1; /* a wreck kills your momentum */
|
|
668
|
+
if (crashes_left[p] > 0) --crashes_left[p];
|
|
669
|
+
if (crashes_left[p] == 0) {
|
|
670
|
+
winner = (u8)(1 - p); /* versus: the OTHER player wins */
|
|
671
|
+
game_over();
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
676
|
+
* 2P INPUT via the TurboTap. pce_joy_read() reads pad 1 (slot 0). For pad 2 we
|
|
677
|
+
* read cc65's JOY_2 directly and translate it to the same clean PCE bitmask
|
|
678
|
+
* pce_input.c builds for pad 1. The host force-enables the TurboTap core
|
|
679
|
+
* option, so JOY_2 carries real port-1 input; without that override port 1 is
|
|
680
|
+
* dead and this would silently fall back to 1P. ── */
|
|
681
|
+
static u8 read_pad2(void) {
|
|
682
|
+
u8 raw = joy_read(JOY_2);
|
|
683
|
+
u8 m = 0;
|
|
684
|
+
if (JOY_UP(raw)) m |= PCE_JOY_UP;
|
|
685
|
+
if (JOY_DOWN(raw)) m |= PCE_JOY_DOWN;
|
|
686
|
+
if (JOY_LEFT(raw)) m |= PCE_JOY_LEFT;
|
|
687
|
+
if (JOY_RIGHT(raw)) m |= PCE_JOY_RIGHT;
|
|
688
|
+
if (JOY_BTN_1(raw)) m |= PCE_JOY_I;
|
|
689
|
+
if (JOY_BTN_2(raw)) m |= PCE_JOY_II;
|
|
690
|
+
if (JOY_BTN_3(raw)) m |= PCE_JOY_SELECT;
|
|
691
|
+
if (JOY_BTN_4(raw)) m |= PCE_JOY_RUN;
|
|
692
|
+
return m;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/* ── GAME LOGIC (clay) — per-player input ───────────────────────────────────
|
|
696
|
+
* LEFT/RIGHT steer between lanes; UP/I accelerate, DOWN/II brake (1P only).
|
|
697
|
+
*
|
|
698
|
+
* Steering uses a short COOLDOWN (lane_cd) rather than pure rising-edge: a held
|
|
699
|
+
* direction steps one lane, then can't step again until lane_cd reaches 0
|
|
700
|
+
* (~9 frames). This still prevents machine-gun lane spam from a held d-pad, but
|
|
701
|
+
* unlike strict `pad & ~prev` edge detection it does NOT depend on catching the
|
|
702
|
+
* exact frame the button transitions — robust against input sampling latency
|
|
703
|
+
* (a tap that spans only a couple of frames still lands). Speed changes stay
|
|
704
|
+
* rising-edge (a held gas shouldn't ramp to max in 4 frames). */
|
|
705
|
+
static void update_player(u8 p, u8 pad) {
|
|
706
|
+
u8 pressed = (u8)(pad & ~prev_pads[p]);
|
|
707
|
+
prev_pads[p] = pad;
|
|
708
|
+
if (!car_active[p]) return;
|
|
709
|
+
if (lane_cd[p]) --lane_cd[p];
|
|
710
|
+
if (!lane_cd[p]) {
|
|
711
|
+
if ((pad & PCE_JOY_LEFT) && car_lane[p] > lane_min[p]) {
|
|
712
|
+
--car_lane[p]; lane_cd[p] = 9; sfx(2, 0x2C0, 4); /* lane tick */
|
|
713
|
+
} else if ((pad & PCE_JOY_RIGHT) && car_lane[p] < lane_max[p]) {
|
|
714
|
+
++car_lane[p]; lane_cd[p] = 9; sfx(2, 0x2C0, 4);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
if (!two_player) {
|
|
718
|
+
if ((pressed & (PCE_JOY_UP | PCE_JOY_I)) && speed < MAX_SPEED) {
|
|
719
|
+
++speed;
|
|
720
|
+
sfx(1, (u16)(0x300 - speed * 0x60), 6); /* engine rev */
|
|
721
|
+
}
|
|
722
|
+
if ((pressed & (PCE_JOY_DOWN | PCE_JOY_II)) && speed > 1) {
|
|
723
|
+
--speed;
|
|
724
|
+
sfx(1, 0x3C0, 5); /* brake blip */
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
if (invuln[p] > 0) --invuln[p];
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
731
|
+
* SPRITE STAGING + THE SATB DMA. The VDC never reads your RAM: sprites live in
|
|
732
|
+
* its INTERNAL sprite attribute table, refreshed by a DMA you schedule by
|
|
733
|
+
* writing R19 (satb_dma() does the copy + the R19 write; the transfer happens
|
|
734
|
+
* at the next vblank). So the per-frame contract is:
|
|
735
|
+
* waitvsync() → restage EVERY slot → satb_dma()
|
|
736
|
+
* Stage during vblank — satb_dma() also streams words through the VWR port, and
|
|
737
|
+
* doing that mid-display tears sprite pattern fetches. Hidden slots park below
|
|
738
|
+
* the display at OFFSCREEN_Y. ── */
|
|
739
|
+
static void stage_sprites(void) {
|
|
740
|
+
u8 i, p;
|
|
741
|
+
for (p = 0; p < 2; ++p) {
|
|
742
|
+
u8 vis = (state == ST_PLAY) && car_active[p] && !(invuln[p] & 2);
|
|
743
|
+
set_sprite((u8)(SLOT_P1 + p), lane_x[car_lane[p]],
|
|
744
|
+
vis ? (u16)CAR_Y : OFFSCREEN_Y, PLAYER_PAT, p ? PAL_P2 : PAL_P1);
|
|
745
|
+
}
|
|
746
|
+
for (i = 0; i < MAX_TRAFFIC; ++i) {
|
|
747
|
+
u8 vis = (state == ST_PLAY) && traffic[i].alive;
|
|
748
|
+
set_sprite((u8)(SLOT_TRAFFIC + i), traffic[i].x,
|
|
749
|
+
vis ? traffic[i].y : OFFSCREEN_Y, ENEMY_PAT, PAL_TRAFFIC);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
void main(void) {
|
|
754
|
+
u8 pad1, pad2, newpad;
|
|
755
|
+
|
|
756
|
+
_pce_keep[0] = 0; /* see the EMPTY-BSS TRAP note in pce_hw.h */
|
|
757
|
+
|
|
758
|
+
/* BRAM first — before any VDC work, so the save file exists within the
|
|
759
|
+
* game's first frames (a headless host sees a non-empty save_ram region
|
|
760
|
+
* as early as possible; see the BRAM idiom). */
|
|
761
|
+
best_init();
|
|
762
|
+
|
|
763
|
+
/* ── HARDWARE IDIOM (load-bearing — see TROUBLESHOOTING) ──
|
|
764
|
+
* Init order: palette → VRAM uploads → BAT paint → joypad → display ON.
|
|
765
|
+
* disp_enable() also sets the VBlank IRQ bit — without it waitvsync()
|
|
766
|
+
* never returns and the game freezes on its first frame. */
|
|
767
|
+
/* BG sub-pal 0: road scene. BG sub-pal 1: HUD/text white. */
|
|
768
|
+
vce_set_color(0, PCE_RGB(0, 1, 0)); /* backdrop: dark green */
|
|
769
|
+
vce_set_color(1, PCE_RGB(1, 5, 1)); /* BG c1: roadside grass green */
|
|
770
|
+
vce_set_color(2, PCE_RGB(2, 2, 2)); /* BG c2: asphalt grey */
|
|
771
|
+
vce_set_color(3, PCE_RGB(7, 7, 1)); /* BG c3: yellow markings/specks */
|
|
772
|
+
vce_set_color(17, PCE_RGB(7, 7, 7)); /* pal1 text: white */
|
|
773
|
+
/* sprite sub-palettes (256 + pal*16 + index) — P1 cyan, P2 amber, traffic
|
|
774
|
+
* red, each on its own sub-palette so the cars read as three liveries. */
|
|
775
|
+
vce_set_color(256 + 0 * 16 + 1, PCE_RGB(2, 6, 7)); /* spr pal0 c1: P1 cyan */
|
|
776
|
+
vce_set_color(256 + 1 * 16 + 1, PCE_RGB(7, 5, 0)); /* spr pal1 c1: P2 amber */
|
|
777
|
+
vce_set_color(256 + 2 * 16 + 1, PCE_RGB(7, 1, 1)); /* spr pal2 c1: traffic red*/
|
|
778
|
+
|
|
779
|
+
upload_art();
|
|
780
|
+
|
|
781
|
+
state = ST_TITLE;
|
|
782
|
+
paint_title();
|
|
783
|
+
music_set(ST_TITLE);
|
|
239
784
|
|
|
240
785
|
pce_joy_init();
|
|
241
786
|
disp_enable();
|
|
242
787
|
|
|
243
788
|
for (;;) {
|
|
244
|
-
u8 slot;
|
|
245
|
-
int16_t step;
|
|
246
789
|
waitvsync();
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
set_sprite(slot++, player.x, player.y, PLAYER_VRAM >> 6, 0);
|
|
252
|
-
for (i = 0; i < MAX_OBST; ++i) {
|
|
253
|
-
u16 ey = obst[i].alive ? obst[i].y : 0x1F0;
|
|
254
|
-
set_sprite(slot++, obst[i].x, ey, ENEMY_VRAM >> 6, 1);
|
|
255
|
-
}
|
|
790
|
+
|
|
791
|
+
/* ── vblank work first: cars + sprite HUD + SATB DMA ── */
|
|
792
|
+
stage_sprites();
|
|
793
|
+
stage_hud();
|
|
256
794
|
satb_dma();
|
|
257
795
|
|
|
258
|
-
|
|
796
|
+
music_tick();
|
|
797
|
+
if (sfx_timer) {
|
|
798
|
+
--sfx_timer;
|
|
799
|
+
if (sfx_timer == 0) { psg_off(0); psg_off(1); psg_off(2); psg_off(3); }
|
|
800
|
+
}
|
|
259
801
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
802
|
+
/* ── input: pad 1 always; pad 2 only in 2P play (TurboTap port 1). ── */
|
|
803
|
+
pad1 = pce_joy_read();
|
|
804
|
+
pad2 = (state == ST_PLAY && two_player) ? read_pad2() : 0;
|
|
805
|
+
|
|
806
|
+
if (state == ST_TITLE) {
|
|
807
|
+
/* The title road is a STATIC backdrop: with no hardware window and
|
|
808
|
+
* no raster split, BYR would scroll the BG title text off-screen
|
|
809
|
+
* (and the row-streamer would wipe it). So the title doesn't scroll
|
|
810
|
+
* — the play state is where the road comes alive. */
|
|
811
|
+
newpad = (u8)(pad1 & ~prev_pads[0]);
|
|
812
|
+
prev_pads[0] = pad1;
|
|
813
|
+
if (newpad & (PCE_JOY_RUN | PCE_JOY_I)) start_game(0);
|
|
814
|
+
else if (newpad & PCE_JOY_II) start_game(1);
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
if (state == ST_OVER) {
|
|
818
|
+
newpad = (u8)(pad1 & ~prev_pads[0]);
|
|
819
|
+
prev_pads[0] = pad1;
|
|
820
|
+
if (newpad & (PCE_JOY_RUN | PCE_JOY_I)) {
|
|
821
|
+
state = ST_TITLE;
|
|
822
|
+
road_scroll = 0;
|
|
823
|
+
vdc_set_reg(VDC_BYR, 0);
|
|
824
|
+
paint_title();
|
|
825
|
+
music_set(ST_TITLE);
|
|
826
|
+
}
|
|
265
827
|
continue;
|
|
266
828
|
}
|
|
267
829
|
|
|
268
|
-
/*
|
|
269
|
-
if (
|
|
270
|
-
if ((pad & PCE_JOY_RIGHT) && !(prev_pad & PCE_JOY_RIGHT) && player_lane < 2) { player_lane++; psg_tone(1, 0x2C0, 16); sfx_timer = 3; }
|
|
271
|
-
player.x = lane_x[player_lane];
|
|
272
|
-
prev_pad = pad;
|
|
830
|
+
/* ── ST_PLAY ──────────────────────────────────────────────────────── */
|
|
831
|
+
if (start_pause) { --start_pause; continue; } /* green-light freeze */
|
|
273
832
|
|
|
274
|
-
|
|
275
|
-
step = (int16_t)(2 + (score / 400));
|
|
276
|
-
if (step > 5) step = 5;
|
|
833
|
+
advance_road(speed);
|
|
277
834
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
835
|
+
update_player(0, pad1);
|
|
836
|
+
if (two_player) update_player(1, pad2);
|
|
837
|
+
if (state != ST_PLAY) continue; /* a crash may have ended the game */
|
|
281
838
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
839
|
+
/* Distance (1P stat): 1 unit per 16 scrolled pixels. A chime every 256
|
|
840
|
+
* units marks a checkpoint. */
|
|
841
|
+
if (!two_player) {
|
|
842
|
+
dist_frac = (u8)(dist_frac + speed);
|
|
843
|
+
if (dist_frac >= 16) {
|
|
844
|
+
dist_frac = (u8)(dist_frac - 16);
|
|
845
|
+
if (dist < 65535u) ++dist;
|
|
846
|
+
if (dist != 0 && (dist & 0xFF) == 0)
|
|
847
|
+
sfx(0, 0x0D6, 8); /* checkpoint chime (C6) */
|
|
848
|
+
}
|
|
286
849
|
}
|
|
287
850
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
851
|
+
/* Traffic flows down at road speed (it reads as slower cars you're
|
|
852
|
+
* overtaking); despawn past the player with a little pass tick. */
|
|
853
|
+
{
|
|
854
|
+
u8 i, p;
|
|
855
|
+
for (i = 0; i < MAX_TRAFFIC; ++i) {
|
|
856
|
+
if (!traffic[i].alive) continue;
|
|
857
|
+
traffic[i].y = (u16)(traffic[i].y + speed);
|
|
858
|
+
if (traffic[i].y > DESPAWN_Y) {
|
|
859
|
+
traffic[i].alive = 0;
|
|
860
|
+
sfx(1, 0x0C0, 2);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
if (++spawn_timer >= SPAWN_PERIOD) { spawn_timer = 0; spawn_traffic(); }
|
|
864
|
+
|
|
865
|
+
/* Traffic ↔ cars. Crash grace: a just-wrecked car blinks and can't
|
|
866
|
+
* collide for 60 frames. */
|
|
867
|
+
for (i = 0; i < MAX_TRAFFIC && state == ST_PLAY; ++i) {
|
|
868
|
+
if (!traffic[i].alive) continue;
|
|
869
|
+
for (p = 0; p < 2; ++p) {
|
|
870
|
+
if (!car_active[p] || invuln[p]) continue;
|
|
871
|
+
if (hits(traffic[i].x, traffic[i].y, lane_x[car_lane[p]], CAR_Y)) {
|
|
872
|
+
traffic[i].alive = 0;
|
|
873
|
+
crash(p);
|
|
874
|
+
break;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
297
877
|
}
|
|
298
878
|
}
|
|
299
|
-
|
|
300
|
-
if (score < 9999) score++;
|
|
301
|
-
if ((score & 7) == 0) draw_score();
|
|
302
|
-
|
|
303
|
-
if (sfx_timer) { --sfx_timer; if (sfx_timer == 0) { psg_off(0); psg_off(1); } }
|
|
304
879
|
}
|
|
305
880
|
}
|