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,149 +1,925 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/* ── racing.c — C64 top-down vertical road racer (complete example game) ──────
|
|
2
|
+
*
|
|
3
|
+
* VAPOR VECTOR — a COMPLETE, working game: title screen, 1P endless race with
|
|
4
|
+
* speed control, 2P simultaneous SPLIT-LANE VERSUS (both cars on screen at
|
|
5
|
+
* once — player 2 on CONTROL PORT 1), a vertically-scrolling road done the
|
|
6
|
+
* C64 way (VIC-II fine $D011 Y-scroll + a software COARSE row shift), a fixed
|
|
7
|
+
* HUD held over the moving road by the C64's signature raster-IRQ split, best
|
|
8
|
+
* distance in-session behind the gated-persistence seam, 2-voice SID music
|
|
9
|
+
* with the C64's filter sweep + SFX. The player's car is a VIC-II HARDWARE
|
|
10
|
+
* SPRITE; the road, lane lines and scenery are CHARACTERS that scroll.
|
|
11
|
+
*
|
|
12
|
+
* THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
|
|
13
|
+
* very different one. The markers tell you what's what:
|
|
14
|
+
* HARDWARE IDIOM (load-bearing) — dodges a documented C64 footgun; reshape
|
|
15
|
+
* your gameplay around it (see TROUBLESHOOTING before changing).
|
|
16
|
+
* GAME LOGIC (clay) — traffic patterns, speeds, tuning, art: reshape freely.
|
|
17
|
+
*
|
|
18
|
+
* What depends on what:
|
|
19
|
+
* c64_registers.h — VIC-II / SID / CIA symbolic addresses (header only).
|
|
20
|
+
* c64_sfx.{h,c} — one-shot SID sound effects on voice 2.
|
|
21
|
+
* The BASIC stub + crt0 come from cc65's c64 target: the .prg loads at
|
|
22
|
+
* $0801, a tiny BASIC line does SYS into the C runtime, and the KERNAL
|
|
23
|
+
* stays banked in (we lean on that for the IRQ vector — see below).
|
|
24
|
+
*
|
|
25
|
+
* Memory map this file assumes (VIC bank 0 = $0000-$3FFF):
|
|
26
|
+
* $0400 screen RAM (40×25 chars) $D800 color RAM (static texture)
|
|
27
|
+
* $0801 this program (code+data grow up from here)
|
|
28
|
+
* $3F00 sprite images (1 × 64 bytes) — NOT $0800, which collides with
|
|
29
|
+
* the .prg load address, and NOT $1000-$1FFF, where the VIC sees
|
|
30
|
+
* the character ROM instead of RAM (a classic invisible-sprite trap).
|
|
31
|
+
* Keep the program under ~14 KB so it stays below $3F00.
|
|
32
|
+
*
|
|
33
|
+
* THE SCROLL — the platformer template (TALUS TROT) scrolls HORIZONTALLY via
|
|
34
|
+
* $D016 + a column shift; this game scrolls VERTICALLY via $D011's YSCROLL +
|
|
35
|
+
* a ROW shift. Same two-layer plan: the VIC-II fine-scrolls only 0-7 px in
|
|
36
|
+
* hardware (YSCROLL, $D011 low 3 bits); past that you COARSE-scroll in
|
|
37
|
+
* software by shifting the visible char ROWS and stamping one fresh row of
|
|
38
|
+
* road at the top from the world. Both halves run here — see scroll_field and
|
|
39
|
+
* the raster split. (C64 MENTAL_MODEL.md → "Scrolling".)
|
|
40
|
+
*/
|
|
5
41
|
|
|
6
42
|
#include "c64_registers.h"
|
|
7
43
|
#include "c64_sfx.h"
|
|
8
44
|
#include <stdint.h>
|
|
9
45
|
|
|
46
|
+
/* cc65 KERNAL disk-I/O prototypes. We DON'T #include <cbm.h> — it drags in
|
|
47
|
+
* <c64.h>, whose VIC/SID/JOY macros collide with this project's
|
|
48
|
+
* c64_registers.h (cc65 errors "macro redefinition is not identical"). These
|
|
49
|
+
* four are the stable cc65 ABI; declaring them directly avoids the clash. */
|
|
50
|
+
unsigned char __fastcall__ cbm_open(unsigned char lfn, unsigned char device,
|
|
51
|
+
unsigned char sec_addr, const char *name);
|
|
52
|
+
void __fastcall__ cbm_close(unsigned char lfn);
|
|
53
|
+
int __fastcall__ cbm_read(unsigned char lfn, void *buffer, unsigned int size);
|
|
54
|
+
int __fastcall__ cbm_write(unsigned char lfn, const void *buffer, unsigned int size);
|
|
55
|
+
|
|
56
|
+
/* The title screen renders this — examples({op:'fork'}) stamps your game's
|
|
57
|
+
* name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
|
|
58
|
+
#define GAME_TITLE "VAPOR VECTOR"
|
|
59
|
+
|
|
10
60
|
#define POKE(addr, val) (*(volatile uint8_t*)(addr) = (val))
|
|
11
61
|
#define PEEK(addr) (*(volatile uint8_t*)(addr))
|
|
12
62
|
|
|
63
|
+
#define SCREEN ((volatile uint8_t*)0x0400)
|
|
64
|
+
#define COLORS ((volatile uint8_t*)0xD800)
|
|
65
|
+
#define SPRITE_POINTERS ((volatile uint8_t*)0x07F8) /* last 8 bytes of screen RAM */
|
|
66
|
+
|
|
67
|
+
/* ── Screen layout (the raster split divides bar from the scrolling road) ────
|
|
68
|
+
* char row 0 — score bar text: DST / BEST / CR / mode (FIXED)
|
|
69
|
+
* char row 1 — solid divider line (FIXED)
|
|
70
|
+
* char row 2 — blank spacer: the split lands mid-row HERE, where a few
|
|
71
|
+
* raster lines of IRQ jitter (and the YSCROLL row-smear) are
|
|
72
|
+
* invisible (uniform color)
|
|
73
|
+
* char rows 3-24 — the vertically-scrolling road
|
|
74
|
+
* PAL raster geometry: with YSCROLL=3 (the power-on default) text row r
|
|
75
|
+
* occupies raster lines 51+8r .. 58+8r. So the spacer row 2 = lines 67-74,
|
|
76
|
+
* and the playfield's first row 3 starts at line 75. */
|
|
77
|
+
#define FIELD_TOP 3
|
|
78
|
+
#define SPLIT_LINE 68 /* inside spacer row 2 (67-74): jitter-proof */
|
|
79
|
+
#define BOTTOM_LINE 251 /* first line below the 25-row text window (ends 250) */
|
|
80
|
+
/* $D011 values for the two halves of the frame. Keep DEN (bit4, screen on),
|
|
81
|
+
* RSEL (bit3, 25 rows) set and bit7 (raster compare bit 8) CLEAR (both split
|
|
82
|
+
* lines < 256); the low 3 bits are the fine Y-scroll 0-7. */
|
|
83
|
+
#define D011_KEEP 0x18 /* DEN + RSEL, bit7=0 — the constant part */
|
|
84
|
+
#define D011_BAR 0x1B /* fine Y = 3 (power-on) — the fixed bar */
|
|
85
|
+
|
|
86
|
+
/* ── GAME LOGIC (clay — reshape freely) — sprite art (24×21, 3 bytes/row) ──
|
|
87
|
+
* Two VIC-II hardware sprites: P1's car and P2's car (versus). The road,
|
|
88
|
+
* lane lines, shoulders and traffic are all CHARACTERS in screen RAM (the
|
|
89
|
+
* scroll shifts them), so they cost no sprite slots. */
|
|
90
|
+
#define SLOT_P1 0
|
|
91
|
+
#define SLOT_P2 1
|
|
92
|
+
#define SPR_DATA(img) (0x3F00 + (img) * 64)
|
|
93
|
+
#define SPR_PTR(img) (uint8_t)(SPR_DATA(img) / 64) /* $3F00/64 = $FC */
|
|
94
|
+
#define IMG_CAR 0
|
|
95
|
+
|
|
96
|
+
static const uint8_t car_sprite[64] = { /* a little top-down car, nose up */
|
|
97
|
+
0x00,0x00,0x00, 0x03,0xC0,0x00, 0x07,0xE0,0x00, 0x07,0xE0,0x00,
|
|
98
|
+
0x3F,0xFC,0x00, 0x7F,0xFE,0x00, 0x7F,0xFE,0x00, 0x66,0x66,0x00,
|
|
99
|
+
0x66,0x66,0x00, 0x7F,0xFE,0x00, 0x7F,0xFE,0x00, 0x7F,0xFE,0x00,
|
|
100
|
+
0x66,0x66,0x00, 0x66,0x66,0x00, 0x7F,0xFE,0x00, 0x7F,0xFE,0x00,
|
|
101
|
+
0x3F,0xFC,0x00, 0x18,0x18,0x00, 0x18,0x18,0x00, 0,0,0, 0,0,0, 0,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
105
|
+
* THE RASTER-IRQ SPLIT — the C64's classic "fixed status bar over a moving
|
|
106
|
+
* world" trick (and the gateway drug to all raster effects). The VIC-II has
|
|
107
|
+
* ONE $D011 fine Y-scroll for the whole frame; to scroll the road while the
|
|
108
|
+
* score bar stays put, you change $D011's YSCROLL MID-FRAME, at an exact
|
|
109
|
+
* raster line, from an interrupt. Two IRQs ping-pong per frame:
|
|
110
|
+
*
|
|
111
|
+
* line 68 (inside the blank spacer row): $D011 = road fine-Y scroll
|
|
112
|
+
* → everything drawn below this line scrolls
|
|
113
|
+
* line 251 (just past the text window): $D011 = bar Y-scroll (3)
|
|
114
|
+
* → next frame's bar rows render fixed; this IRQ is also the
|
|
115
|
+
* game's frame heartbeat (increments frame_count)
|
|
116
|
+
*
|
|
117
|
+
* The handshake, register by register:
|
|
118
|
+
* $D012 raster compare line (low 8 bits)
|
|
119
|
+
* $D011 b7 raster compare bit 8 — MUST be 0 here (both lines < 256).
|
|
120
|
+
* We rewrite $D011 every split with bit7 left clear; forgetting
|
|
121
|
+
* it is the classic "my IRQ fires on the wrong line / twice"
|
|
122
|
+
* bug once lines ≥ 256 get involved.
|
|
123
|
+
* $D01A b0 raster IRQ enable
|
|
124
|
+
* $D019 IRQ latch. ACK by WRITING THE BITS BACK (write-1-to-clear).
|
|
125
|
+
* THE LOAD-BEARING LINE: skip the ack and the IRQ re-fires the
|
|
126
|
+
* instant it returns, forever — the main loop starves and the
|
|
127
|
+
* machine looks hung.
|
|
128
|
+
* $0314/15 the KERNAL's IRQ indirection. The hardware vector ($FFFE)
|
|
129
|
+
* points into KERNAL ROM, which saves A/X/Y and jumps through
|
|
130
|
+
* $0314 — so with the KERNAL banked in (cc65 default) we just
|
|
131
|
+
* repoint $0314. Exit via jmp $EA81 (KERNAL: restore regs +
|
|
132
|
+
* rti), SKIPPING $EA31's jiffy-clock/keyboard scan.
|
|
133
|
+
* $DC0D CIA1 interrupt control. The KERNAL leaves a 60Hz CIA timer
|
|
134
|
+
* IRQ running (the jiffy clock); disable it ($7F = clear all
|
|
135
|
+
* sources) and ack it (read $DC0D) or it shares the IRQ line
|
|
136
|
+
* with the raster and fires our handler at random lines.
|
|
137
|
+
*
|
|
138
|
+
* Y-SCROLL SMEAR + JITTER: changing YSCROLL mid-frame makes the VIC repeat or
|
|
139
|
+
* drop a few pixel rows at the split line, and the IRQ itself starts 0-7
|
|
140
|
+
* cycles late plus the KERNAL thunk (~35 cycles) — so the $D011 write lands
|
|
141
|
+
* one-to-two raster lines after SPLIT_LINE. We hide BOTH by splitting inside a
|
|
142
|
+
* UNIFORM blank spacer row, where a smeared/shifted blank row changes nothing.
|
|
143
|
+
* Splits next to visible detail need cycle-exact stabilization (double-IRQ
|
|
144
|
+
* trick) — don't go there until you need to.
|
|
145
|
+
*
|
|
146
|
+
* The handler is ASSEMBLY-IN-C on purpose: cc65's generated C uses shared
|
|
147
|
+
* zero-page scratch registers, so a C-level IRQ body would corrupt whatever
|
|
148
|
+
* the main loop was computing. These asm lines touch only A + the flags
|
|
149
|
+
* (which the KERNAL thunk already saved). requires: KERNAL banked in,
|
|
150
|
+
* frame_count/field_d011 file-scope NON-static (asm %v needs the symbol). */
|
|
151
|
+
volatile uint8_t frame_count; /* bumped by the bottom IRQ — frame heartbeat */
|
|
152
|
+
volatile uint8_t field_d011; /* road $D011 value, precomputed by main */
|
|
153
|
+
|
|
154
|
+
void raster_irq(void) {
|
|
155
|
+
asm("lda $d019"); /* read VIC IRQ latch... */
|
|
156
|
+
asm("sta $d019"); /* ...write it back = ACK (write-1-to-clear).
|
|
157
|
+
* THE line you must not lose (see above). */
|
|
158
|
+
asm("lda $d012"); /* which raster line woke us? (self-correcting
|
|
159
|
+
* dispatch — no phase variable to desync) */
|
|
160
|
+
asm("cmp #150");
|
|
161
|
+
asm("bcs %g", at_bottom); /* ≥150 → we're at BOTTOM_LINE */
|
|
162
|
+
/* — split point (line ~68, inside the blank spacer row) — */
|
|
163
|
+
asm("lda %v", field_d011);
|
|
164
|
+
asm("sta $d011"); /* road fine-Y from here down */
|
|
165
|
+
asm("lda #251"); /* = BOTTOM_LINE (cc65's asm %b only takes */
|
|
166
|
+
asm("sta $d012"); /* signed bytes, so these are literals — the */
|
|
167
|
+
asm("jmp $ea81"); /* #if below keeps them honest) */
|
|
168
|
+
at_bottom:
|
|
169
|
+
asm("lda #$1B"); /* = D011_BAR */
|
|
170
|
+
asm("sta $d011"); /* bar Y-scroll for the top of the NEXT frame */
|
|
171
|
+
asm("inc %v", frame_count);/* frame heartbeat for the main loop */
|
|
172
|
+
asm("lda #%b", SPLIT_LINE);
|
|
173
|
+
asm("sta $d012"); /* next stop: the split line */
|
|
174
|
+
asm("jmp $ea81"); /* KERNAL: pla/tay/pla/tax/pla/rti */
|
|
175
|
+
}
|
|
176
|
+
#if BOTTOM_LINE != 251 || D011_BAR != 0x1B
|
|
177
|
+
#error raster_irq's asm immediates are out of sync with BOTTOM_LINE / D011_BAR
|
|
178
|
+
#endif
|
|
179
|
+
|
|
180
|
+
static void install_raster_irq(void) {
|
|
181
|
+
asm("sei"); /* no IRQs while we rewire them */
|
|
182
|
+
POKE(CIA1_PRA + 0x0D, 0x7F); /* $DC0D: disable ALL CIA1 IRQ sources
|
|
183
|
+
* (kills the KERNAL jiffy/keyboard IRQ
|
|
184
|
+
* — we read the sticks ourselves) */
|
|
185
|
+
(void)PEEK(CIA1_PRA + 0x0D); /* reading $DC0D acks anything pending */
|
|
186
|
+
POKE(0x0314, (uint8_t)((unsigned)raster_irq & 0xFF));
|
|
187
|
+
POKE(0x0315, (uint8_t)((unsigned)raster_irq >> 8));
|
|
188
|
+
POKE(VIC_CTRL1, D011_BAR); /* $D011: screen on, 25 rows, YSCROLL=3,
|
|
189
|
+
* bit7 (raster compare bit8) = 0 — both
|
|
190
|
+
* our lines are < 256 */
|
|
191
|
+
POKE(VIC_RASTER, SPLIT_LINE); /* first stop */
|
|
192
|
+
POKE(VIC_IRQ_ENA, 0x01); /* raster IRQ on */
|
|
193
|
+
POKE(VIC_IRQ, 0xFF); /* clear any stale latch bits */
|
|
194
|
+
asm("cli");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* Wait for the bottom IRQ's heartbeat. Replaces the usual poll-$D012 loop —
|
|
198
|
+
* the IRQ owns the raster now, the main loop just paces itself on it. */
|
|
199
|
+
static void wait_frame(void) {
|
|
200
|
+
uint8_t f = frame_count;
|
|
201
|
+
while (frame_count == f) { }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/* ── HARDWARE IDIOM (load-bearing — see TROUBLESHOOTING) ── reading BOTH
|
|
205
|
+
* joystick ports. CIA1 port A ($DC00) = control port 2, port B ($DC01) =
|
|
206
|
+
* control port 1. Active-low: a pressed switch reads 0, so invert and mask
|
|
207
|
+
* to bits 0-4 (up/down/left/right/fire).
|
|
208
|
+
*
|
|
209
|
+
* THE PORT-1 GOTCHA: $DC01 is ALSO the keyboard row register — the matrix
|
|
210
|
+
* hangs off the same CIA lines. Writing $FF to $DC00 first deselects every
|
|
211
|
+
* keyboard column, so held keys can't pull $DC01 rows low and ghost into
|
|
212
|
+
* the port-1 stick. That's also why "port 2 is the C64 game port": P1 lives
|
|
213
|
+
* there by convention, and this game puts the SECOND player on port 1.
|
|
214
|
+
* requires: install_raster_irq already disabled the KERNAL's keyboard scan,
|
|
215
|
+
* so nothing else rewrites $DC00. */
|
|
216
|
+
static uint8_t read_stick_port2(void) { /* player 1 */
|
|
217
|
+
POKE(CIA1_PRA, 0xFF);
|
|
218
|
+
return (uint8_t)(~PEEK(CIA1_PRA) & 0x1F);
|
|
219
|
+
}
|
|
220
|
+
static uint8_t read_stick_port1(void) { /* player 2 */
|
|
221
|
+
POKE(CIA1_PRA, 0xFF);
|
|
222
|
+
return (uint8_t)(~PEEK(CIA1_PRB) & 0x1F);
|
|
223
|
+
}
|
|
224
|
+
#define JOY_UP 0x01
|
|
225
|
+
#define JOY_DOWN 0x02
|
|
13
226
|
#define JOY_LEFT 0x04
|
|
14
227
|
#define JOY_RIGHT 0x08
|
|
228
|
+
#define JOY_FIRE 0x10
|
|
15
229
|
|
|
16
|
-
|
|
230
|
+
/* ── HARDWARE IDIOM (load-bearing) — best-distance persistence: DISK SAVE ─────────
|
|
231
|
+
* The C64 has no battery SRAM — the honest save medium is the FLOPPY. A game
|
|
232
|
+
* persists by writing a file to drive 8; VICE commits it into the live 1541
|
|
233
|
+
* disk image (true-drive GCR write-back), so a save survives a power cycle
|
|
234
|
+
* exactly as it did on real hardware. (To capture it headlessly the host does
|
|
235
|
+
* state({op:'exportDisk', path}); re-loading that .d64 restores the save.)
|
|
236
|
+
*
|
|
237
|
+
* REQUIRES THE GAME RUN FROM A DISK: build/package it as a .d64 and load THAT
|
|
238
|
+
* (loadMedia autostarts it). A bare .prg injected straight into RAM has no
|
|
239
|
+
* mounted disk to save to, so the save is a silent no-op — still honest (the
|
|
240
|
+
* value just stays in-session), it simply has nowhere to persist.
|
|
241
|
+
*
|
|
242
|
+
* We keep a 2-byte record in a SEQ file "HI" on drive 8. These are the STABLE
|
|
243
|
+
* SEAM: the game calls best_load at boot and best_save on a new record;
|
|
244
|
+
* reshape the record format freely, just keep the two function signatures. */
|
|
245
|
+
#define SAVE_NAME "@0:HI,S,W" /* @ = replace-if-exists; S=SEQ, W=write */
|
|
246
|
+
#define LOAD_NAME "0:HI,S,R"
|
|
17
247
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
248
|
+
static uint16_t best_load(void) {
|
|
249
|
+
uint16_t v = 0;
|
|
250
|
+
uint8_t buf[2];
|
|
251
|
+
if (cbm_open(2, 8, 2, LOAD_NAME) == 0) {
|
|
252
|
+
if (cbm_read(2, buf, 2) == 2) v = (uint16_t)buf[0] | ((uint16_t)buf[1] << 8);
|
|
253
|
+
cbm_close(2);
|
|
254
|
+
}
|
|
255
|
+
return v; /* 0 if the file isn't there yet (first ever boot) */
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
static void best_save(uint16_t v) {
|
|
259
|
+
uint8_t buf[2];
|
|
260
|
+
buf[0] = (uint8_t)(v & 0xFF);
|
|
261
|
+
buf[1] = (uint8_t)(v >> 8);
|
|
262
|
+
if (cbm_open(2, 8, 2, SAVE_NAME) == 0) {
|
|
263
|
+
cbm_write(2, buf, 2);
|
|
264
|
+
cbm_close(2);
|
|
265
|
+
}
|
|
266
|
+
/* No disk mounted (ran as a bare .prg) -> cbm_open fails -> silent no-op. */
|
|
267
|
+
}
|
|
22
268
|
|
|
23
|
-
|
|
269
|
+
/* ── GAME LOGIC (clay) — SID music: 2 voices + THE filter sweep ─────────────
|
|
270
|
+
* Voice 0 = melody (pulse), voice 1 = bass (sawtooth THROUGH THE FILTER),
|
|
271
|
+
* voice 2 is reserved for sound effects (c64_sfx). Each voice walks a
|
|
272
|
+
* (freq, frames) note table once per frame; end wraps → continuous loop.
|
|
273
|
+
*
|
|
274
|
+
* THE SID FILTER — the C64's sonic signature, and the part most "music
|
|
275
|
+
* drivers ported from other chips" miss. One analog-modeled filter, shared
|
|
276
|
+
* by all voices, four registers:
|
|
277
|
+
* $D415 cutoff low 3 bits $D416 cutoff high 8 bits (11-bit total)
|
|
278
|
+
* $D417 high nibble = resonance 0-15; low 3 bits ROUTE voices into the
|
|
279
|
+
* filter (bit0=voice0, bit1=voice1, bit2=voice2)
|
|
280
|
+
* $D418 bit4=lowpass bit5=bandpass bit6=highpass — AND master volume in
|
|
281
|
+
* bits 0-3. Volume and filter mode share a register: any "set
|
|
282
|
+
* volume" helper that writes plain $0F silently turns the filter
|
|
283
|
+
* OFF (c64_sfx's sfx_init does exactly that, so music_init runs
|
|
284
|
+
* AFTER it and re-asserts the mode bits).
|
|
285
|
+
* FOOTGUN: bit 7 of $D418 is "3OFF" — it MUTES voice 3 entirely.
|
|
286
|
+
* Set it by accident and all your sound effects vanish.
|
|
287
|
+
* The sweep: a triangle LFO walks the cutoff up and down each frame over
|
|
288
|
+
* the resonant lowpass — the bass goes from muffled to snarling and back,
|
|
289
|
+
* the "wah" that screams Commodore. Hear it change: that IS the chip. */
|
|
290
|
+
#define N_A2 0x0F3Cu
|
|
291
|
+
#define N_C3 0x1199u
|
|
292
|
+
#define N_D3 0x13EEu
|
|
293
|
+
#define N_E3 0x1666u
|
|
294
|
+
#define N_F3 0x1798u
|
|
295
|
+
#define N_G3 0x1AE6u
|
|
296
|
+
#define N_A3 0x1E78u
|
|
297
|
+
#define N_B3 0x2253u
|
|
298
|
+
#define N_C4 0x2333u
|
|
299
|
+
#define N_D4 0x27DDu
|
|
300
|
+
#define N_E4 0x2CCCu
|
|
301
|
+
#define N_F4 0x2F30u
|
|
302
|
+
#define N_G4 0x35CCu
|
|
303
|
+
#define N_A4 0x3CF1u
|
|
304
|
+
#define N_B4 0x44A7u
|
|
305
|
+
#define N_C5 0x4666u
|
|
306
|
+
#define N_D5 0x4FBAu
|
|
307
|
+
#define N_E5 0x5998u
|
|
308
|
+
#define N_G5 0x6B99u
|
|
309
|
+
#define N_REST 0u
|
|
310
|
+
#define STEP 8 /* frames per melodic eighth-note (~155 BPM PAL) */
|
|
24
311
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
312
|
+
typedef struct { uint16_t freq; uint8_t len; } Note;
|
|
313
|
+
|
|
314
|
+
/* The table IS the song — edit these to rescore your fork. A driving riff
|
|
315
|
+
* over a pumping bass; the road never stops, neither does the loop. */
|
|
316
|
+
static const Note melody[] = {
|
|
317
|
+
{ N_E4, STEP }, { N_G4, STEP }, { N_A4, STEP*2 }, { N_G4, STEP }, { N_E4, STEP }, { N_A4, STEP*2 },
|
|
318
|
+
{ N_E4, STEP }, { N_G4, STEP }, { N_C5, STEP*2 }, { N_B4, STEP }, { N_A4, STEP }, { N_G4, STEP*2 },
|
|
319
|
+
{ N_D4, STEP }, { N_F4, STEP }, { N_A4, STEP*2 }, { N_F4, STEP }, { N_D4, STEP }, { N_A4, STEP*2 },
|
|
320
|
+
{ N_E4, STEP }, { N_G4, STEP }, { N_B4, STEP }, { N_D5, STEP }, { N_C5, STEP*2 }, { N_REST, STEP },
|
|
321
|
+
{ N_A4, STEP }, { N_C5, STEP }, { N_E5, STEP*2 }, { N_C5, STEP }, { N_A4, STEP*2 }, { N_G4, STEP },
|
|
322
|
+
{ N_E4, STEP }, { N_G4, STEP }, { N_A4, STEP }, { N_B4, STEP }, { N_A4, STEP*2 }, { N_G4, STEP*2 },
|
|
323
|
+
};
|
|
324
|
+
static const Note bassline[] = {
|
|
325
|
+
/* Octave-pumping bass — the filter sweep chews on this. */
|
|
326
|
+
{ N_A2, STEP*3 }, { N_A3, STEP }, { N_A2, STEP*2 }, { N_E3, STEP*2 },
|
|
327
|
+
{ N_C3, STEP*3 }, { N_C4, STEP }, { N_C3, STEP*2 }, { N_G3, STEP*2 },
|
|
328
|
+
{ N_D3, STEP*3 }, { N_D4, STEP }, { N_D3, STEP*2 }, { N_A3, STEP*2 },
|
|
329
|
+
{ N_E3, STEP*3 }, { N_B3, STEP }, { N_E3, STEP*2 }, { N_G3, STEP*2 },
|
|
33
330
|
};
|
|
331
|
+
#define MELODY_LEN (sizeof(melody) / sizeof(melody[0]))
|
|
332
|
+
#define BASS_LEN (sizeof(bassline) / sizeof(bassline[0]))
|
|
333
|
+
|
|
334
|
+
static uint8_t m_pos[2], m_left[2];
|
|
335
|
+
static uint16_t filter_cut; /* 11-bit cutoff, 0-2047 */
|
|
336
|
+
static uint8_t filter_up;
|
|
337
|
+
|
|
338
|
+
static void music_trigger(uint8_t v, uint16_t freq, uint8_t wave) {
|
|
339
|
+
if (freq == N_REST) {
|
|
340
|
+
POKE(SID_CTRL(v), wave); /* gate off: release tail plays */
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
POKE(SID_FREQ_LO(v), (uint8_t)(freq & 0xFF));
|
|
344
|
+
POKE(SID_FREQ_HI(v), (uint8_t)(freq >> 8));
|
|
345
|
+
POKE(SID_CTRL(v), wave); /* gate OFF then ON — the 6581/8580 */
|
|
346
|
+
POKE(SID_CTRL(v), wave | SID_GATE); /* envelope only retriggers on the
|
|
347
|
+
* 0→1 gate edge */
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
static void music_init(void) {
|
|
351
|
+
/* Melody: pulse at 50% duty, snappy envelope. */
|
|
352
|
+
POKE(SID_PW_LO(0), 0x00); POKE(SID_PW_HI(0), 0x08);
|
|
353
|
+
POKE(SID_AD(0), 0x07); /* attack 0, decay 7 */
|
|
354
|
+
POKE(SID_SR(0), 0x84); /* sustain 8, release 4 */
|
|
355
|
+
/* Bass: sawtooth (harmonically rich — gives the filter teeth to chew). */
|
|
356
|
+
POKE(SID_AD(1), 0x06);
|
|
357
|
+
POKE(SID_SR(1), 0xA5);
|
|
358
|
+
/* Filter: route VOICE 1 ONLY into it (bit 1 of $D417), resonance 13/15. */
|
|
359
|
+
POKE(SID_RES_FILT, 0xD2);
|
|
360
|
+
/* Lowpass mode + master volume 15. NOTE bits shared with volume, and bit
|
|
361
|
+
* 7 (3OFF) stays 0 or voice-2 sound effects go silent — see block doc. */
|
|
362
|
+
POKE(SID_VOL_MODE, 0x1F);
|
|
363
|
+
filter_cut = 0x180; filter_up = 1;
|
|
364
|
+
m_pos[0] = m_pos[1] = 0;
|
|
365
|
+
m_left[0] = m_left[1] = 1; /* triggers both voices on the next update */
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
static void music_update(void) {
|
|
369
|
+
/* Note sequencing, one table per voice. */
|
|
370
|
+
if (--m_left[0] == 0) {
|
|
371
|
+
music_trigger(0, melody[m_pos[0]].freq, SID_PULSE);
|
|
372
|
+
m_left[0] = melody[m_pos[0]].len;
|
|
373
|
+
if (++m_pos[0] >= MELODY_LEN) m_pos[0] = 0;
|
|
374
|
+
}
|
|
375
|
+
if (--m_left[1] == 0) {
|
|
376
|
+
music_trigger(1, bassline[m_pos[1]].freq, SID_SAWTOOTH);
|
|
377
|
+
m_left[1] = bassline[m_pos[1]].len;
|
|
378
|
+
if (++m_pos[1] >= BASS_LEN) m_pos[1] = 0;
|
|
379
|
+
}
|
|
380
|
+
/* THE FILTER SWEEP — triangle LFO on the cutoff, ~10s round trip.
|
|
381
|
+
* 11-bit value split across two registers: low 3 bits in $D415,
|
|
382
|
+
* high 8 in $D416. */
|
|
383
|
+
if (filter_up) { filter_cut += 6; if (filter_cut >= 0x700) filter_up = 0; }
|
|
384
|
+
else { filter_cut -= 6; if (filter_cut <= 0x180) filter_up = 1; }
|
|
385
|
+
POKE(SID_FILTER_LO, (uint8_t)(filter_cut & 0x07));
|
|
386
|
+
POKE(SID_FILTER_HI, (uint8_t)(filter_cut >> 3));
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/* ── GAME LOGIC (clay) — screen text. The C64 has NO VRAM port: screen RAM
|
|
390
|
+
* is plain memory, writable any time, mid-frame, no vblank dance. The only
|
|
391
|
+
* translation is ASCII → SCREEN CODES (not PETSCII!): A-Z land at 1-26;
|
|
392
|
+
* space through '?' (incl. digits) keep their ASCII values. ── */
|
|
393
|
+
static void draw_text(uint8_t row, uint8_t col, const char *s) {
|
|
394
|
+
uint16_t off = (uint16_t)row * 40 + col;
|
|
395
|
+
uint8_t ch;
|
|
396
|
+
while ((ch = (uint8_t)*s++) != 0) {
|
|
397
|
+
if (ch >= 'A' && ch <= 'Z') ch -= 64; /* A-Z → screen codes 1-26 */
|
|
398
|
+
SCREEN[off] = ch; /* 32-63 map straight through */
|
|
399
|
+
COLORS[off] = COLOR_WHITE;
|
|
400
|
+
++off;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
static void draw_u16(uint8_t row, uint8_t col, uint16_t v) {
|
|
405
|
+
uint8_t i, d[5];
|
|
406
|
+
uint16_t off = (uint16_t)row * 40 + col;
|
|
407
|
+
for (i = 0; i < 5; i++) { d[i] = v % 10; v /= 10; }
|
|
408
|
+
for (i = 0; i < 5; i++) {
|
|
409
|
+
SCREEN[off + i] = (uint8_t)('0' + d[4 - i]); /* digit screen code = ASCII */
|
|
410
|
+
COLORS[off + i] = COLOR_WHITE;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/* ── GAME LOGIC (clay) — xorshift-style PRNG (cheap, period 255) ── */
|
|
415
|
+
static uint8_t rng_state = 0x4D;
|
|
416
|
+
static uint8_t rand8(void) {
|
|
417
|
+
uint8_t lsb = (uint8_t)(rng_state & 1);
|
|
418
|
+
rng_state >>= 1;
|
|
419
|
+
if (lsb) rng_state ^= 0xB8;
|
|
420
|
+
return rng_state;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/* ── GAME LOGIC (clay) — THE ROAD ────────────────────────────────────────────
|
|
424
|
+
* The playfield is a top-down road that scrolls DOWN past cars parked near
|
|
425
|
+
* the bottom. Chars 3..24 are the road; the layout per char column:
|
|
426
|
+
* 0..ROAD_L-1 grass (left berm)
|
|
427
|
+
* ROAD_L solid shoulder line
|
|
428
|
+
* ROAD_L+1 .. ROAD_R-1 asphalt, with dashed lane lines and the double
|
|
429
|
+
* center divider at CENTER (the 2P territory border)
|
|
430
|
+
* ROAD_R solid shoulder line
|
|
431
|
+
* ROAD_R+1 .. 39 grass (right berm)
|
|
432
|
+
* Four 4-cell lanes sit between the shoulders; lane centers in lane_col[]. */
|
|
433
|
+
#define ROAD_L 11 /* left shoulder column */
|
|
434
|
+
#define ROAD_R 28 /* right shoulder column */
|
|
435
|
+
#define CENTER 20 /* double-line center divider (2P border) */
|
|
436
|
+
#define LANE_DASH1 14 /* dashed lane line between lanes 0 and 1 */
|
|
437
|
+
#define LANE_DASH2 25 /* dashed lane line between lanes 2 and 3 */
|
|
438
|
+
static const uint8_t lane_col[4] = { 12, 17, 22, 27 }; /* lane center cols */
|
|
439
|
+
|
|
440
|
+
/* Char codes for road cells. */
|
|
441
|
+
#define CH_GRASS 0x66 /* checker glyph = textured grass */
|
|
442
|
+
#define CH_SHOULDER 0xA0 /* reverse-space solid = the white edge line */
|
|
443
|
+
#define CH_ASPHALT 0x20 /* blank = open asphalt */
|
|
444
|
+
#define CH_DASH 0x5D /* vertical bar glyph = lane dashes */
|
|
445
|
+
#define CH_DIVIDE 0xA0 /* reverse-space solid = double center line */
|
|
446
|
+
#define CH_TRAFFIC 0x51 /* filled circle glyph = rival traffic car */
|
|
447
|
+
#define CH_BLANK 0x20
|
|
448
|
+
|
|
449
|
+
/* The STATIC color texture (paint_colors) never scrolls — the row shift moves
|
|
450
|
+
* only the CHARS, and they pick up each cell's resident color for free. We
|
|
451
|
+
* lay the road colors PER COLUMN (grass green, shoulders gray, asphalt dark),
|
|
452
|
+
* uniform down every row, so the coarse shift costs half the byte-moves. ── */
|
|
453
|
+
static uint8_t col_color[40];
|
|
454
|
+
static void build_col_color(void) {
|
|
455
|
+
uint8_t c;
|
|
456
|
+
for (c = 0; c < 40; c++) {
|
|
457
|
+
if (c < ROAD_L || c > ROAD_R) col_color[c] = COLOR_GREEN; /* grass */
|
|
458
|
+
else if (c == ROAD_L || c == ROAD_R) col_color[c] = COLOR_LIGHT_GRAY; /* shoulder */
|
|
459
|
+
else if (c == CENTER) col_color[c] = COLOR_YELLOW; /* divider */
|
|
460
|
+
else if (c == LANE_DASH1 || c == LANE_DASH2) col_color[c] = COLOR_LIGHT_GRAY;
|
|
461
|
+
else col_color[c] = COLOR_DARK_GRAY; /* asphalt */
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/* Paint the STATIC color texture for the whole road window — ONCE, at boot. */
|
|
466
|
+
static void paint_colors(void) {
|
|
467
|
+
uint8_t r, c;
|
|
468
|
+
for (r = FIELD_TOP; r < 25; r++) {
|
|
469
|
+
volatile uint8_t *crow = COLORS + (uint16_t)r * 40;
|
|
470
|
+
for (c = 0; c < 40; c++) crow[c] = col_color[c];
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
static uint8_t road_phase; /* dashed-line animation phase, world row */
|
|
475
|
+
|
|
476
|
+
/* Stamp ONE road row's CHARS into screen RAM at screen row `sr`. `phase`
|
|
477
|
+
* walks the dashed-line pattern so the lane dashes animate as rows scroll.
|
|
478
|
+
* The COARSE scroll calls this once per 8 px (for the freshly exposed TOP
|
|
479
|
+
* edge), NOT per cell of the whole screen — a full 22-row repaint of cc65 C
|
|
480
|
+
* is ~50 frames (a frozen second). Keep it lean. */
|
|
481
|
+
static void draw_road_row(uint8_t sr, uint8_t phase) {
|
|
482
|
+
uint8_t c;
|
|
483
|
+
uint8_t *s = (uint8_t*)(0x0400) + (uint16_t)sr * 40; /* plain RAM (see scroll_field) */
|
|
484
|
+
uint8_t dash = (uint8_t)((phase & 3) < 2); /* 4-on/4-off dash */
|
|
485
|
+
for (c = 0; c < 40; c++) {
|
|
486
|
+
uint8_t ch;
|
|
487
|
+
if (c < ROAD_L || c > ROAD_R) ch = CH_GRASS;
|
|
488
|
+
else if (c == ROAD_L || c == ROAD_R) ch = CH_SHOULDER;
|
|
489
|
+
else if (c == CENTER) ch = CH_DIVIDE;
|
|
490
|
+
else if ((c == LANE_DASH1 || c == LANE_DASH2) && dash) ch = CH_DASH;
|
|
491
|
+
else ch = CH_ASPHALT;
|
|
492
|
+
*s++ = ch;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/* Repaint the WHOLE visible road window's CHARS. Runs ONCE per race start
|
|
497
|
+
* (not per frame). */
|
|
498
|
+
static void paint_road(void) {
|
|
499
|
+
uint8_t sr;
|
|
500
|
+
for (sr = FIELD_TOP; sr < 25; sr++) draw_road_row(sr, sr);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/* ── HARDWARE IDIOM (load-bearing) — the vertical COARSE scroll. The road
|
|
504
|
+
* moves DOWN (toward the player), so we shift the 22 visible road rows one
|
|
505
|
+
* char DOWN in SCREEN RAM and stamp a fresh row at the TOP. Color RAM is the
|
|
506
|
+
* static texture (paint_colors), so this touches ONLY screen RAM — half the
|
|
507
|
+
* byte-moves. Runs only on the frame the fine offset wraps (every 8 px).
|
|
508
|
+
* SCHEDULING IS THE TRICK: called right after wait_frame() (i.e. just after
|
|
509
|
+
* the line-251 IRQ). The beam won't draw road row 3 until line 75 of the NEXT
|
|
510
|
+
* frame (~8500 cycles away) and then takes 504 cycles/row; this loop spends
|
|
511
|
+
* ~600 cycles/row, so with that head start it stays ahead of the beam — no
|
|
512
|
+
* tearing, no double buffer. We copy bottom-up so a row isn't overwritten
|
|
513
|
+
* before it's read. (The grown-up alternative is page-flipping via $D018.) */
|
|
514
|
+
static void scroll_field(void) {
|
|
515
|
+
uint8_t r;
|
|
516
|
+
/* NON-volatile pointers on purpose: screen RAM is plain memory (not MMIO),
|
|
517
|
+
* so cc65 keeps the running pointer in zero page and emits a tight indexed
|
|
518
|
+
* copy. Marking it volatile (as the per-cell sprite writes do, for mid-frame
|
|
519
|
+
* correctness) would force a reload per access and roughly DOUBLE this
|
|
520
|
+
* loop's cost — and this loop is the scroll's whole frame budget. */
|
|
521
|
+
for (r = 24; r > FIELD_TOP; r--) {
|
|
522
|
+
uint8_t *dst = (uint8_t*)(0x0400) + (uint16_t)r * 40;
|
|
523
|
+
uint8_t *src = dst - 40;
|
|
524
|
+
uint8_t c;
|
|
525
|
+
for (c = 0; c < 40; c++) dst[c] = src[c];
|
|
526
|
+
}
|
|
527
|
+
++road_phase;
|
|
528
|
+
draw_road_row(FIELD_TOP, road_phase); /* fresh road enters at the top */
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/* ── GAME LOGIC (clay) — game state ── */
|
|
532
|
+
#define ST_TITLE 0
|
|
533
|
+
#define ST_PLAY 1
|
|
534
|
+
#define ST_OVER 2
|
|
535
|
+
static uint8_t state;
|
|
536
|
+
static uint8_t two_player;
|
|
537
|
+
static uint8_t winner; /* versus result: 1 = P1 wins, 2 = P2 */
|
|
538
|
+
|
|
539
|
+
/* ── Players + traffic. The cars are VIC-II HARDWARE SPRITES (P1, P2); the
|
|
540
|
+
* road, lane lines and rival traffic are CHARACTERS in the scrolling field. ──
|
|
541
|
+
* 1P: all 4 lanes, UP/FIRE accelerates, DOWN brakes (speed 1..MAX_SPEED).
|
|
542
|
+
* 2P versus: ONE screen = ONE road scroll, so both share a fixed speed and
|
|
543
|
+
* only steer — P1 (port 2) owns the left 2 lanes, P2 (port 1) the right 2,
|
|
544
|
+
* split at the center divider. Each starts with CRASHES_MAX crashes; first
|
|
545
|
+
* to use them all LOSES. */
|
|
546
|
+
#define MAX_TRAFFIC 4
|
|
547
|
+
#define CAR_ROW 20 /* both cars' fixed char row (near the bottom) */
|
|
548
|
+
#define CRASHES_MAX 3
|
|
549
|
+
#define SPAWN_PERIOD 38 /* frames between traffic spawns */
|
|
550
|
+
#define SPEED_2P 2 /* fixed road speed in versus */
|
|
551
|
+
#define MAX_SPEED 5 /* px/frame — keep < 8 so the row streamer's *
|
|
552
|
+
* one-row-per-8px restamp can't skip a row */
|
|
553
|
+
|
|
554
|
+
static uint8_t car_lane[2]; /* which of the 4 lanes (0..3) */
|
|
555
|
+
static uint8_t car_active[2];
|
|
556
|
+
static uint8_t crashes_left[2];
|
|
557
|
+
static uint8_t invuln[2]; /* post-crash blink/no-collide frames */
|
|
558
|
+
static uint8_t lane_min[2], lane_max[2]; /* 2P split territories */
|
|
34
559
|
|
|
35
|
-
|
|
560
|
+
static uint8_t traffic_alive[MAX_TRAFFIC];
|
|
561
|
+
static uint8_t traffic_lane[MAX_TRAFFIC];
|
|
562
|
+
static int16_t traffic_y[MAX_TRAFFIC]; /* world Y in px (top of road = 0) */
|
|
563
|
+
static uint8_t traffic_col[MAX_TRAFFIC]; /* last screen column it was drawn */
|
|
564
|
+
static int16_t traffic_prev_row[MAX_TRAFFIC];
|
|
36
565
|
|
|
37
|
-
static
|
|
38
|
-
static
|
|
39
|
-
static uint8_t
|
|
40
|
-
static
|
|
41
|
-
static uint8_t
|
|
42
|
-
static
|
|
43
|
-
static
|
|
566
|
+
static uint8_t speed; /* road px/frame, 1..MAX_SPEED */
|
|
567
|
+
static uint16_t dist; /* 1P distance, 1 unit = 16 px */
|
|
568
|
+
static uint8_t dist_frac;
|
|
569
|
+
static uint16_t best; /* persisted best 1P distance */
|
|
570
|
+
static uint8_t spawn_timer;
|
|
571
|
+
static uint16_t scroll_px; /* total road px scrolled this run */
|
|
572
|
+
static uint8_t fine_prev;
|
|
573
|
+
static uint8_t start_pause; /* freeze frames at the green light */
|
|
574
|
+
static uint8_t prev0, prev1; /* edge-detect held buttons */
|
|
44
575
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
576
|
+
/* Sprite Y for a char row's top: a sprite at $D001 = 51 + 8*r appears at the
|
|
577
|
+
* top of char row r (window row 0 sits at $D001≈50). Cars sit ON CAR_ROW. */
|
|
578
|
+
#define SPR_Y_FOR_ROW(r) (uint8_t)(51 + 8 * (r))
|
|
579
|
+
/* Lane center → sprite X (24-px visible origin; lane cols are screen chars,
|
|
580
|
+
* minus half the 24-px sprite to center it on the lane). */
|
|
581
|
+
#define LANE_X(lane) (int16_t)((int16_t)lane_col[lane] * 8 + 24 - 12)
|
|
582
|
+
|
|
583
|
+
/* ── HARDWARE IDIOM (load-bearing) — staging a sprite with the 9th X bit.
|
|
584
|
+
* VIC sprite X is 9 bits: low 8 in $D000+2n, bit 8 for ALL sprites packed
|
|
585
|
+
* into $D010. Forget $D010 and anything past X=255 wraps back to the left
|
|
586
|
+
* edge — the classic "my sprite teleports at two-thirds screen" bug. We
|
|
587
|
+
* accumulate the MSB bits while staging and commit the byte once. ── */
|
|
588
|
+
static uint8_t spr_msb, spr_ena;
|
|
589
|
+
static void stage_begin(void) { spr_msb = 0; spr_ena = 0; }
|
|
590
|
+
static void stage_sprite(uint8_t slot, int16_t x, uint8_t y) {
|
|
591
|
+
POKE(VIC_SPRITE_X(slot), (uint8_t)(x & 0xFF));
|
|
592
|
+
POKE(VIC_SPRITE_Y(slot), y);
|
|
593
|
+
if (x > 255) spr_msb |= (uint8_t)(1 << slot);
|
|
594
|
+
spr_ena |= (uint8_t)(1 << slot);
|
|
595
|
+
}
|
|
596
|
+
static void stage_commit(void) {
|
|
597
|
+
POKE(VIC_SPRITES_X8, spr_msb);
|
|
598
|
+
POKE(VIC_SPR_ENA, spr_ena); /* unstaged slots vanish — no stale sprites */
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/* ── GAME LOGIC (clay) — score bar (rows 0-1) ── */
|
|
602
|
+
static void draw_bar_labels(void) {
|
|
603
|
+
uint8_t c;
|
|
604
|
+
for (c = 0; c < 40; c++) { /* row 1: solid divider line */
|
|
605
|
+
SCREEN[40 + c] = 0xA0;
|
|
606
|
+
COLORS[40 + c] = COLOR_DARK_GRAY;
|
|
607
|
+
SCREEN[80 + c] = CH_BLANK; /* row 2: the blank spacer the
|
|
608
|
+
* raster split hides in */
|
|
609
|
+
SCREEN[c] = CH_BLANK;
|
|
610
|
+
}
|
|
611
|
+
draw_text(0, 0, "DST");
|
|
612
|
+
draw_text(0, 11, "BEST");
|
|
613
|
+
draw_text(0, 23, "CR");
|
|
614
|
+
draw_text(0, 32, two_player ? "2P" : "1P");
|
|
615
|
+
}
|
|
616
|
+
static void draw_bar_stats(void) {
|
|
617
|
+
draw_u16(0, 4, dist);
|
|
618
|
+
draw_u16(0, 16, best);
|
|
619
|
+
if (two_player) {
|
|
620
|
+
/* versus: show each player's remaining crashes (P1-P2). */
|
|
621
|
+
SCREEN[26] = (uint8_t)('0' + crashes_left[0]);
|
|
622
|
+
SCREEN[27] = (uint8_t)('-');
|
|
623
|
+
SCREEN[28] = (uint8_t)('0' + crashes_left[1]);
|
|
624
|
+
COLORS[26] = COLOR_CYAN; COLORS[27] = COLOR_WHITE; COLORS[28] = COLOR_GREEN;
|
|
625
|
+
} else {
|
|
626
|
+
SCREEN[26] = (uint8_t)('0' + crashes_left[0]);
|
|
627
|
+
COLORS[26] = COLOR_WHITE;
|
|
628
|
+
}
|
|
48
629
|
}
|
|
49
630
|
|
|
50
|
-
|
|
631
|
+
/* ── GAME LOGIC (clay) — title / start / game over ──────────────────────────
|
|
632
|
+
* Transition rule (see paint_road's note): never repaint the whole field on
|
|
633
|
+
* a fire press. The title draws its text ON TOP of the parked road; start
|
|
634
|
+
* repaints the road once (cheap enough at a state change, not per frame). */
|
|
635
|
+
static void draw_text_band(uint8_t row, uint8_t col, const char *s) {
|
|
636
|
+
uint8_t c;
|
|
637
|
+
volatile uint8_t *p = SCREEN + (uint16_t)row * 40;
|
|
638
|
+
for (c = 0; c < 40; c++) p[c] = CH_BLANK;
|
|
639
|
+
draw_text(row, col, s);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
static void paint_title(void) {
|
|
643
|
+
draw_bar_labels();
|
|
644
|
+
draw_bar_stats();
|
|
645
|
+
draw_text_band(7, (40 - (sizeof(GAME_TITLE) - 1)) / 2, GAME_TITLE);
|
|
646
|
+
draw_text_band(11, 11, "PORT 2 FIRE - 1P");
|
|
647
|
+
draw_text_band(13, 9, "PORT 1 FIRE - 2P VERSUS");
|
|
648
|
+
draw_text_band(17, 15, "BEST");
|
|
649
|
+
draw_u16(17, 20, best);
|
|
650
|
+
field_d011 = D011_BAR; /* title field holds still (text lives in it) */
|
|
651
|
+
POKE(VIC_SPR_ENA, 0);
|
|
652
|
+
state = ST_TITLE;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
static void reset_traffic(void) {
|
|
51
656
|
uint8_t i;
|
|
52
|
-
|
|
53
|
-
|
|
657
|
+
for (i = 0; i < MAX_TRAFFIC; i++) {
|
|
658
|
+
traffic_alive[i] = 0;
|
|
659
|
+
traffic_prev_row[i] = -1;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
static void start_game(uint8_t players) {
|
|
664
|
+
two_player = players;
|
|
665
|
+
winner = 0;
|
|
666
|
+
car_lane[0] = 1; car_lane[1] = 2;
|
|
667
|
+
car_active[0] = 1; car_active[1] = players;
|
|
668
|
+
crashes_left[0] = CRASHES_MAX;
|
|
669
|
+
crashes_left[1] = players ? CRASHES_MAX : 0;
|
|
670
|
+
invuln[0] = invuln[1] = 0;
|
|
671
|
+
if (players) { /* split the road at the divider */
|
|
672
|
+
lane_min[0] = 0; lane_max[0] = 1; /* P1 owns lanes 0-1 (left) */
|
|
673
|
+
lane_min[1] = 2; lane_max[1] = 3; /* P2 owns lanes 2-3 (right) */
|
|
674
|
+
} else {
|
|
675
|
+
lane_min[0] = 0; lane_max[0] = 3; /* 1P: all four lanes */
|
|
676
|
+
}
|
|
677
|
+
speed = players ? SPEED_2P : 2;
|
|
678
|
+
dist = 0; dist_frac = 0;
|
|
679
|
+
spawn_timer = 0;
|
|
680
|
+
scroll_px = 0; fine_prev = 0;
|
|
681
|
+
road_phase = 0;
|
|
682
|
+
start_pause = 40; /* "green light" breather */
|
|
683
|
+
prev0 = prev1 = 0x1F; /* swallow the start FIRE held */
|
|
684
|
+
reset_traffic();
|
|
685
|
+
field_d011 = D011_BAR;
|
|
686
|
+
paint_road(); /* repaint the road once for this run */
|
|
687
|
+
draw_bar_labels();
|
|
688
|
+
draw_bar_stats();
|
|
689
|
+
sfx_tone(2, 0x40, 0x20, 6); /* start chirp */
|
|
690
|
+
state = ST_PLAY;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
static void game_over(void) {
|
|
694
|
+
POKE(VIC_SPR_ENA, 0); /* sprites off before the message paints */
|
|
695
|
+
field_d011 = D011_BAR;
|
|
696
|
+
if (!two_player && dist > best) {
|
|
697
|
+
best = dist;
|
|
698
|
+
best_save(best); /* the persistence seam — see its doc */
|
|
699
|
+
}
|
|
700
|
+
if (two_player) {
|
|
701
|
+
draw_text_band(11, 16, winner == 1 ? "P1 WINS" : "P2 WINS");
|
|
702
|
+
} else {
|
|
703
|
+
draw_text_band(11, 15, "GAME OVER");
|
|
704
|
+
}
|
|
705
|
+
draw_text_band(13, 13, "FIRE - TITLE");
|
|
706
|
+
draw_bar_stats();
|
|
707
|
+
sfx_noise(24);
|
|
708
|
+
state = ST_OVER;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/* ── GAME LOGIC (clay) — a crash: lose one of this player's lives ──────────
|
|
712
|
+
* 1P: out of crashes → game over. 2P versus: the FIRST player to exhaust
|
|
713
|
+
* their crashes loses; the other wins on the spot. */
|
|
714
|
+
static void crash_player(uint8_t p) {
|
|
715
|
+
sfx_noise(16);
|
|
716
|
+
if (crashes_left[p]) --crashes_left[p];
|
|
717
|
+
invuln[p] = 90; /* mercy frames + blink */
|
|
718
|
+
draw_bar_stats();
|
|
719
|
+
if (two_player) {
|
|
720
|
+
if (crashes_left[p] == 0) { winner = (uint8_t)(p == 0 ? 2 : 1); game_over(); }
|
|
721
|
+
} else if (crashes_left[0] == 0) {
|
|
722
|
+
game_over();
|
|
723
|
+
}
|
|
54
724
|
}
|
|
55
725
|
|
|
56
|
-
static void
|
|
726
|
+
static void spawn_traffic(void) {
|
|
57
727
|
uint8_t i;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
728
|
+
for (i = 0; i < MAX_TRAFFIC; i++) {
|
|
729
|
+
if (!traffic_alive[i]) {
|
|
730
|
+
traffic_alive[i] = 1;
|
|
731
|
+
traffic_lane[i] = (uint8_t)(rand8() & 3);
|
|
732
|
+
traffic_y[i] = -8; /* just above the top of the road */
|
|
733
|
+
traffic_prev_row[i] = -1;
|
|
64
734
|
return;
|
|
65
735
|
}
|
|
66
736
|
}
|
|
67
737
|
}
|
|
68
738
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
739
|
+
/* Erase a traffic car's old char (restore the road cell under it) before it
|
|
740
|
+
* moves — otherwise it leaves a trail. Redraw the affected cell from the road
|
|
741
|
+
* template (lane center cells are dash-line or asphalt; never a shoulder). */
|
|
742
|
+
static void clear_traffic_cell(uint8_t row, uint8_t col) {
|
|
743
|
+
uint8_t ch;
|
|
744
|
+
if (row < FIELD_TOP || row > 24) return;
|
|
745
|
+
if (col == CENTER) ch = CH_DIVIDE;
|
|
746
|
+
else if (col == LANE_DASH1 || col == LANE_DASH2)
|
|
747
|
+
ch = ((uint8_t)(((row + road_phase) & 3) < 2)) ? CH_DASH : CH_ASPHALT;
|
|
748
|
+
else ch = CH_ASPHALT;
|
|
749
|
+
SCREEN[(uint16_t)row * 40 + col] = ch;
|
|
750
|
+
COLORS[(uint16_t)row * 40 + col] = col_color[col];
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
static void copy_sprite_image(uint8_t img, const uint8_t *src) {
|
|
754
|
+
uint8_t i;
|
|
755
|
+
volatile uint8_t *dst = (volatile uint8_t*)SPR_DATA(img);
|
|
756
|
+
for (i = 0; i < 64; i++) dst[i] = src[i];
|
|
72
757
|
}
|
|
73
758
|
|
|
74
759
|
void main(void) {
|
|
75
|
-
uint8_t
|
|
760
|
+
uint8_t pad0, pad1, p, i;
|
|
761
|
+
|
|
762
|
+
/* ── HARDWARE IDIOM (load-bearing) — boot order. VIC + SID config before
|
|
763
|
+
* the IRQ goes live; sfx_init BEFORE music_init (sfx_init writes a plain
|
|
764
|
+
* volume to $D418, music_init re-asserts the filter-mode bits on top). ── */
|
|
76
765
|
POKE(VIC_SPR_ENA, 0);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
POKE(
|
|
83
|
-
POKE(
|
|
84
|
-
|
|
85
|
-
/*
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
{
|
|
89
|
-
uint16_t k;
|
|
90
|
-
volatile uint8_t *scr = (volatile uint8_t*)0x0400;
|
|
91
|
-
for (k = 0; k < 1000; k++) scr[k] = 0x20;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
player.x = LANE1_X; player.y = 220; player.alive = 1;
|
|
95
|
-
for (i = 0; i < MAX_OBS; i++) obstacles[i].alive = 0;
|
|
96
|
-
spawn_timer = 0;
|
|
97
|
-
game_over_timer = 0;
|
|
766
|
+
POKE(VIC_BORDER, COLOR_BLACK);
|
|
767
|
+
POKE(VIC_BG0, COLOR_BLACK);
|
|
768
|
+
copy_sprite_image(IMG_CAR, car_sprite);
|
|
769
|
+
SPRITE_POINTERS[SLOT_P1] = SPR_PTR(IMG_CAR);
|
|
770
|
+
SPRITE_POINTERS[SLOT_P2] = SPR_PTR(IMG_CAR);
|
|
771
|
+
POKE(VIC_SPR_COL(SLOT_P1), COLOR_CYAN);
|
|
772
|
+
POKE(VIC_SPR_COL(SLOT_P2), COLOR_GREEN);
|
|
773
|
+
POKE(CIA1_DDRA, 0xFF); /* port A drives keyboard columns */
|
|
774
|
+
POKE(CIA1_DDRB, 0x00); /* port B reads rows / stick 1 */
|
|
775
|
+
|
|
776
|
+
build_col_color();
|
|
98
777
|
sfx_init();
|
|
99
|
-
|
|
778
|
+
music_init();
|
|
779
|
+
best = best_load(); /* 0 until the core save round lands */
|
|
780
|
+
|
|
781
|
+
field_d011 = D011_BAR;
|
|
782
|
+
paint_colors(); /* STATIC color texture — once, ever */
|
|
783
|
+
paint_road(); /* the ONE full-field char paint (boot) */
|
|
784
|
+
install_raster_irq(); /* the split + heartbeat go live */
|
|
785
|
+
paint_title();
|
|
100
786
|
|
|
101
787
|
for (;;) {
|
|
102
|
-
|
|
103
|
-
|
|
788
|
+
wait_frame(); /* the line-251 IRQ paces everything */
|
|
789
|
+
|
|
790
|
+
music_update();
|
|
104
791
|
sfx_update();
|
|
792
|
+
pad0 = read_stick_port2(); /* P1 — control port 2 (convention) */
|
|
793
|
+
pad1 = read_stick_port1(); /* P2 — control port 1 */
|
|
794
|
+
|
|
795
|
+
if (state == ST_TITLE) {
|
|
796
|
+
/* Mode select doubles as a controls demo: the stick that presses FIRE
|
|
797
|
+
* picks the mode — port 2 starts 1P, port 1 starts 2P versus. */
|
|
798
|
+
if ((pad0 & JOY_FIRE) && !(prev0 & JOY_FIRE)) start_game(0);
|
|
799
|
+
else if ((pad1 & JOY_FIRE) && !(prev1 & JOY_FIRE)) start_game(1);
|
|
800
|
+
prev0 = pad0; prev1 = pad1;
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
105
803
|
|
|
106
|
-
if (
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
804
|
+
if (state == ST_OVER) {
|
|
805
|
+
if (((pad0 & JOY_FIRE) && !(prev0 & JOY_FIRE)) ||
|
|
806
|
+
((pad1 & JOY_FIRE) && !(prev1 & JOY_FIRE))) paint_title();
|
|
807
|
+
prev0 = pad0; prev1 = pad1;
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/* ── ST_PLAY ─────────────────────────────────────────────────────────
|
|
812
|
+
* Set field_d011 EARLY — it must be settled long before the beam reaches
|
|
813
|
+
* SPLIT_LINE — and run the coarse shift right after the heartbeat. */
|
|
814
|
+
if (start_pause) {
|
|
815
|
+
--start_pause;
|
|
816
|
+
stage_begin();
|
|
817
|
+
stage_sprite(SLOT_P1, LANE_X(car_lane[0]), SPR_Y_FOR_ROW(CAR_ROW));
|
|
818
|
+
if (two_player) stage_sprite(SLOT_P2, LANE_X(car_lane[1]), SPR_Y_FOR_ROW(CAR_ROW));
|
|
819
|
+
stage_commit();
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/* 1P speed control: UP / FIRE accelerate, DOWN brakes (edge-triggered). */
|
|
824
|
+
if (!two_player) {
|
|
825
|
+
if ((pad0 & (JOY_UP | JOY_FIRE)) && !(prev0 & (JOY_UP | JOY_FIRE)) && speed < MAX_SPEED) {
|
|
826
|
+
++speed; sfx_tone(2, 0x80, 0x18, 3);
|
|
112
827
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
828
|
+
if ((pad0 & JOY_DOWN) && !(prev0 & JOY_DOWN) && speed > 1) {
|
|
829
|
+
--speed; sfx_tone(2, 0x30, 0x10, 3);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/* Steering: LEFT/RIGHT change lane (edge-triggered, clamped to territory).
|
|
834
|
+
* P1 reads port 2, P2 reads port 1. */
|
|
835
|
+
for (p = 0; p < (two_player ? 2 : 1); p++) {
|
|
836
|
+
uint8_t pp = p ? pad1 : pad0;
|
|
837
|
+
uint8_t prevp = p ? prev1 : prev0;
|
|
838
|
+
if (!car_active[p]) continue;
|
|
839
|
+
if ((pp & JOY_LEFT) && !(prevp & JOY_LEFT) && car_lane[p] > lane_min[p]) {
|
|
840
|
+
--car_lane[p]; sfx_tone(2, 0x50, 0x14, 2);
|
|
116
841
|
}
|
|
117
|
-
if ((
|
|
118
|
-
|
|
842
|
+
if ((pp & JOY_RIGHT) && !(prevp & JOY_RIGHT) && car_lane[p] < lane_max[p]) {
|
|
843
|
+
++car_lane[p]; sfx_tone(2, 0x50, 0x14, 2);
|
|
119
844
|
}
|
|
120
|
-
|
|
121
|
-
|
|
845
|
+
if (invuln[p]) --invuln[p];
|
|
846
|
+
}
|
|
847
|
+
prev0 = pad0; prev1 = pad1;
|
|
848
|
+
|
|
849
|
+
/* ── FINE + COARSE vertical scroll. The road moves DOWN: field_d011 low
|
|
850
|
+
* 3 bits = the fine Y offset (counts UP 0..7 as the road advances). When
|
|
851
|
+
* it wraps past a char boundary, COARSE-shift the rows down and stamp a
|
|
852
|
+
* fresh road row at the top. ── */
|
|
853
|
+
scroll_px += speed;
|
|
854
|
+
{
|
|
855
|
+
uint8_t fine = (uint8_t)(scroll_px & 7);
|
|
856
|
+
field_d011 = (uint8_t)(D011_KEEP | fine);
|
|
857
|
+
if (fine < fine_prev) scroll_field(); /* wrapped past 7→0 → coarse step */
|
|
858
|
+
fine_prev = fine;
|
|
859
|
+
}
|
|
122
860
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
861
|
+
/* Distance: 16 scrolled px = 1 unit (≈ one car length). */
|
|
862
|
+
dist_frac += speed;
|
|
863
|
+
while (dist_frac >= 16) { dist_frac -= 16; ++dist; draw_bar_stats(); }
|
|
864
|
+
|
|
865
|
+
/* Traffic: rival cars drift DOWN the road a touch faster than the scroll
|
|
866
|
+
* so the player overtakes them. Erase the old cell, advance, redraw as a
|
|
867
|
+
* char in its lane-center column. */
|
|
868
|
+
++spawn_timer;
|
|
869
|
+
if (spawn_timer >= SPAWN_PERIOD) { spawn_timer = 0; spawn_traffic(); }
|
|
870
|
+
for (i = 0; i < MAX_TRAFFIC; i++) {
|
|
871
|
+
int16_t prow;
|
|
872
|
+
uint8_t col, srow;
|
|
873
|
+
if (!traffic_alive[i]) continue;
|
|
874
|
+
if (traffic_prev_row[i] >= 0) /* erase previous cell */
|
|
875
|
+
clear_traffic_cell((uint8_t)traffic_prev_row[i], traffic_col[i]);
|
|
876
|
+
traffic_y[i] += speed + 1; /* a touch faster than scroll */
|
|
877
|
+
if (traffic_y[i] >= (int16_t)((25 - FIELD_TOP) * 8)) {
|
|
878
|
+
traffic_alive[i] = 0; /* slipped past the bottom */
|
|
879
|
+
traffic_prev_row[i] = -1;
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
prow = (int16_t)(FIELD_TOP + (traffic_y[i] >> 3));
|
|
883
|
+
col = lane_col[traffic_lane[i]];
|
|
884
|
+
srow = (uint8_t)prow;
|
|
885
|
+
if (srow >= FIELD_TOP && srow <= 24) {
|
|
886
|
+
SCREEN[(uint16_t)srow * 40 + col] = CH_TRAFFIC;
|
|
887
|
+
COLORS[(uint16_t)srow * 40 + col] = COLOR_LIGHT_RED;
|
|
888
|
+
traffic_prev_row[i] = prow;
|
|
889
|
+
traffic_col[i] = col;
|
|
890
|
+
} else {
|
|
891
|
+
traffic_prev_row[i] = -1;
|
|
127
892
|
}
|
|
128
|
-
|
|
129
|
-
if (spawn_timer >= 40) { spawn_timer = 0; spawn(); }
|
|
893
|
+
}
|
|
130
894
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
895
|
+
/* Collisions: a player crashes if a live traffic car shares its lane AND
|
|
896
|
+
* is within one char row of CAR_ROW. */
|
|
897
|
+
for (p = 0; p < (two_player ? 2 : 1); p++) {
|
|
898
|
+
if (!car_active[p] || invuln[p]) continue;
|
|
899
|
+
for (i = 0; i < MAX_TRAFFIC; i++) {
|
|
900
|
+
if (!traffic_alive[i]) continue;
|
|
901
|
+
if (traffic_lane[i] != car_lane[p]) continue;
|
|
902
|
+
if (traffic_prev_row[i] >= (int16_t)(CAR_ROW - 1) &&
|
|
903
|
+
traffic_prev_row[i] <= (int16_t)(CAR_ROW + 1)) {
|
|
904
|
+
clear_traffic_cell((uint8_t)traffic_prev_row[i], traffic_col[i]);
|
|
905
|
+
traffic_alive[i] = 0;
|
|
906
|
+
traffic_prev_row[i] = -1;
|
|
907
|
+
crash_player(p);
|
|
135
908
|
break;
|
|
136
909
|
}
|
|
137
910
|
}
|
|
911
|
+
if (state != ST_PLAY) break;
|
|
138
912
|
}
|
|
913
|
+
if (state != ST_PLAY) continue;
|
|
139
914
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
915
|
+
/* Stage the car sprites, then commit enable + X-MSB once. Invulnerable
|
|
916
|
+
* cars blink by skipping their slot every few frames. */
|
|
917
|
+
stage_begin();
|
|
918
|
+
for (p = 0; p < 2; p++) {
|
|
919
|
+
if (!car_active[p]) continue;
|
|
920
|
+
if (invuln[p] & 4) continue; /* blink */
|
|
921
|
+
stage_sprite(p ? SLOT_P2 : SLOT_P1, LANE_X(car_lane[p]), SPR_Y_FOR_ROW(CAR_ROW));
|
|
147
922
|
}
|
|
923
|
+
stage_commit();
|
|
148
924
|
}
|
|
149
925
|
}
|