romdevtools 0.28.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +51 -41
- package/CHANGELOG.md +46 -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 +1 -1
- package/src/host/LibretroHost.js +59 -1
- 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/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/rom-id.js +5 -1
- package/src/mcp/tools/run-until.js +4 -2
- 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 +4 -3
- 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/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,263 +1,568 @@
|
|
|
1
|
-
/* ── racing.c — Game Boy Advance
|
|
1
|
+
/* ── racing.c — Game Boy Advance top-down road racer (complete game) ──────────
|
|
2
2
|
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* VERGE PILOT — a COMPLETE, working game: press-start title, a 1P endless
|
|
4
|
+
* top-down road race, music + SFX, vivid 15-bit colour, and a persistent BEST
|
|
5
|
+
* DISTANCE in cartridge SRAM. The GBA signature on show is the console's
|
|
6
|
+
* "Mode-7" trick: the ROAD is an AFFINE BACKGROUND (BG2, Mode 1) that the PPU
|
|
7
|
+
* rotates and scales per frame for free — so the road SCROLLS toward you
|
|
8
|
+
* (recedes), SCALES with your speed (faster = the road rushes up bigger), and
|
|
9
|
+
* BANKS as you steer (the whole strip tilts into the turn). That receding,
|
|
10
|
+
* banking affine road is the natural GBA racer showcase — the handheld cousin
|
|
11
|
+
* of the SNES Mode-7 racer, done with GBA affine BG hardware.
|
|
7
12
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
+
* THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
|
|
14
|
+
* very different one. The markers tell you what's what:
|
|
15
|
+
* HARDWARE IDIOM (load-bearing) — dodges a documented GBA footgun; reshape
|
|
16
|
+
* your gameplay around it (see TROUBLESHOOTING before changing).
|
|
17
|
+
* GAME LOGIC (clay) — road art, traffic, speeds, tuning: reshape freely.
|
|
18
|
+
*
|
|
19
|
+
* What depends on what:
|
|
20
|
+
* gba_sfx.{h,c} — PSG sound: sfx_tone/sfx_noise one-shots + the music loop
|
|
21
|
+
* (sfx_music_tick once per frame — forget it and the game is silent).
|
|
22
|
+
* libtonc (the build links it) — VBlankIntrWait/key_poll/TTE/lu_sin/lu_cos/
|
|
23
|
+
* tonccpy and the affine-BG matrix registers (REG_BG2PA..PD, BG2X/Y).
|
|
24
|
+
*
|
|
25
|
+
* HANDHELD, SO SINGLE-PLAYER ONLY (honest note): 2P versus on the GBA means a
|
|
26
|
+
* link cable between two units — a second emulator instance this environment
|
|
27
|
+
* can't provide. So VERGE PILOT is a 1P ENDLESS racer chasing your own best
|
|
28
|
+
* distance, not split-screen versus. (Contrast the NES/Genesis racing
|
|
29
|
+
* templates, which ARE 2P versus — two controllers on one machine.)
|
|
30
|
+
*
|
|
31
|
+
* THE AFFINE CHOICE (read before reshaping): a FULL per-scanline perspective
|
|
32
|
+
* floor (a true Mode-7 "ground plane" where each screen row samples the road
|
|
33
|
+
* at a different scale) needs an HBlank-IRQ table that rewrites the matrix 160
|
|
34
|
+
* times a frame — powerful but heavy, and a distraction in a starter. VERGE
|
|
35
|
+
* PILOT takes the HONEST, lighter showcase: ONE affine matrix per frame that
|
|
36
|
+
* (1) scrolls the road texture downward (it recedes toward the horizon as you
|
|
37
|
+
* drive), (2) scales it with your speed, and (3) rotates/banks it as you
|
|
38
|
+
* steer. The matrix is provably non-identity and the scale+bank are visible on
|
|
39
|
+
* screen — the affine hardware is genuinely doing the work. Want the full
|
|
40
|
+
* floor later? Drive road_apply() from an HBlank handler with a per-row scale.
|
|
13
41
|
*/
|
|
14
42
|
|
|
15
43
|
#include <tonc.h>
|
|
16
44
|
#include "gba_sfx.h"
|
|
17
45
|
|
|
18
|
-
/*
|
|
19
|
-
|
|
46
|
+
/* The title screen renders this — examples({op:'fork'}) stamps your game's
|
|
47
|
+
* name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
|
|
48
|
+
#define GAME_TITLE "VERGE PILOT"
|
|
49
|
+
|
|
50
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
51
|
+
* Road / car geometry + race tuning. The road is an affine BG2 strip; the
|
|
52
|
+
* player car and traffic are sprites that live in SCREEN space over it. */
|
|
53
|
+
#define SCREEN_W 240
|
|
54
|
+
#define SCREEN_H 160
|
|
55
|
+
#define ROAD_X0 56 /* left edge of the asphalt (screen px) */
|
|
56
|
+
#define ROAD_X1 184 /* right edge of the asphalt */
|
|
57
|
+
#define CAR_Y 128 /* player car's fixed screen Y (near bottom) */
|
|
58
|
+
#define CAR_SLOTS 5 /* discrete lanes the car steers between */
|
|
59
|
+
#define MAX_TRAFFIC 4 /* obstacle cars on the road at once */
|
|
60
|
+
#define SPEED_MIN 1 /* road px/frame */
|
|
61
|
+
#define SPEED_MAX 6
|
|
62
|
+
#define START_SPEED 2
|
|
63
|
+
|
|
64
|
+
/* Sprite slot discipline (128 OAM entries; we use 5):
|
|
65
|
+
* 0 → player car
|
|
66
|
+
* 1..4 → traffic */
|
|
67
|
+
#define SLOT_CAR 0
|
|
68
|
+
#define SLOT_TRAFFIC 1
|
|
69
|
+
|
|
70
|
+
#define TILE_CAR 1 /* OBJ tile 1 = player car (4bpp 8x8) */
|
|
71
|
+
#define TILE_RIVAL 2 /* OBJ tile 2 = oncoming/rival car */
|
|
72
|
+
|
|
73
|
+
/* 4bpp sprite tiles (8 rows × 32 bits; each nibble is a palette index within
|
|
74
|
+
* the sprite's palbank. Index 0 = transparent). */
|
|
75
|
+
static const u32 tile_car[8] = { /* your car, nose up, bright cockpit */
|
|
76
|
+
0x00133100, 0x01333310, 0x13322331, 0x13333331,
|
|
77
|
+
0x13333331, 0x13311331, 0x13000031, 0x01000010,
|
|
78
|
+
};
|
|
79
|
+
static const u32 tile_rival[8] = { /* traffic, tail up (you overtake it) */
|
|
80
|
+
0x01000010, 0x13000031, 0x13311331, 0x13333331,
|
|
81
|
+
0x13333331, 0x13322331, 0x01333310, 0x00133100,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/* ── GAME LOGIC (clay — reshape freely) — game state (plain BSS).
|
|
85
|
+
* NOTE for headless verification: unlike the Genesis template (whose work-RAM
|
|
86
|
+
* globals are readable by symbol name), the GBA libretro core exposes NO
|
|
87
|
+
* IWRAM/EWRAM region, so a headless agent reads game state from what's ON
|
|
88
|
+
* HARDWARE — OAM (the cars), the BG2 affine matrix registers (the road), TTE
|
|
89
|
+
* ink pixels (the screen/HUD), and save_ram (the record). Keep game globals
|
|
90
|
+
* static and surface anything the harness must read onto hardware. */
|
|
91
|
+
#define ST_TITLE 0
|
|
92
|
+
#define ST_PLAY 1
|
|
93
|
+
#define ST_OVER 2
|
|
94
|
+
static u8 state;
|
|
95
|
+
|
|
96
|
+
static s16 car_slot; /* 0..CAR_SLOTS-1, the player's lane */
|
|
97
|
+
static s16 car_x; /* eased screen-x of the car (smooth steer) */
|
|
98
|
+
static u8 speed; /* road px/frame, SPEED_MIN..SPEED_MAX */
|
|
99
|
+
static u8 lives; /* crashes left */
|
|
100
|
+
static u8 invuln; /* post-crash blink / no-collide frames */
|
|
101
|
+
static u16 dist; /* distance travelled (the score) */
|
|
102
|
+
static u16 dist_sub; /* subcounter: 16 scrolled px = +1 distance */
|
|
103
|
+
static u16 best; /* battery-backed best distance — SRAM idiom */
|
|
104
|
+
static u8 new_best; /* result screen shows NEW BEST */
|
|
105
|
+
static u16 road_scroll; /* BG2 texture Y offset (drives the recede) */
|
|
106
|
+
static s16 bank; /* current road bank angle bias (steer lean) */
|
|
107
|
+
|
|
108
|
+
/* Traffic pool — fixed slots, no allocation. Each car has a lane + a SCREEN y
|
|
109
|
+
* that grows downward (it approaches from the top of the road and slides past
|
|
110
|
+
* the player), and an `alive` flag. */
|
|
111
|
+
static u8 tr_alive[MAX_TRAFFIC];
|
|
112
|
+
static u8 tr_slot[MAX_TRAFFIC];
|
|
113
|
+
static s16 tr_y[MAX_TRAFFIC];
|
|
114
|
+
static u8 spawn_timer;
|
|
115
|
+
|
|
116
|
+
#define START_LIVES 3
|
|
117
|
+
|
|
118
|
+
/* ── GAME LOGIC (clay) — xorshift16 PRNG. The GBA is deterministic; without a
|
|
119
|
+
* noise source the traffic would spawn in a fixed lockstep pattern that an
|
|
120
|
+
* idle run could memorise. The PRNG scatters spawn lanes/timing so each run is
|
|
121
|
+
* fresh. ── */
|
|
122
|
+
static u16 rng = 0xC0A7;
|
|
123
|
+
static u8 random8(void) {
|
|
124
|
+
u16 r = rng;
|
|
125
|
+
r ^= r << 7;
|
|
126
|
+
r ^= r >> 9;
|
|
127
|
+
r ^= r << 8;
|
|
128
|
+
rng = r;
|
|
129
|
+
return (u8)r;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
133
|
+
* PERSISTENT SRAM at 0x0E000000. Two footguns, both fatal-but-silent:
|
|
134
|
+
* 1. The SRAM bus is 8 BITS WIDE. Byte reads/writes only — a u16/u32
|
|
135
|
+
* access doesn't fault, it just reads the same byte mirrored (and a
|
|
136
|
+
* wide write stores one byte), so your data "almost" round-trips and
|
|
137
|
+
* then the checksum never matches. Every access below is via vu8.
|
|
138
|
+
* 2. Emulators and flashcarts detect the SAVE TYPE by scanning the ROM
|
|
139
|
+
* image for a marker string. Without "SRAM_V" in the ROM, mGBA gives
|
|
140
|
+
* the cart NO save memory at all and writes to 0x0E000000 vanish.
|
|
141
|
+
* The aligned, (used)-attributed const below plants that marker —
|
|
142
|
+
* delete it and persistence dies even though this code is untouched.
|
|
143
|
+
* Layout: 'V' 'X' best-lo best-hi checksum (xor ^ 0xA5) — magic+checksum so a
|
|
144
|
+
* fresh (0xFF-filled) cart reads as "no record" instead of garbage.
|
|
145
|
+
* PERSISTENCE CHOICE: an endless racer's natural chase-stat is the longest
|
|
146
|
+
* DISTANCE survived — exactly what a returning player tries to beat.
|
|
147
|
+
* requires: nothing else — self-contained; safe to transplant whole. */
|
|
148
|
+
#define SRAM_BYTE ((volatile u8 *)0x0E000000)
|
|
149
|
+
__attribute__((used, aligned(4))) static const char sram_type_marker[] = "SRAM_V113";
|
|
150
|
+
|
|
151
|
+
static u16 best_load(void) {
|
|
152
|
+
u8 lo, hi;
|
|
153
|
+
if (SRAM_BYTE[0] != 'V' || SRAM_BYTE[1] != 'X') return 0;
|
|
154
|
+
lo = SRAM_BYTE[2];
|
|
155
|
+
hi = SRAM_BYTE[3];
|
|
156
|
+
if (SRAM_BYTE[4] != (u8)(lo ^ hi ^ 0xA5)) return 0;
|
|
157
|
+
return (u16)(lo | (hi << 8));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
static void best_save(u16 v) {
|
|
161
|
+
SRAM_BYTE[0] = 'V';
|
|
162
|
+
SRAM_BYTE[1] = 'X';
|
|
163
|
+
SRAM_BYTE[2] = (u8)v;
|
|
164
|
+
SRAM_BYTE[3] = (u8)(v >> 8);
|
|
165
|
+
SRAM_BYTE[4] = (u8)((u8)v ^ (u8)(v >> 8) ^ 0xA5);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* ── GAME LOGIC (clay) — TTE text helpers ────────────────────────────────────
|
|
169
|
+
* Draw right-aligned decimal digits at pixel (x,y) WITHOUT tte_printf. The
|
|
170
|
+
* bundled libtonc's tte_printf with a %d conversion is broken (it routes
|
|
171
|
+
* through a vsnprintf path that isn't wired in this build — it garbles output
|
|
172
|
+
* AND wedges the loop when called per-frame, GBA-1). We build the string
|
|
173
|
+
* ourselves and use tte_write, which processes the #{P:x,y} position command
|
|
174
|
+
* but does NO format conversion → safe every frame. */
|
|
175
|
+
static void draw_num(int x, int y, unsigned v, int digits) {
|
|
20
176
|
char buf[24];
|
|
21
177
|
int i, n = 0;
|
|
22
|
-
buf[n++]='#'; buf[n++]='{'; buf[n++]='P'; buf[n++]=':';
|
|
23
|
-
if (x >= 100) buf[n++] = '0' + (x/100)%10;
|
|
24
|
-
if (x >= 10) buf[n++] = '0' + (x/10)%10;
|
|
25
|
-
buf[n++] = '0' + x%10;
|
|
26
|
-
buf[n++]
|
|
27
|
-
|
|
28
|
-
|
|
178
|
+
buf[n++] = '#'; buf[n++] = '{'; buf[n++] = 'P'; buf[n++] = ':';
|
|
179
|
+
if (x >= 100) buf[n++] = (char)('0' + (x / 100) % 10);
|
|
180
|
+
if (x >= 10) buf[n++] = (char)('0' + (x / 10) % 10);
|
|
181
|
+
buf[n++] = (char)('0' + x % 10);
|
|
182
|
+
buf[n++] = ',';
|
|
183
|
+
if (y >= 100) buf[n++] = (char)('0' + (y / 100) % 10);
|
|
184
|
+
if (y >= 10) buf[n++] = (char)('0' + (y / 10) % 10);
|
|
185
|
+
buf[n++] = (char)('0' + y % 10);
|
|
186
|
+
buf[n++] = '}';
|
|
187
|
+
for (i = digits - 1; i >= 0; i--) { buf[n + i] = (char)('0' + (v % 10)); v /= 10; }
|
|
188
|
+
n += digits; buf[n] = 0;
|
|
29
189
|
tte_write(buf);
|
|
30
190
|
}
|
|
31
191
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
192
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
193
|
+
* AFFINE ROAD (BG2, Mode 1) — the GBA's "Mode 7": one background the PPU
|
|
194
|
+
* rotates/scales per frame for free. THIS is the racer's whole visual hook.
|
|
195
|
+
* Four matrix registers + one reference point do the work:
|
|
196
|
+
*
|
|
197
|
+
* REG_BG2PA..PD — a 2x2 matrix in 8.8 FIXED POINT (256 == 1.0) that maps
|
|
198
|
+
* SCREEN pixels → TEXTURE pixels: tex = P · (screen - origin) + ref.
|
|
199
|
+
* Because it maps screen→texture (the INVERSE of "how is the image
|
|
200
|
+
* transformed"), a matrix that SAMPLES 2 texture-px per screen-px makes
|
|
201
|
+
* the image look HALF size: bigger pa = smaller image. To zoom IN by z
|
|
202
|
+
* write 1/z; to rotate the image one way, write the matrix of the other.
|
|
203
|
+
* REG_BG2X/Y — the texture point sampled at screen pixel (0,0), 20.8 fixed.
|
|
204
|
+
* We ADVANCE BG2Y by the scroll each frame so the road texture slides
|
|
205
|
+
* downward → the road RECEDES toward the horizon as you drive. To pivot
|
|
206
|
+
* the rotation/scale around the screen centre (cx,cy)=(120,120, near the
|
|
207
|
+
* car) anchored at texture point (tx,ty): BG2X = (tx<<8) - (pa*cx+pb*cy)
|
|
208
|
+
* (same shape for Y with pc/pd) — "walk back from the anchor by half a
|
|
209
|
+
* screen through the matrix".
|
|
210
|
+
*
|
|
211
|
+
* The math (libtonc's bg_aff_rotscale does the same). lu_sin/lu_cos take a u16
|
|
212
|
+
* angle (full circle = 0x10000), return 4.12 fixed → >>4 to 8.8. For bank θ
|
|
213
|
+
* and zoom z (8.8):
|
|
214
|
+
* inv = 65536/z (8.8 reciprocal: 1/z)
|
|
215
|
+
* pa = cos·inv>>8 pb = -sin·inv>>8
|
|
216
|
+
* pc = sin·inv>>8 pd = cos·inv>>8
|
|
217
|
+
*
|
|
218
|
+
* Footguns this block already dodges:
|
|
219
|
+
* - These 6 registers are WRITE-ONLY. You cannot read-modify-update; keep
|
|
220
|
+
* your angle/zoom in variables (bank/road_scroll) and rewrite ALL of them
|
|
221
|
+
* every frame.
|
|
222
|
+
* - Affine BGs are ALWAYS 8bpp, and the map is 1 BYTE per tile (no flip
|
|
223
|
+
* bits, no palbank — plain tile index), unlike regular BGs' u16 entries.
|
|
224
|
+
* - VRAM IGNORES BYTE WRITES (a u8 store writes the byte TWICE into the
|
|
225
|
+
* 16-bit lane). Build tiles/map in a work-RAM buffer and tonccpy() it —
|
|
226
|
+
* tonccpy is VRAM-safe.
|
|
227
|
+
* - BG_WRAP makes the 256x256 texture tile forever; without it the road
|
|
228
|
+
* runs out and everything past the edge renders as tile 0.
|
|
229
|
+
* requires: DCNT_MODE1 (BG2 affine there), BG2CNT → CBB 1 / SBB 26,
|
|
230
|
+
* road_apply() called every frame, BG palette indices 224..230 (bank 14 —
|
|
231
|
+
* bank 15 belongs to TTE; see the palette footgun at road_build). */
|
|
232
|
+
static void road_apply(u16 theta, u32 zoom_q8, u16 scroll_y) {
|
|
233
|
+
s32 inv = (s32)(65536u / zoom_q8); /* 8.8 ── 1/zoom */
|
|
234
|
+
s32 cc = ((lu_cos(theta) >> 4) * inv) >> 8; /* 8.8 ── cosθ/zoom */
|
|
235
|
+
s32 ss = ((lu_sin(theta) >> 4) * inv) >> 8; /* 8.8 ── sinθ/zoom */
|
|
236
|
+
REG_BG2PA = (s16)cc; REG_BG2PB = (s16)-ss;
|
|
237
|
+
REG_BG2PC = (s16)ss; REG_BG2PD = (s16)cc;
|
|
238
|
+
/* Pivot around (120,120) — under the car — anchored at texture (128, ty).
|
|
239
|
+
* ty advances with scroll_y so the road slides toward the player. */
|
|
240
|
+
{
|
|
241
|
+
s32 ty = (s32)scroll_y;
|
|
242
|
+
REG_BG2X = (128 << 8) - (cc * 120 + (-ss) * 120);
|
|
243
|
+
REG_BG2Y = ((128 + ty) << 8) - (ss * 120 + cc * 120);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
37
246
|
|
|
38
|
-
|
|
39
|
-
|
|
247
|
+
/* ── GAME LOGIC (clay) — the road ART (the idiom above is the machinery; this
|
|
248
|
+
* is just what the texture looks like — replace at will).
|
|
249
|
+
* 8bpp tiles are 64 bytes, 1 byte per pixel, row-major. We stage 6 tiles + the
|
|
250
|
+
* 32x32 one-byte-per-entry map in work RAM, then tonccpy to VRAM (CBB 1 tiles,
|
|
251
|
+
* SBB 26 map) per the byte-write footgun above. The texture needs VERTICAL
|
|
252
|
+
* content (lane dashes that run up the strip, a centre line) so the scroll
|
|
253
|
+
* reads as forward motion, plus LATERAL structure (shoulders, grass) so the
|
|
254
|
+
* bank-rotation reads as a tilt.
|
|
255
|
+
* PALETTE FOOTGUN: an 8bpp BG indexes the FULL 256-color BG palette, and
|
|
256
|
+
* tte_init_chr4c_default OWNS BANK 15 (240-255: ink 241 = yellow). Park 8bpp
|
|
257
|
+
* road colours in bank 14 (224..) or the road turns ink-yellow the moment TTE
|
|
258
|
+
* initialises. */
|
|
259
|
+
#define RC 224 /* road colours live at 224..230 — clear of TTE's bank 15 */
|
|
260
|
+
#define T_GRASS 0
|
|
261
|
+
#define T_SHOULDER 1
|
|
262
|
+
#define T_ASPHALT 2
|
|
263
|
+
#define T_DASH 3
|
|
264
|
+
#define T_CENTER 4
|
|
265
|
+
#define T_SPECK 5
|
|
266
|
+
static void road_build(void) {
|
|
267
|
+
static u8 tiles[6][64];
|
|
268
|
+
static u8 rmap[1024];
|
|
269
|
+
int x, y, tx, ty;
|
|
40
270
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
};
|
|
271
|
+
pal_bg_mem[RC + 0] = RGB15(3, 14, 5); /* grass green (vivid) */
|
|
272
|
+
pal_bg_mem[RC + 1] = RGB15(6, 20, 8); /* grass highlight */
|
|
273
|
+
pal_bg_mem[RC + 2] = RGB15(7, 7, 9); /* asphalt dark */
|
|
274
|
+
pal_bg_mem[RC + 3] = RGB15(11, 11, 13); /* asphalt light (dither) */
|
|
275
|
+
pal_bg_mem[RC + 4] = RGB15(31, 31, 22); /* lane dash (warm white) */
|
|
276
|
+
pal_bg_mem[RC + 5] = RGB15(31, 18, 4); /* centre line (hot amber) */
|
|
277
|
+
pal_bg_mem[RC + 6] = RGB15(28, 30, 31); /* shoulder (near-white) */
|
|
49
278
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
static const u32 tile_road[8] = {
|
|
61
|
-
0x77777777, 0x77777777, 0x77777777, 0x77777777,
|
|
62
|
-
0x77777777, 0x77777777, 0x77777777, 0x77777777,
|
|
63
|
-
};
|
|
64
|
-
static const u32 tile_road_dash[8] = {
|
|
65
|
-
0x77877877, 0x77877877, 0x77877877, 0x77877877,
|
|
66
|
-
0x77777777, 0x77777777, 0x77777777, 0x77777777,
|
|
67
|
-
};
|
|
68
|
-
static const u32 tile_road_edge[8] = {
|
|
69
|
-
0x87777778, 0x87777778, 0x87777778, 0x87777778,
|
|
70
|
-
0x87777778, 0x87777778, 0x87777778, 0x87777778,
|
|
71
|
-
};
|
|
279
|
+
for (y = 0; y < 8; y++)
|
|
280
|
+
for (x = 0; x < 8; x++) {
|
|
281
|
+
tiles[T_GRASS][y * 8 + x] = (u8)(((x + y) & 3) ? RC : RC + 1);
|
|
282
|
+
tiles[T_SHOULDER][y * 8 + x] = (u8)(RC + 6);
|
|
283
|
+
tiles[T_ASPHALT][y * 8 + x] = (u8)(((x * 3 + y * 5) % 7) ? RC + 2 : RC + 3);
|
|
284
|
+
/* dash: a fat vertical stripe, dashed every other tile-row band */
|
|
285
|
+
tiles[T_DASH][y * 8 + x] = (u8)((x >= 3 && x <= 4 && y < 5) ? RC + 4 : RC + 2);
|
|
286
|
+
tiles[T_CENTER][y * 8 + x] = (u8)((x >= 3 && x <= 4) ? RC + 5 : RC + 2);
|
|
287
|
+
tiles[T_SPECK][y * 8 + x] = (u8)(((x == 2 && y == 5) || (x == 6 && y == 2)) ? RC + 3 : RC + 2);
|
|
288
|
+
}
|
|
72
289
|
|
|
73
|
-
|
|
290
|
+
/* Map: a 32x32 (256x256) road texture. A central asphalt band (cols 9..22)
|
|
291
|
+
* with grass shoulders outside, a hot centre line down the middle, and
|
|
292
|
+
* dashed lane lines either side of centre — VERTICAL content so the
|
|
293
|
+
* downward scroll reads as forward motion, lateral content so the bank
|
|
294
|
+
* rotation reads as a tilt. WRAP makes it an endless road. */
|
|
295
|
+
for (ty = 0; ty < 32; ty++)
|
|
296
|
+
for (tx = 0; tx < 32; tx++) {
|
|
297
|
+
u8 t;
|
|
298
|
+
if (tx < 9 || tx > 22) t = T_GRASS; /* grass shoulders */
|
|
299
|
+
else if (tx == 9 || tx == 22) t = T_SHOULDER; /* white shoulders */
|
|
300
|
+
else if (tx == 15 || tx == 16) t = T_CENTER; /* centre line */
|
|
301
|
+
else if (tx == 12 || tx == 19) t = (ty & 1) ? T_DASH : T_ASPHALT; /* dashes */
|
|
302
|
+
else t = ((tx * 5 + ty * 3) % 13 == 0) ? T_SPECK : T_ASPHALT;
|
|
303
|
+
rmap[ty * 32 + tx] = t;
|
|
304
|
+
}
|
|
74
305
|
|
|
75
|
-
|
|
306
|
+
tonccpy(&tile8_mem[1][0], tiles, sizeof(tiles)); /* tiles → charblock 1 */
|
|
307
|
+
tonccpy(se_mem[26], rmap, sizeof(rmap)); /* map → screenblock 26 */
|
|
308
|
+
REG_BG2CNT = BG_CBB(1) | BG_SBB(26) | BG_AFF_32x32 | BG_WRAP | BG_PRIO(3);
|
|
309
|
+
}
|
|
76
310
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
static
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
static u16 prev_keys;
|
|
83
|
-
static u8 player_lane;
|
|
311
|
+
/* Map a discrete lane slot to a target screen-x for the player car. The car
|
|
312
|
+
* eases toward this so steering feels like a turn, not a teleport. */
|
|
313
|
+
static s16 slot_x(s16 slot) {
|
|
314
|
+
return (s16)(ROAD_X0 + 8 + slot * ((ROAD_X1 - ROAD_X0 - 16) / (CAR_SLOTS - 1)));
|
|
315
|
+
}
|
|
84
316
|
|
|
85
|
-
|
|
317
|
+
/* ── GAME LOGIC (clay) — HUD / screens (TTE on BG1, priority 0) ── */
|
|
318
|
+
static void draw_hud_labels(void) {
|
|
319
|
+
tte_erase_screen();
|
|
320
|
+
tte_write("#{P:8,4}DIST");
|
|
321
|
+
tte_write("#{P:150,4}LIFE");
|
|
322
|
+
}
|
|
86
323
|
|
|
87
|
-
static
|
|
88
|
-
|
|
89
|
-
|
|
324
|
+
static void draw_hud_numbers(void) {
|
|
325
|
+
tte_erase_rect(48, 4, 100, 12); draw_num(48, 4, dist, 5);
|
|
326
|
+
tte_erase_rect(196, 4, 210, 12); draw_num(196, 4, lives, 1);
|
|
90
327
|
}
|
|
91
328
|
|
|
92
|
-
static void
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
329
|
+
static void enter_title(void) {
|
|
330
|
+
state = ST_TITLE;
|
|
331
|
+
tte_erase_screen();
|
|
332
|
+
tte_write("#{P:64,40}" GAME_TITLE);
|
|
333
|
+
tte_write("#{P:76,72}PRESS START");
|
|
334
|
+
tte_write("#{P:84,92}BEST");
|
|
335
|
+
draw_num(128, 92, best, 5);
|
|
336
|
+
tte_write("#{P:20,116}LEFT RIGHT STEER - A B SPEED");
|
|
337
|
+
tte_write("#{P:44,128}1P ENDLESS - NO LINK 2P");
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
static void enter_play(void) {
|
|
341
|
+
int i;
|
|
342
|
+
state = ST_PLAY;
|
|
343
|
+
car_slot = CAR_SLOTS / 2;
|
|
344
|
+
car_x = slot_x(car_slot);
|
|
345
|
+
speed = START_SPEED;
|
|
346
|
+
lives = START_LIVES;
|
|
347
|
+
invuln = 0;
|
|
348
|
+
dist = 0; dist_sub = 0;
|
|
349
|
+
new_best = 0;
|
|
350
|
+
road_scroll = 0;
|
|
351
|
+
bank = 0;
|
|
352
|
+
for (i = 0; i < MAX_TRAFFIC; i++) tr_alive[i] = 0;
|
|
99
353
|
spawn_timer = 0;
|
|
100
|
-
|
|
354
|
+
/* Stir the PRNG with time-on-title so each run differs. */
|
|
355
|
+
rng ^= (u16)REG_VCOUNT ^ ((u16)REG_VCOUNT << 7);
|
|
356
|
+
if (rng == 0) rng = 0xC0A7;
|
|
357
|
+
draw_hud_labels();
|
|
358
|
+
draw_hud_numbers();
|
|
101
359
|
}
|
|
102
360
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
361
|
+
static void enter_over(void) {
|
|
362
|
+
state = ST_OVER;
|
|
363
|
+
if (dist > best) {
|
|
364
|
+
best = dist;
|
|
365
|
+
new_best = 1;
|
|
366
|
+
best_save(best); /* byte-wise SRAM write — see SRAM idiom */
|
|
367
|
+
}
|
|
368
|
+
tte_write("#{P:84,56}WRECKED");
|
|
369
|
+
tte_write("#{P:84,72}DIST");
|
|
370
|
+
draw_num(140, 72, dist, 5);
|
|
371
|
+
if (new_best) tte_write("#{P:72,88}NEW BEST");
|
|
372
|
+
tte_write("#{P:76,108}PRESS START");
|
|
373
|
+
sfx_tone(1, 1100, 12);
|
|
374
|
+
sfx_tone(2, 900, 14);
|
|
113
375
|
}
|
|
114
376
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
377
|
+
/* ── GAME LOGIC (clay) — spawn traffic into a free slot ── */
|
|
378
|
+
static void spawn_traffic(void) {
|
|
379
|
+
int i;
|
|
380
|
+
for (i = 0; i < MAX_TRAFFIC; i++)
|
|
381
|
+
if (!tr_alive[i]) {
|
|
382
|
+
tr_alive[i] = 1;
|
|
383
|
+
tr_slot[i] = random8() % CAR_SLOTS;
|
|
384
|
+
tr_y[i] = -8; /* enters from the top of the road */
|
|
121
385
|
return;
|
|
122
386
|
}
|
|
123
|
-
}
|
|
124
387
|
}
|
|
125
388
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
389
|
+
/* AABB, both boxes ~12 px (cars are 8px sprites; a slightly fat box makes the
|
|
390
|
+
* crash feel fair at speed). */
|
|
391
|
+
static u8 hits(s16 ax, s16 ay, s16 bx, s16 by) {
|
|
392
|
+
s16 dx = (s16)(ax > bx ? ax - bx : bx - ax);
|
|
393
|
+
s16 dy = (s16)(ay > by ? ay - by : by - ay);
|
|
394
|
+
return (u8)(dx < 12 && dy < 12);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
static void crash(void) {
|
|
398
|
+
sfx_noise(14);
|
|
399
|
+
invuln = 60;
|
|
400
|
+
if (speed > SPEED_MIN) speed--; /* a wreck kills your momentum */
|
|
401
|
+
if (lives > 0) lives--;
|
|
402
|
+
draw_hud_numbers();
|
|
403
|
+
if (lives == 0) enter_over();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/* ── GAME LOGIC (clay) — one ST_PLAY tick. The road is the affine BG2; the car
|
|
407
|
+
* and traffic are sprites over it. Edge cases handled: the car eases toward its
|
|
408
|
+
* lane (no teleport); a just-crashed car blinks and can't collide for 60
|
|
409
|
+
* frames; traffic flows down at road speed and despawns past the bottom. ── */
|
|
410
|
+
static void update_play(void) {
|
|
411
|
+
int i;
|
|
412
|
+
s16 target_x;
|
|
413
|
+
|
|
414
|
+
random8(); /* tick the noise source every frame */
|
|
415
|
+
|
|
416
|
+
/* Steer: LEFT/RIGHT move between discrete lanes (edge-triggered so a held
|
|
417
|
+
* d-pad doesn't machine-gun across the road). The road BANKS toward the
|
|
418
|
+
* turn — bank eases back to 0 when you're straight. */
|
|
419
|
+
if (key_hit(KEY_LEFT) && car_slot > 0) { car_slot--; sfx_tone(1, 1400, 3); }
|
|
420
|
+
if (key_hit(KEY_RIGHT) && car_slot < CAR_SLOTS - 1) { car_slot++; sfx_tone(1, 1400, 3); }
|
|
421
|
+
target_x = slot_x(car_slot);
|
|
422
|
+
if (car_x < target_x) { car_x += 3; if (car_x > target_x) car_x = target_x; }
|
|
423
|
+
if (car_x > target_x) { car_x -= 3; if (car_x < target_x) car_x = target_x; }
|
|
424
|
+
/* bank = how far the car is from screen centre → tilt the whole road */
|
|
156
425
|
{
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
426
|
+
s16 want = (s16)((car_x - 120) * 6); /* ±~0x0180-ish lean */
|
|
427
|
+
if (bank < want) bank += 24;
|
|
428
|
+
if (bank > want) bank -= 24;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/* Throttle: A/UP faster, B/DOWN slower. */
|
|
432
|
+
if (key_hit(KEY_A | KEY_UP) && speed < SPEED_MAX) { speed++; sfx_tone(2, (u16)(900 + speed * 90), 4); }
|
|
433
|
+
if (key_hit(KEY_B | KEY_DOWN) && speed > SPEED_MIN) { speed--; sfx_tone(2, 1500, 3); }
|
|
434
|
+
|
|
435
|
+
/* Recede: advance the road texture downward by `speed`. */
|
|
436
|
+
road_scroll = (u16)(road_scroll + speed);
|
|
437
|
+
|
|
438
|
+
/* Distance (the score): 1 unit per 16 scrolled px. A chime every 256. */
|
|
439
|
+
dist_sub = (u16)(dist_sub + speed);
|
|
440
|
+
if (dist_sub >= 16) {
|
|
441
|
+
dist_sub -= 16;
|
|
442
|
+
if (dist < 65000u) dist++;
|
|
443
|
+
if (dist != 0 && (dist & 0xFF) == 0) sfx_tone(1, 1800, 8); /* checkpoint */
|
|
444
|
+
draw_hud_numbers();
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/* Traffic flows down at road speed (reads as cars you overtake). */
|
|
448
|
+
for (i = 0; i < MAX_TRAFFIC; i++) {
|
|
449
|
+
if (!tr_alive[i]) continue;
|
|
450
|
+
tr_y[i] = (s16)(tr_y[i] + speed + 1); /* a touch faster than scroll */
|
|
451
|
+
if (tr_y[i] > SCREEN_H) { tr_alive[i] = 0; sfx_tone(2, 700, 2); }
|
|
452
|
+
}
|
|
453
|
+
if (++spawn_timer >= (u8)(28 - speed * 2)) { /* denser at higher speed */
|
|
454
|
+
spawn_timer = 0;
|
|
455
|
+
spawn_traffic();
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/* Crash check: traffic ↔ player. Grace window after a crash. */
|
|
459
|
+
if (invuln > 0) invuln--;
|
|
460
|
+
if (!invuln) {
|
|
461
|
+
for (i = 0; i < MAX_TRAFFIC; i++) {
|
|
462
|
+
if (!tr_alive[i]) continue;
|
|
463
|
+
if (hits((s16)(slot_x(tr_slot[i]) + 4), (s16)(tr_y[i] + 4), (s16)(car_x + 4), CAR_Y + 4)) {
|
|
464
|
+
tr_alive[i] = 0;
|
|
465
|
+
crash();
|
|
466
|
+
if (state != ST_PLAY) return;
|
|
171
467
|
}
|
|
172
468
|
}
|
|
173
469
|
}
|
|
470
|
+
}
|
|
174
471
|
|
|
175
|
-
|
|
472
|
+
/* ── GAME LOGIC (clay) — stage the sprites: player car + traffic. Off-screen /
|
|
473
|
+
* inactive slots park at y=200. ── */
|
|
474
|
+
static OBJ_ATTR obj_buffer[128];
|
|
475
|
+
static void stage_sprites(void) {
|
|
476
|
+
int i;
|
|
477
|
+
int playing = (state == ST_PLAY);
|
|
478
|
+
/* player car (blinks during the post-crash grace) */
|
|
479
|
+
obj_set_attr(&obj_buffer[SLOT_CAR], ATTR0_SQUARE, ATTR1_SIZE_8,
|
|
480
|
+
(u16)(ATTR2_PALBANK(0) | TILE_CAR));
|
|
481
|
+
{
|
|
482
|
+
int hide = !playing || (invuln && (invuln & 2));
|
|
483
|
+
obj_set_pos(&obj_buffer[SLOT_CAR], hide ? 250 : car_x, hide ? 200 : CAR_Y);
|
|
484
|
+
}
|
|
485
|
+
for (i = 0; i < MAX_TRAFFIC; i++) {
|
|
486
|
+
int on = playing && tr_alive[i];
|
|
487
|
+
obj_set_attr(&obj_buffer[SLOT_TRAFFIC + i], ATTR0_SQUARE, ATTR1_SIZE_8,
|
|
488
|
+
(u16)(ATTR2_PALBANK(1) | TILE_RIVAL));
|
|
489
|
+
obj_set_pos(&obj_buffer[SLOT_TRAFFIC + i],
|
|
490
|
+
on ? slot_x(tr_slot[i]) : 250, on ? tr_y[i] : 200);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
int main(void) {
|
|
495
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
496
|
+
* Init order: tiles/palettes → oam_init → irq_init + II_VBLANK → TTE init
|
|
497
|
+
* → DISPCNT last. VBlankIntrWait() HANGS FOREVER without the vblank IRQ
|
|
498
|
+
* registered (the #1 "frozen on frame 1" cause), and enabling DISPCNT
|
|
499
|
+
* layers before their tiles/maps exist flashes garbage. Mode 1 = BG0/BG1
|
|
500
|
+
* regular, BG2 AFFINE. TTE owns BG1 (CBB 2 / SBB 30) — keep other layers
|
|
501
|
+
* off those blocks.
|
|
502
|
+
* requires: nothing prior; this IS the boot. */
|
|
503
|
+
|
|
504
|
+
/* Sprite tiles → OBJ char base (tile_mem[4]). */
|
|
505
|
+
tonccpy(&tile_mem[4][TILE_CAR], tile_car, sizeof(tile_car));
|
|
506
|
+
tonccpy(&tile_mem[4][TILE_RIVAL], tile_rival, sizeof(tile_rival));
|
|
507
|
+
|
|
508
|
+
/* OBJ palettes: bank 0 = your car (cyan body, hot cockpit), bank 1 = rival
|
|
509
|
+
* (red). The GBA's 15-bit RGB gives saturated colours the GB/NES can only
|
|
510
|
+
* hint at. */
|
|
511
|
+
pal_obj_bank[0][1] = RGB15(6, 20, 31); /* your car body cyan */
|
|
512
|
+
pal_obj_bank[0][2] = RGB15(31, 31, 24); /* cockpit glint near-white */
|
|
513
|
+
pal_obj_bank[0][3] = RGB15(4, 10, 22); /* car outline deep blue */
|
|
514
|
+
pal_obj_bank[1][1] = RGB15(31, 7, 7); /* rival body red */
|
|
515
|
+
pal_obj_bank[1][2] = RGB15(31, 24, 10); /* rival windshield amber */
|
|
516
|
+
pal_obj_bank[1][3] = RGB15(16, 2, 2); /* rival outline maroon */
|
|
517
|
+
|
|
518
|
+
road_build(); /* affine BG2: tiles + map + BG2CNT */
|
|
519
|
+
|
|
520
|
+
oam_init(obj_buffer, 128); /* hides all 128, matrices = identity */
|
|
176
521
|
|
|
177
|
-
/* IRQ setup — required for VBlankIntrWait() to function. */
|
|
178
522
|
irq_init(NULL);
|
|
179
523
|
irq_add(II_VBLANK, NULL);
|
|
180
524
|
|
|
181
|
-
sfx_init();
|
|
525
|
+
sfx_init(); /* APU on; music loop ticks below */
|
|
182
526
|
|
|
183
|
-
/* TTE
|
|
184
|
-
*
|
|
527
|
+
/* TTE text on BG1 (4bpp char block 2, screenblock 30), priority 0 so text
|
|
528
|
+
* draws over everything. Mode 1 = BG0/BG1 regular, BG2 affine. */
|
|
185
529
|
tte_init_chr4c_default(1, BG_CBB(2) | BG_SBB(30));
|
|
186
530
|
REG_BG1CNT |= BG_PRIO(0);
|
|
187
|
-
REG_DISPCNT =
|
|
188
|
-
tte_write("#{P:160,8}SCORE 00000");
|
|
189
|
-
tte_write("#{P:64,150}L/R MOVES LANE");
|
|
531
|
+
REG_DISPCNT = DCNT_MODE1 | DCNT_BG1 | DCNT_BG2 | DCNT_OBJ | DCNT_OBJ_1D;
|
|
190
532
|
|
|
191
|
-
|
|
192
|
-
|
|
533
|
+
best = best_load(); /* cartridge SRAM — 0 on first boot */
|
|
534
|
+
enter_title();
|
|
193
535
|
|
|
194
536
|
while (1) {
|
|
537
|
+
/* Idiomatic Tonc heartbeat: wait vblank, poll keys, update, then commit
|
|
538
|
+
* OAM + the affine matrix while still inside vblank (the update is far
|
|
539
|
+
* quicker than the 4.9ms vblank window). */
|
|
195
540
|
VBlankIntrWait();
|
|
196
541
|
key_poll();
|
|
542
|
+
sfx_music_tick(); /* forget this → silent game */
|
|
197
543
|
|
|
198
|
-
if (
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
203
|
-
if (game_over_timer == 0) {
|
|
204
|
-
tte_erase_rect(72, 80, 240, 96);
|
|
205
|
-
reset_run();
|
|
206
|
-
}
|
|
544
|
+
if (state == ST_TITLE) {
|
|
545
|
+
if (key_hit(KEY_START | KEY_A)) enter_play();
|
|
546
|
+
} else if (state == ST_OVER) {
|
|
547
|
+
if (key_hit(KEY_START)) enter_title();
|
|
207
548
|
} else {
|
|
208
|
-
|
|
209
|
-
if ((now & KEY_LEFT) && !(prev_keys & KEY_LEFT) && player_lane > 0) {
|
|
210
|
-
player_lane--;
|
|
211
|
-
sfx_tone(2, 1300, 2); /* lane switch */
|
|
212
|
-
}
|
|
213
|
-
if ((now & KEY_RIGHT) && !(prev_keys & KEY_RIGHT) && player_lane < 2) {
|
|
214
|
-
player_lane++;
|
|
215
|
-
sfx_tone(2, 1300, 2);
|
|
216
|
-
}
|
|
217
|
-
player.x = lane_x[player_lane];
|
|
218
|
-
prev_keys = now;
|
|
219
|
-
|
|
220
|
-
/* Obstacle speed grows slowly with score. */
|
|
221
|
-
s16 step = 2 + (s16)(score / 500);
|
|
222
|
-
if (step > 4) step = 4;
|
|
223
|
-
|
|
224
|
-
for (int i = 0; i < MAX_OBSTACLES; i++) {
|
|
225
|
-
if (!obstacles[i].alive) continue;
|
|
226
|
-
obstacles[i].y += step;
|
|
227
|
-
if (obstacles[i].y > 160) obstacles[i].alive = 0;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (++spawn_timer >= 32) { spawn_timer = 0; spawn_obstacle(); }
|
|
231
|
-
|
|
232
|
-
for (int i = 0; i < MAX_OBSTACLES; i++) {
|
|
233
|
-
if (obstacles[i].alive && aabb(&player, &obstacles[i])) {
|
|
234
|
-
game_over_timer = 60;
|
|
235
|
-
sfx_noise(30); /* crash */
|
|
236
|
-
break;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
if (score < 65500u) score++;
|
|
549
|
+
update_play();
|
|
241
550
|
}
|
|
242
551
|
|
|
243
|
-
/*
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
ATTR1_SIZE_8,
|
|
254
|
-
ATTR2_PALBANK(1) | TILE_CAR_EN);
|
|
255
|
-
obj_set_pos(&obj_buffer[1 + i], obstacles[i].x, ey);
|
|
552
|
+
/* Apply the affine road every frame. On the title/result we still spin
|
|
553
|
+
* a slow, gently-banking road so the screen is never a blank card and
|
|
554
|
+
* the affine hardware is visibly alive. In play, zoom pulses with
|
|
555
|
+
* speed: faster = the road scales UP and rushes toward you. */
|
|
556
|
+
if (state == ST_PLAY) {
|
|
557
|
+
u32 zoom = (u32)(220 + speed * 14); /* 0.86..1.19 (8.8) */
|
|
558
|
+
road_apply((u16)bank, zoom, road_scroll);
|
|
559
|
+
} else {
|
|
560
|
+
road_scroll = (u16)(road_scroll + 1);
|
|
561
|
+
road_apply((u16)(road_scroll << 6), 256, road_scroll);
|
|
256
562
|
}
|
|
257
|
-
oam_copy(oam_mem, obj_buffer, 1 + MAX_OBSTACLES);
|
|
258
563
|
|
|
259
|
-
|
|
260
|
-
|
|
564
|
+
stage_sprites();
|
|
565
|
+
oam_copy(oam_mem, obj_buffer, 128);
|
|
261
566
|
}
|
|
262
567
|
return 0;
|
|
263
568
|
}
|