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,211 +1,672 @@
|
|
|
1
|
-
/* ── platformer.c — Game Boy Advance
|
|
1
|
+
/* ── platformer.c — Game Boy Advance side-scrolling platformer (complete game) ─
|
|
2
2
|
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
3
|
+
* GEAR GROTTO — a COMPLETE, working game: title screen, score + persistent
|
|
4
|
+
* hi-score (cartridge SRAM), music + SFX, gravity/jump physics over a
|
|
5
|
+
* scrolling tile level, coins + distance scoring, and the GBA's signature
|
|
6
|
+
* affine hardware shown where it actually fits a platformer:
|
|
7
|
+
* - a spinning GEAR HAZARD: a 32x32 OBJ that continuously rotates (and
|
|
8
|
+
* scale-pulses) via an OAM affine matrix (8.8 fixed point, affine slot 0,
|
|
9
|
+
* double-size flag). Touch it and you lose a life. The classic
|
|
10
|
+
* spinning-saw/gear obstacle, done the hardware-honest way.
|
|
8
11
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
12
|
+
* The level is a regular (text/tiled) Mode-0 BG: a 64x32 map = a 512-px run
|
|
13
|
+
* of pits, platforms, ground and coins, scrolled by REG_BG0HOFS as the camera
|
|
14
|
+
* follows the player. The 64x32 map WRAPS in hardware at 512 px, so writing
|
|
15
|
+
* (cam & 511) to REG_BG0HOFS and looking up world columns with (& 63) makes
|
|
16
|
+
* the run LOOP SEAMLESSLY — an endless runner with no streaming. The HUD
|
|
17
|
+
* lives on a SECOND regular BG (TTE) that we never scroll.
|
|
11
18
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
19
|
+
* THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
|
|
20
|
+
* very different one. The markers tell you what's what:
|
|
21
|
+
* HARDWARE IDIOM (load-bearing) — dodges a documented GBA footgun; reshape
|
|
22
|
+
* your gameplay around it (see TROUBLESHOOTING before changing).
|
|
23
|
+
* GAME LOGIC (clay) — level layout, physics tuning, scoring, art: reshape
|
|
24
|
+
* freely.
|
|
25
|
+
*
|
|
26
|
+
* What depends on what:
|
|
27
|
+
* gba_sfx.{h,c} — PSG sound: sfx_tone/sfx_noise one-shots + the music loop
|
|
28
|
+
* (sfx_music_tick once per frame — forget it and the game is silent).
|
|
29
|
+
* libtonc (the build links it) — VBlankIntrWait/key_poll/OAM/TTE.
|
|
30
|
+
*
|
|
31
|
+
* HANDHELD, SO SINGLE-PLAYER ONLY (honest note): 2P on GBA means a link
|
|
32
|
+
* cable between two units — a second emulator instance this environment
|
|
33
|
+
* can't provide. Title is press-start, no mode select.
|
|
34
|
+
*
|
|
35
|
+
* Frame budget: ARM7TDMI at 16.78MHz with this object count (player + 3 coins
|
|
36
|
+
* + the gear) doesn't come close to a full frame; the affine math is a handful
|
|
37
|
+
* of multiplies per frame, not per pixel — the PPU does the per-pixel work.
|
|
15
38
|
*/
|
|
16
39
|
|
|
17
40
|
#include <tonc.h>
|
|
18
41
|
#include "gba_sfx.h"
|
|
19
42
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
#define
|
|
43
|
+
/* The title screen renders this — examples({op:'fork'}) stamps your game's
|
|
44
|
+
* name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
|
|
45
|
+
#define GAME_TITLE "GEAR GROTTO"
|
|
23
46
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
47
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
48
|
+
* Object pools — fixed slots, no allocation. Sprite slot discipline (128 OAM
|
|
49
|
+
* entries total, we use 5):
|
|
50
|
+
* slot 0 → player
|
|
51
|
+
* slot 1..3 → coins
|
|
52
|
+
* slot 4 → gear hazard (AFFINE — uses OAM affine parameter slot 0; see
|
|
53
|
+
* the affine-sprite idiom below for why slot CHOICE matters)
|
|
54
|
+
*/
|
|
55
|
+
#define SLOT_PLAYER 0
|
|
56
|
+
#define SLOT_COIN 1
|
|
57
|
+
#define NUM_COINS 3
|
|
58
|
+
#define SLOT_GEAR 4
|
|
59
|
+
|
|
60
|
+
#define TILE_PLAYER 1 /* sprite tile 1, 8x8 4bpp */
|
|
61
|
+
#define TILE_COIN 2
|
|
62
|
+
#define TILE_GEAR 16 /* 32x32 4bpp = 16 tiles, ids 16..31 */
|
|
63
|
+
|
|
64
|
+
#define START_LIVES 3
|
|
65
|
+
|
|
66
|
+
/* World geometry — a 512-px level in a BG0 64x32 map (whole world, no stream).
|
|
67
|
+
* Physics runs in WORLD pixels; sprites draw at SCREEN = world - camera. */
|
|
68
|
+
#define WORLD_W 512
|
|
69
|
+
#define SCREEN_W 240
|
|
70
|
+
#define SCREEN_H 160
|
|
71
|
+
|
|
72
|
+
/* 4bpp sprite tiles (8 rows × 32 bits each = 32 bytes). Each nibble is a
|
|
73
|
+
* palette index within the sprite's palbank. Index 0 = transparent. */
|
|
28
74
|
static const u32 tile_player[8] = {
|
|
29
|
-
0x00033000, 0x00333300,
|
|
30
|
-
0x03333330, 0x03333330,
|
|
75
|
+
0x00033000, 0x00333300, 0x03311330, 0x03333330,
|
|
76
|
+
0x03333330, 0x03333330, 0x03300330, 0x03000030,
|
|
77
|
+
};
|
|
78
|
+
static const u32 tile_coin[8] = {
|
|
79
|
+
0x00022000, 0x00222200, 0x02244220, 0x02422420,
|
|
80
|
+
0x02422420, 0x02244220, 0x00222200, 0x00022000,
|
|
31
81
|
};
|
|
32
82
|
|
|
33
|
-
typedef struct {
|
|
83
|
+
typedef struct { s32 x; s16 y; u8 alive; } Coin;
|
|
84
|
+
|
|
85
|
+
static OBJ_ATTR obj_buffer[128];
|
|
86
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
87
|
+
* OAM AFFINE SLOT LAYOUT. There is no separate affine-matrix memory: the 32
|
|
88
|
+
* OBJ_AFFINE parameter sets live INTERLEAVED inside OAM itself, in the
|
|
89
|
+
* 16-bit "fill" field of every OBJ_ATTR (4 sprites × 8 bytes carry one
|
|
90
|
+
* 8-byte matrix between them — pa in sprite 4n, pb in 4n+1, pc in 4n+2,
|
|
91
|
+
* pd in 4n+3). Casting the shadow-OAM buffer to OBJ_AFFINE* is the whole
|
|
92
|
+
* trick: obj_aff_buffer[k] aliases the fill words of sprites 4k..4k+3, and
|
|
93
|
+
* one oam_copy() of the full buffer commits sprites AND matrices together.
|
|
94
|
+
* Consequences you must respect:
|
|
95
|
+
* - oam_init() already set all 32 matrices to identity (pa=pd=0x0100).
|
|
96
|
+
* - NEVER memset OBJ_ATTRs to 0 — that zeroes the interleaved matrices
|
|
97
|
+
* (pa=0 means "scale by infinity": every affine sprite vanishes).
|
|
98
|
+
* - Matrix slot k is INDEPENDENT of which sprite uses it (attr1 AFF_ID
|
|
99
|
+
* picks any of the 32) — but the bytes live under sprites 4k..4k+3.
|
|
100
|
+
* requires: obj_buffer staged with oam_init(), committed with oam_copy(). */
|
|
101
|
+
static OBJ_AFFINE *const obj_aff_buffer = (OBJ_AFFINE *)obj_buffer;
|
|
102
|
+
|
|
103
|
+
/* ── GAME LOGIC (clay) — player physics state ────────────────────────────────
|
|
104
|
+
* Position is fixed-point: 1 px = 16 subpixel units (Q.4). Gravity adds well
|
|
105
|
+
* under 1 px/frame near the jump apex, so sub-pixel Y is mandatory — integer
|
|
106
|
+
* Y would stutter the arc. X stays integer (walking is whole-pixel). */
|
|
107
|
+
#define SUBPX 16 /* subpixels per pixel (Q.4) */
|
|
108
|
+
#define GRAVITY 12 /* +12/16 px per frame per frame */
|
|
109
|
+
#define JUMP_VEL (-200) /* launch vy (Q.4) → ~6-tile apex */
|
|
110
|
+
#define MAX_FALL 80 /* terminal velocity 5 px/frame — keep < 6: *
|
|
111
|
+
* the landing window is 6 px, a faster fall *
|
|
112
|
+
* would tunnel through a platform top */
|
|
113
|
+
#define MOVE_SPEED 2 /* px/frame walk + scroll speed */
|
|
114
|
+
#define SCROLL_WALL 96 /* px: past this the world scrolls, not you */
|
|
34
115
|
|
|
35
|
-
|
|
36
|
-
|
|
116
|
+
static s32 px; /* player WORLD x, whole px (left edge) — *
|
|
117
|
+
* s32: the endless camera grows without bound*/
|
|
118
|
+
static s32 py_q4; /* player WORLD y, Q.4 (top edge) */
|
|
119
|
+
static s16 vy_q4; /* vertical velocity, Q.4 */
|
|
120
|
+
static u8 on_ground;
|
|
121
|
+
static s32 cam_x; /* camera world-x (BG0 scroll, ever-growing) */
|
|
122
|
+
static u16 dist_sub; /* sub-counter: 64 px walked = +1 point */
|
|
37
123
|
|
|
38
|
-
|
|
39
|
-
static
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
124
|
+
static Coin coins[NUM_COINS];
|
|
125
|
+
static u16 score, hiscore;
|
|
126
|
+
static u8 lives;
|
|
127
|
+
static u16 frame; /* free-running frame counter (drives gear) */
|
|
128
|
+
|
|
129
|
+
/* Gear hazard state — the affine sprite showcase. WORLD-anchored; it drifts
|
|
130
|
+
* left with the camera like the level does. */
|
|
131
|
+
static s32 gear_wx, gear_wy; /* CENTER of the gear, WORLD pixels */
|
|
132
|
+
static u16 gear_theta; /* rotation angle: full circle = 0x10000 */
|
|
133
|
+
static u16 gear_pulse; /* scale-pulse phase */
|
|
134
|
+
|
|
135
|
+
/* Game states — the shell every example shares: title → play → game over. */
|
|
136
|
+
#define ST_TITLE 0
|
|
137
|
+
#define ST_PLAY 1
|
|
138
|
+
#define ST_OVER 2
|
|
139
|
+
static u8 state;
|
|
140
|
+
|
|
141
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
142
|
+
* PERSISTENT SRAM at 0x0E000000. Two footguns, both fatal-but-silent:
|
|
143
|
+
* 1. The SRAM bus is 8 BITS WIDE. Byte reads/writes only — a u16/u32
|
|
144
|
+
* access doesn't fault, it just reads the same byte mirrored (and a
|
|
145
|
+
* wide write stores one byte), so your data "almost" round-trips and
|
|
146
|
+
* then the checksum never matches. Every access below is via vu8.
|
|
147
|
+
* 2. Emulators and flashcarts detect the SAVE TYPE by scanning the ROM
|
|
148
|
+
* image for a marker string. Without "SRAM_V" in the ROM, mGBA gives
|
|
149
|
+
* the cart NO save memory at all and writes to 0x0E000000 vanish.
|
|
150
|
+
* The aligned, (used)-attributed const below plants that marker —
|
|
151
|
+
* delete it and persistence dies even though this code is untouched.
|
|
152
|
+
* Layout: 'V' 'X' score-lo score-hi checksum (xor ^ 0xA5) — magic+checksum
|
|
153
|
+
* so a fresh (0xFF-filled) cart reads as "no record" instead of garbage.
|
|
154
|
+
* requires: nothing else — self-contained; safe to transplant whole. */
|
|
155
|
+
#define SRAM_BYTE ((volatile u8 *)0x0E000000)
|
|
156
|
+
__attribute__((used, aligned(4))) static const char sram_type_marker[] = "SRAM_V113";
|
|
157
|
+
|
|
158
|
+
static u16 hiscore_load(void) {
|
|
159
|
+
u8 lo, hi;
|
|
160
|
+
if (SRAM_BYTE[0] != 'V' || SRAM_BYTE[1] != 'X') return 0;
|
|
161
|
+
lo = SRAM_BYTE[2];
|
|
162
|
+
hi = SRAM_BYTE[3];
|
|
163
|
+
if (SRAM_BYTE[4] != (u8)(lo ^ hi ^ 0xA5)) return 0;
|
|
164
|
+
return (u16)(lo | (hi << 8));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
static void hiscore_save(u16 v) {
|
|
168
|
+
SRAM_BYTE[0] = 'V';
|
|
169
|
+
SRAM_BYTE[1] = 'X';
|
|
170
|
+
SRAM_BYTE[2] = (u8)v;
|
|
171
|
+
SRAM_BYTE[3] = (u8)(v >> 8);
|
|
172
|
+
SRAM_BYTE[4] = (u8)((u8)v ^ (u8)(v >> 8) ^ 0xA5);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* ── GAME LOGIC (clay) — TTE text helpers ────────────────────────────────────
|
|
176
|
+
* Draw right-aligned decimal digits at pixel (x,y) WITHOUT tte_printf. The
|
|
177
|
+
* bundled libtonc's tte_printf with a %d conversion is broken (it routes
|
|
178
|
+
* through a vsnprintf path that isn't wired in this build — it garbles
|
|
179
|
+
* output AND wedges the loop when called per-frame, GBA-1). We build the
|
|
180
|
+
* string ourselves and use tte_write, which processes the #{P:x,y} position
|
|
181
|
+
* command but does NO format conversion → safe every frame. */
|
|
182
|
+
static void draw_num(int x, int y, unsigned v, int digits) {
|
|
183
|
+
char buf[24];
|
|
184
|
+
int i, n = 0;
|
|
185
|
+
buf[n++] = '#'; buf[n++] = '{'; buf[n++] = 'P'; buf[n++] = ':';
|
|
186
|
+
if (x >= 100) buf[n++] = (char)('0' + (x / 100) % 10);
|
|
187
|
+
if (x >= 10) buf[n++] = (char)('0' + (x / 10) % 10);
|
|
188
|
+
buf[n++] = (char)('0' + x % 10);
|
|
189
|
+
buf[n++] = ',';
|
|
190
|
+
if (y >= 100) buf[n++] = (char)('0' + (y / 100) % 10);
|
|
191
|
+
if (y >= 10) buf[n++] = (char)('0' + (y / 10) % 10);
|
|
192
|
+
buf[n++] = (char)('0' + y % 10);
|
|
193
|
+
buf[n++] = '}';
|
|
194
|
+
for (i = digits - 1; i >= 0; i--) { buf[n + i] = (char)('0' + (v % 10)); v /= 10; }
|
|
195
|
+
n += digits; buf[n] = 0;
|
|
196
|
+
tte_write(buf);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
200
|
+
* AFFINE SPRITE (the gear hazard) — the 8.8 screen→texture matrix stored in
|
|
201
|
+
* OAM affine slot 0 (see the slot-layout idiom at obj_aff_buffer). Three
|
|
202
|
+
* OBJ-specific footguns this block dodges:
|
|
203
|
+
* 1. attr0 mode bits: ATTR0_AFF (01) turns affine ON; ATTR0_AFF_DBL (11)
|
|
204
|
+
* is affine + DOUBLE-SIZE. Without double-size the sprite is clipped
|
|
205
|
+
* to its original WxH box — a rotated 32x32 has its corners CUT OFF
|
|
206
|
+
* (≈29% of the diagonal) and a zoomed-up one is cropped to 32x32.
|
|
207
|
+
* Double-size renders into a 64x64 window so rotation/zoom≤2x fits.
|
|
208
|
+
* 2. Double-size MOVES THE SPRITE: attr0/attr1 x/y are the top-left of
|
|
209
|
+
* the RENDER WINDOW, so the visual center sits at (x+32, y+32) for a
|
|
210
|
+
* 32x32 sprite — position it by center and subtract 32 (a plain
|
|
211
|
+
* sprite would subtract 16). Forks that toggle DBL must re-anchor.
|
|
212
|
+
* 3. ATTR0_HIDE does NOT hide an affine sprite — mode bits 01/11 reuse
|
|
213
|
+
* the hide bit. To hide the gear, drop attr0 back to a REGULAR hidden
|
|
214
|
+
* object (ATTR0_HIDE alone).
|
|
215
|
+
* The MATRIX (8.8 fixed point, 256 == 1.0) maps SCREEN pixels → TEXTURE
|
|
216
|
+
* pixels (the INVERSE of "how the image is transformed"), so to SHOW the
|
|
217
|
+
* texture rotated by θ and zoomed by z you write the matrix of -θ and 1/z:
|
|
218
|
+
* inv = 65536/z (8.8 reciprocal: 1/z)
|
|
219
|
+
* pa = cos·inv>>8 pb = -sin·inv>>8
|
|
220
|
+
* pc = sin·inv>>8 pd = cos·inv>>8
|
|
221
|
+
* lu_sin/lu_cos take a u16 angle (full circle = 0x10000) and return 4.12
|
|
222
|
+
* fixed → >>4 converts to 8.8.
|
|
223
|
+
* requires: OAM affine slot 0 free (sprites 0..3's fill words — fine here,
|
|
224
|
+
* they're regular objects whose fill is untouched), obj_buffer committed
|
|
225
|
+
* by oam_copy() every frame, gear tiles at OBJ tile 16 (4bpp 32x32, 1D). */
|
|
226
|
+
static void gear_stage(s16 screen_cx, s16 screen_cy) {
|
|
227
|
+
OBJ_ATTR *o = &obj_buffer[SLOT_GEAR];
|
|
228
|
+
/* zoom pulse: 1.0 ± 0.20 from the sine LUT (4.12 → ±~50 in 8.8). A
|
|
229
|
+
* pure rotation is readable, but the gentle breathing makes the affine
|
|
230
|
+
* scaling visible too — both halves of the idiom on screen at once. */
|
|
231
|
+
u32 zoom = (u32)(256 + (lu_sin(gear_pulse) >> 7));
|
|
232
|
+
s32 inv = (s32)(65536u / zoom);
|
|
233
|
+
s32 cc = ((lu_cos(gear_theta) >> 4) * inv) >> 8;
|
|
234
|
+
s32 ss = ((lu_sin(gear_theta) >> 4) * inv) >> 8;
|
|
235
|
+
obj_aff_buffer[0].pa = (s16)cc; obj_aff_buffer[0].pb = (s16)-ss;
|
|
236
|
+
obj_aff_buffer[0].pc = (s16)ss; obj_aff_buffer[0].pd = (s16)cc;
|
|
237
|
+
|
|
238
|
+
o->attr0 = (u16)(ATTR0_AFF_DBL | ATTR0_SQUARE | ATTR0_4BPP
|
|
239
|
+
| ((screen_cy - 32) & 0x00FF)); /* window top */
|
|
240
|
+
o->attr1 = (u16)(ATTR1_SIZE_32 | ATTR1_AFF_ID(0)
|
|
241
|
+
| ((screen_cx - 32) & 0x01FF)); /* window left */
|
|
242
|
+
o->attr2 = (u16)(ATTR2_PALBANK(3) | TILE_GEAR);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/* ── GAME LOGIC (clay) — gear ART: a toothed disc with ONE bright tooth (the
|
|
246
|
+
* asymmetry makes the spin readable; a symmetric disc looks static).
|
|
247
|
+
* Drawn procedurally into a 32x32 4bpp staging buffer laid out exactly as
|
|
248
|
+
* OBJ VRAM wants it in 1D mapping: 16 consecutive 8x8 tiles, row-major
|
|
249
|
+
* within the sprite, 2 pixels per byte (low nibble = left pixel). */
|
|
250
|
+
static void gear_build_tiles(void) {
|
|
251
|
+
static u32 tiles[16][8];
|
|
252
|
+
int x, y;
|
|
253
|
+
for (y = 0; y < 32; y++)
|
|
254
|
+
for (x = 0; x < 32; x++) {
|
|
255
|
+
int dx = x - 16, dy = y - 16;
|
|
256
|
+
int r2 = dx * dx + dy * dy;
|
|
257
|
+
int c = 0;
|
|
258
|
+
if (r2 < 25) c = 3; /* hub bore */
|
|
259
|
+
else if (r2 < 64) c = 1; /* hub */
|
|
260
|
+
else if (r2 < 144) c = 2; /* gear body*/
|
|
261
|
+
/* eight square teeth around the rim (one per 45°) */
|
|
262
|
+
if (r2 >= 144 && r2 < 225) {
|
|
263
|
+
if ((dx >= -2 && dx <= 2) || (dy >= -2 && dy <= 2)
|
|
264
|
+
|| (dx - dy >= -3 && dx - dy <= 3)
|
|
265
|
+
|| (dx + dy >= -3 && dx + dy <= 3)) c = 2;
|
|
266
|
+
}
|
|
267
|
+
if (dx >= -2 && dx <= 2 && dy > 8 && dy < 16) c = 4; /* BRIGHT tooth ↓ (spin marker) */
|
|
268
|
+
if (c) {
|
|
269
|
+
int t = (y / 8) * 4 + (x / 8);
|
|
270
|
+
tiles[t][y % 8] |= (u32)c << (4 * (x % 8));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
tonccpy(&tile_mem[4][TILE_GEAR], tiles, sizeof(tiles));
|
|
274
|
+
pal_obj_bank[3][1] = RGB15(20, 20, 22); /* hub steel */
|
|
275
|
+
pal_obj_bank[3][2] = RGB15(13, 13, 16); /* gear body */
|
|
276
|
+
pal_obj_bank[3][3] = RGB15(28, 26, 10); /* bore brass */
|
|
277
|
+
pal_obj_bank[3][4] = RGB15(31, 12, 6); /* THE hot tooth (spin marker) */
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/* ── GAME LOGIC (clay — reshape freely) ─────────────────────────────────── */
|
|
281
|
+
static u8 rng_state = 0xA5;
|
|
282
|
+
static u8 rand8(void) { /* Galois LFSR, period 255 */
|
|
283
|
+
u8 lsb = (u8)(rng_state & 1);
|
|
284
|
+
rng_state >>= 1;
|
|
285
|
+
if (lsb) rng_state ^= 0xB8;
|
|
286
|
+
return rng_state;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
290
|
+
* The level — a 64-column world (512 px). For each column:
|
|
291
|
+
* ground_row[c] — map row of the ground's grass top, NO_GROUND = a pit.
|
|
292
|
+
* plat_row[c] — row of a one-way floating platform, 0 = none.
|
|
293
|
+
* Rows are map rows (y = row*8). Map is 32 rows tall (256 px); the playfield
|
|
294
|
+
* sits in rows 3..19, ground around row 18 (y=144). Identical layout repeats
|
|
295
|
+
* are fine — this is a fixed hand-authored run, not a procedural loop. */
|
|
296
|
+
#define NO_GROUND 0xFF
|
|
297
|
+
#define GROUND_ROW 18 /* y = 144 — the main floor */
|
|
298
|
+
#define MAP_COLS 64
|
|
299
|
+
static const u8 ground_row[MAP_COLS] = {
|
|
300
|
+
18,18,18,18,18,18,18,18, /* long start runway */
|
|
301
|
+
18,18,18,18,18,18,18,18, /* (lets it scroll */
|
|
302
|
+
18,18,18,18,NO_GROUND,NO_GROUND,18,18, /* pit 1 (cols 20-21)*/
|
|
303
|
+
18,18,18,18,18,18,18,18,
|
|
304
|
+
18,18,NO_GROUND,NO_GROUND,NO_GROUND,18,18,18, /* pit 2 wide (34-36)*/
|
|
305
|
+
18,18,18,18,18,18,18,18,
|
|
306
|
+
18,18,18,NO_GROUND,NO_GROUND,18,18,18, /* pit 3 (51-52) */
|
|
307
|
+
18,18,18,18,18,18,18,18, /* finish runway */
|
|
308
|
+
};
|
|
309
|
+
static const u8 plat_row[MAP_COLS] = {
|
|
310
|
+
0,0,0,0,0,0,12,12, /* warm-up slab */
|
|
311
|
+
12,0,0,0,0,0,0,0,
|
|
312
|
+
0,0,11,11,11,0,0,0, /* slab over pit 1 */
|
|
313
|
+
0,0,0,10,10,10,0,0, /* high slab */
|
|
314
|
+
0,11,11,11,11,0,0,0, /* slab over pit 2 */
|
|
315
|
+
0,0,0,0,9,9,0,0, /* high slab */
|
|
316
|
+
0,0,12,12,12,0,0,0, /* slab over pit 3 */
|
|
317
|
+
0,0,0,0,13,13,13,0, /* finish slab */
|
|
47
318
|
};
|
|
48
|
-
/* (int) cast so `for (int i = 0; i < N_PLATFORMS; ...)` doesn't compare a
|
|
49
|
-
* signed counter against an unsigned size_t (-Wsign-compare). */
|
|
50
|
-
#define N_PLATFORMS ((int)(sizeof(platforms) / sizeof(platforms[0])))
|
|
51
319
|
|
|
52
|
-
|
|
320
|
+
#define BG_BLANK 0
|
|
321
|
+
#define BG_GRASS 1 /* ground surface + floating slabs */
|
|
322
|
+
#define BG_DIRT 2 /* ground body */
|
|
323
|
+
#define BG_BRICK 3 /* backdrop accent */
|
|
324
|
+
|
|
325
|
+
/* ── GAME LOGIC (clay) — BG tile art (regular Mode-0 4bpp BG tiles).
|
|
326
|
+
* Each 8x8 4bpp tile is 8 u32 rows; each nibble a palette index in the BG
|
|
327
|
+
* palbank we use (bank 0 — regular BGs carry a 4-bit palbank per map entry,
|
|
328
|
+
* unlike affine BGs). Index 0 transparent. */
|
|
329
|
+
static const u32 bg_tile_grass[8] = {
|
|
330
|
+
0x11111111, 0x11111111, 0x21212121, 0x22222222,
|
|
331
|
+
0x22222222, 0x22222222, 0x22222222, 0x22222222,
|
|
332
|
+
};
|
|
333
|
+
static const u32 bg_tile_dirt[8] = {
|
|
334
|
+
0x22222222, 0x22322222, 0x22222222, 0x22222232,
|
|
335
|
+
0x22222222, 0x23222222, 0x22222222, 0x22222223,
|
|
336
|
+
};
|
|
337
|
+
static const u32 bg_tile_brick[8] = {
|
|
338
|
+
0x33333333, 0x30303030, 0x33333333, 0x03030303,
|
|
339
|
+
0x33333333, 0x30303030, 0x33333333, 0x03030303,
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
343
|
+
* SCROLLING TILE BG (BG0, Mode 0) — a REGULAR text BG, the bread-and-butter
|
|
344
|
+
* GBA background. A 64x32 map (BG_REG_64x32) is exactly the 512-px world, so
|
|
345
|
+
* the whole level fits and we never stream: the camera just writes
|
|
346
|
+
* REG_BG0HOFS each frame. Footguns this block dodges:
|
|
347
|
+
* - A 64-wide regular map is TWO 32x32 screenblocks SIDE BY SIDE (SBB n =
|
|
348
|
+
* left 32 cols, SBB n+1 = right 32 cols). A flat (col,row) write must
|
|
349
|
+
* route col<32 to the left block and col>=32 to the right — MAP_SET
|
|
350
|
+
* below does that. (libtonc's se_mem[] indexes one screenblock.)
|
|
351
|
+
* - Each map entry is a u16: tile id (10 bits) + hflip/vflip + a 4-bit
|
|
352
|
+
* PALBANK. SE_BUILD(tile, palbank, hflip, vflip) packs it. (Regular BGs
|
|
353
|
+
* carry a palbank per tile; affine BGs do NOT — that's the other idiom.)
|
|
354
|
+
* - VRAM ignores byte writes (a u8 store duplicates the byte into both
|
|
355
|
+
* halves of the 16-bit lane). We only ever write whole u16 SE entries
|
|
356
|
+
* and tonccpy() tile data, both VRAM-safe.
|
|
357
|
+
* requires: DCNT_MODE0 + DCNT_BG0, BG0CNT pointing CBB 0 / SBB 28 (so 28+29
|
|
358
|
+
* hold the 64-wide map), REG_BG0HOFS written every frame, BG1 (TTE) kept
|
|
359
|
+
* clear of SBB 28/29. */
|
|
360
|
+
static SCR_ENTRY *const sbbL = se_mem[28]; /* left 32 cols */
|
|
361
|
+
static SCR_ENTRY *const sbbR = se_mem[29]; /* right 32 cols */
|
|
362
|
+
#define MAP_SET(tx, ty, se) do { \
|
|
363
|
+
if ((tx) < 32) sbbL[(ty) * 32 + (tx)] = (se); \
|
|
364
|
+
else sbbR[(ty) * 32 + ((tx) - 32)] = (se); \
|
|
365
|
+
} while (0)
|
|
366
|
+
|
|
367
|
+
static void build_level(void) {
|
|
368
|
+
int tx, ty;
|
|
369
|
+
u8 g, pr;
|
|
370
|
+
|
|
371
|
+
pal_bg_mem[0] = RGB15(2, 3, 8); /* cave backdrop (BG backdrop) */
|
|
372
|
+
pal_bg_mem[1] = RGB15(10, 24, 8); /* grass green */
|
|
373
|
+
pal_bg_mem[2] = RGB15(14, 9, 4); /* dirt brown */
|
|
374
|
+
pal_bg_mem[3] = RGB15(6, 7, 13); /* brick slate */
|
|
53
375
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
376
|
+
tonccpy(&tile_mem[0][BG_GRASS], bg_tile_grass, sizeof(bg_tile_grass));
|
|
377
|
+
tonccpy(&tile_mem[0][BG_DIRT], bg_tile_dirt, sizeof(bg_tile_dirt));
|
|
378
|
+
tonccpy(&tile_mem[0][BG_BRICK], bg_tile_brick, sizeof(bg_tile_brick));
|
|
379
|
+
|
|
380
|
+
for (ty = 0; ty < 32; ty++)
|
|
381
|
+
for (tx = 0; tx < MAP_COLS; tx++) {
|
|
382
|
+
u16 se = SE_BUILD(BG_BLANK, 0, 0, 0);
|
|
383
|
+
g = ground_row[tx];
|
|
384
|
+
pr = plat_row[tx];
|
|
385
|
+
if (pr && ty == pr) se = SE_BUILD(BG_GRASS, 0, 0, 0); /* slab */
|
|
386
|
+
else if (g != NO_GROUND && ty == g) se = SE_BUILD(BG_GRASS, 0, 0, 0);
|
|
387
|
+
else if (g != NO_GROUND && ty > g) se = SE_BUILD(BG_DIRT, 0, 0, 0);
|
|
388
|
+
else if (ty < 8 && ((tx * 5 + ty * 7) & 7) == 0)
|
|
389
|
+
se = SE_BUILD(BG_BRICK, 0, 0, 0); /* backdrop */
|
|
390
|
+
MAP_SET(tx, ty, se);
|
|
63
391
|
}
|
|
392
|
+
REG_BG0CNT = BG_CBB(0) | BG_SBB(28) | BG_REG_64x32 | BG_4BPP | BG_PRIO(2);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/* ── GAME LOGIC (clay) — landing probe against the column map ──────────────
|
|
396
|
+
* One-way platforms: catch the player only while FALLING through a narrow
|
|
397
|
+
* window at a surface top. Window is feet in [top-1 .. top+5] so a 5px/frame
|
|
398
|
+
* terminal fall can't step over it (tunnelling). Columns are WORLD columns. */
|
|
399
|
+
static s16 land_top(int wcol, s16 feet) {
|
|
400
|
+
u8 r;
|
|
401
|
+
s16 top;
|
|
402
|
+
wcol &= (MAP_COLS - 1); /* wrap: the 64-col map loops endlessly */
|
|
403
|
+
r = plat_row[wcol];
|
|
404
|
+
if (r) {
|
|
405
|
+
top = (s16)(r << 3);
|
|
406
|
+
if (feet + 1 >= top && feet <= top + 5) return top;
|
|
64
407
|
}
|
|
65
|
-
|
|
408
|
+
r = ground_row[wcol];
|
|
409
|
+
if (r != NO_GROUND) {
|
|
410
|
+
top = (s16)(r << 3);
|
|
411
|
+
if (feet + 1 >= top && feet <= top + 5) return top;
|
|
412
|
+
}
|
|
413
|
+
return -1;
|
|
66
414
|
}
|
|
67
415
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
416
|
+
/* ── GAME LOGIC (clay) — coins (world-anchored sprite objects) ── */
|
|
417
|
+
static const s16 coin_heights[4] = { 88, 72, 104, 56 };
|
|
418
|
+
static void place_coin(u8 i, s32 wx) {
|
|
419
|
+
coins[i].x = wx;
|
|
420
|
+
coins[i].y = coin_heights[rand8() & 3];
|
|
421
|
+
coins[i].alive = 1;
|
|
422
|
+
}
|
|
74
423
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
424
|
+
/* Box overlap in WORLD coords. s32 so it stays correct as the endless camera
|
|
425
|
+
* grows past 16 bits — overlapping objects always have a small difference. */
|
|
426
|
+
static int aabb(s32 ax, s32 ay, s32 bx, s32 by, s32 r) {
|
|
427
|
+
s32 dx = ax - bx, dy = ay - by;
|
|
428
|
+
if (dx < 0) dx = -dx;
|
|
429
|
+
if (dy < 0) dy = -dy;
|
|
430
|
+
return dx < r && dy < r;
|
|
431
|
+
}
|
|
78
432
|
|
|
79
|
-
|
|
80
|
-
|
|
433
|
+
/* ── GAME LOGIC (clay) — HUD / screens (TTE on BG1, priority 0) ── */
|
|
434
|
+
static void draw_hud_labels(void) {
|
|
435
|
+
tte_erase_screen();
|
|
436
|
+
tte_write("#{P:8,4}SC");
|
|
437
|
+
tte_write("#{P:96,4}HI");
|
|
438
|
+
tte_write("#{P:200,4}x");
|
|
439
|
+
}
|
|
81
440
|
|
|
82
|
-
|
|
83
|
-
|
|
441
|
+
static void draw_hud_numbers(void) {
|
|
442
|
+
tte_erase_rect(28, 4, 70, 12); draw_num(28, 4, score, 5);
|
|
443
|
+
tte_erase_rect(116, 4, 158, 12); draw_num(116, 4, hiscore, 5);
|
|
444
|
+
tte_erase_rect(210, 4, 220, 12); draw_num(210, 4, lives, 1);
|
|
445
|
+
}
|
|
84
446
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
447
|
+
static void enter_title(void) {
|
|
448
|
+
state = ST_TITLE;
|
|
449
|
+
tte_erase_screen();
|
|
450
|
+
tte_write("#{P:60,40}" GAME_TITLE);
|
|
451
|
+
tte_write("#{P:76,80}PRESS START");
|
|
452
|
+
tte_write("#{P:88,100}HI");
|
|
453
|
+
draw_num(112, 100, hiscore, 5);
|
|
454
|
+
tte_write("#{P:40,128}DPAD MOVE - A JUMP");
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
static void enter_play(void) {
|
|
458
|
+
int i;
|
|
459
|
+
state = ST_PLAY;
|
|
460
|
+
px = 24; py_q4 = (s32)(112 << 4); vy_q4 = 0; on_ground = 1;
|
|
461
|
+
cam_x = 0; dist_sub = 0;
|
|
462
|
+
score = 0; lives = START_LIVES; frame = 0;
|
|
463
|
+
for (i = 0; i < NUM_COINS; i++)
|
|
464
|
+
place_coin((u8)i, (s16)(80 + i * 130));
|
|
465
|
+
gear_wx = 384; gear_wy = 116; /* hovers over the mid-level run */
|
|
466
|
+
gear_theta = 0; gear_pulse = 0;
|
|
467
|
+
draw_hud_labels();
|
|
468
|
+
draw_hud_numbers();
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
static void enter_over(void) {
|
|
472
|
+
state = ST_OVER;
|
|
473
|
+
if (score > hiscore) {
|
|
474
|
+
hiscore = score;
|
|
475
|
+
hiscore_save(hiscore); /* byte-wise SRAM write — see the idiom */
|
|
476
|
+
draw_hud_numbers();
|
|
477
|
+
}
|
|
478
|
+
tte_write("#{P:84,64}GAME OVER");
|
|
479
|
+
tte_write("#{P:76,84}PRESS START");
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
static void lose_life(void) {
|
|
483
|
+
sfx_noise(14);
|
|
484
|
+
if (lives > 0) lives--;
|
|
485
|
+
draw_hud_numbers();
|
|
486
|
+
/* respawn at the player's fixed screen lane, on the ground (the camera is
|
|
487
|
+
* one-way and never resets — the run keeps moving forward). */
|
|
488
|
+
px = (s16)(cam_x + SCROLL_WALL);
|
|
489
|
+
py_q4 = (s32)((GROUND_ROW * 8 - 8) << 4);
|
|
490
|
+
vy_q4 = 0; on_ground = 1;
|
|
491
|
+
/* shove the gear far ahead so we don't respawn straight onto it */
|
|
492
|
+
gear_wx = (s16)(cam_x + SCREEN_W + 120);
|
|
493
|
+
if (lives == 0) enter_over();
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/* ── GAME LOGIC (clay) — one ST_PLAY tick ── */
|
|
497
|
+
static void update_play(void) {
|
|
498
|
+
int i;
|
|
499
|
+
s16 ipy, feet, npy, top;
|
|
500
|
+
|
|
501
|
+
/* Horizontal: the player walks up to SCROLL_WALL (screen x), then holds
|
|
502
|
+
* that lane while the WORLD scrolls under them — a one-way endless runner.
|
|
503
|
+
* cam_x grows without bound; only the BG register and column lookups wrap
|
|
504
|
+
* (mod 512 / mod 64), so the 64x32 map loops seamlessly. */
|
|
505
|
+
if (key_held(KEY_LEFT) && px > cam_x + 8) px -= MOVE_SPEED;
|
|
506
|
+
if (key_held(KEY_RIGHT)) {
|
|
507
|
+
s16 screen_x = (s16)(px - cam_x);
|
|
508
|
+
px += MOVE_SPEED;
|
|
509
|
+
if (screen_x >= SCROLL_WALL) {
|
|
510
|
+
cam_x += MOVE_SPEED; /* world scrolls (camera leads) */
|
|
511
|
+
dist_sub += MOVE_SPEED;
|
|
512
|
+
if (dist_sub >= 64) { dist_sub -= 64; if (score < 65000u) { score++; draw_hud_numbers(); } }
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/* Jump. */
|
|
517
|
+
if (key_hit(KEY_A) && on_ground) {
|
|
518
|
+
vy_q4 = JUMP_VEL;
|
|
519
|
+
on_ground = 0;
|
|
520
|
+
sfx_tone(1, 1500, 6); /* boing */
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/* Gravity + sub-pixel Y. */
|
|
524
|
+
if (vy_q4 < MAX_FALL) vy_q4 += GRAVITY;
|
|
525
|
+
ipy = (s16)(py_q4 >> 4);
|
|
526
|
+
npy = (s16)((py_q4 + vy_q4) >> 4);
|
|
527
|
+
|
|
528
|
+
/* Fell into a pit (below the playfield) → lose a life. */
|
|
529
|
+
if (npy > SCREEN_H + 8) { lose_life(); return; }
|
|
530
|
+
|
|
531
|
+
/* Landing: probe the world columns under the player's feet, while falling.
|
|
532
|
+
* land_top wraps the column (& 63), so the loop's pits/slabs keep coming. */
|
|
533
|
+
if (vy_q4 >= 0) {
|
|
534
|
+
feet = (s16)(npy + 8);
|
|
535
|
+
top = land_top(px >> 3, feet);
|
|
536
|
+
if (top < 0) top = land_top((px + 7) >> 3, feet);
|
|
537
|
+
if (top >= 0 && ipy + 8 <= top + 6) {
|
|
538
|
+
py_q4 = (s32)((top - 8) << 4);
|
|
539
|
+
if (!on_ground) sfx_tone(2, 800, 3); /* landing thud */
|
|
540
|
+
vy_q4 = 0; on_ground = 1;
|
|
541
|
+
} else {
|
|
542
|
+
py_q4 += vy_q4;
|
|
543
|
+
on_ground = 0;
|
|
544
|
+
}
|
|
545
|
+
} else {
|
|
546
|
+
py_q4 += vy_q4; /* rising */
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/* Gear hazard: spin + pulse, world-anchored (drifts with the scroll). The
|
|
550
|
+
* collision is the UNROTATED box around the gear center — honest
|
|
551
|
+
* simplification; a rotating hitbox buys little for a round gear. Once it
|
|
552
|
+
* slides off the left, recycle it ahead at a fresh height. */
|
|
553
|
+
gear_theta = (u16)(gear_theta + 0x0300); /* ~4.2°/frame */
|
|
554
|
+
gear_pulse = (u16)(gear_pulse + 0x0180);
|
|
555
|
+
if (gear_wx < cam_x - 40) {
|
|
556
|
+
gear_wx = (s16)(cam_x + SCREEN_W + 40 + (rand8() & 63));
|
|
557
|
+
gear_wy = (s16)(96 + (rand8() & 31));
|
|
558
|
+
}
|
|
559
|
+
{
|
|
560
|
+
s32 plx = px, ply = (py_q4 >> 4);
|
|
561
|
+
if (aabb(plx + 4, ply + 4, gear_wx, gear_wy, 16)) {
|
|
562
|
+
lose_life();
|
|
563
|
+
if (state != ST_PLAY) return;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/* Coins: collect on overlap, recycle ahead of the camera. */
|
|
568
|
+
{
|
|
569
|
+
s32 plx = px, ply = (py_q4 >> 4);
|
|
570
|
+
for (i = 0; i < NUM_COINS; i++) {
|
|
571
|
+
if (!coins[i].alive) continue;
|
|
572
|
+
if (aabb(plx + 4, ply + 4, coins[i].x + 4, coins[i].y + 4, 8)) {
|
|
573
|
+
coins[i].alive = 0;
|
|
574
|
+
if (score < 65000u) score += 10;
|
|
575
|
+
draw_hud_numbers();
|
|
576
|
+
sfx_tone(1, 1900, 4); /* coin ping */
|
|
111
577
|
}
|
|
578
|
+
/* recycle a coin once it's well behind the camera */
|
|
579
|
+
if (coins[i].x < cam_x - 16)
|
|
580
|
+
place_coin((u8)i, (s16)(cam_x + SCREEN_W + (rand8() & 63)));
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/* ── GAME LOGIC (clay) — stage the regular sprites (the gear has its own
|
|
586
|
+
* idiom block). Off-screen objects park at y=200; either works for REGULAR
|
|
587
|
+
* sprites. The gear is staged in SCREEN space = world - camera. ── */
|
|
588
|
+
static void stage_sprites(void) {
|
|
589
|
+
int i;
|
|
590
|
+
int playing = (state == ST_PLAY);
|
|
591
|
+
s16 sx = (s16)(px - cam_x), sy = (s16)(py_q4 >> 4);
|
|
592
|
+
|
|
593
|
+
obj_set_attr(&obj_buffer[SLOT_PLAYER], ATTR0_SQUARE, ATTR1_SIZE_8,
|
|
594
|
+
ATTR2_PALBANK(0) | TILE_PLAYER);
|
|
595
|
+
obj_set_pos(&obj_buffer[SLOT_PLAYER], playing ? sx : 250, playing ? sy : 200);
|
|
596
|
+
|
|
597
|
+
for (i = 0; i < NUM_COINS; i++) {
|
|
598
|
+
obj_set_attr(&obj_buffer[SLOT_COIN + i], ATTR0_SQUARE, ATTR1_SIZE_8,
|
|
599
|
+
ATTR2_PALBANK(1) | TILE_COIN);
|
|
600
|
+
obj_set_pos(&obj_buffer[SLOT_COIN + i],
|
|
601
|
+
(playing && coins[i].alive) ? (s16)(coins[i].x - cam_x) : 250,
|
|
602
|
+
(playing && coins[i].alive) ? coins[i].y : 200);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (playing) {
|
|
606
|
+
gear_stage((s16)(gear_wx - cam_x), gear_wy);
|
|
607
|
+
} else {
|
|
608
|
+
obj_buffer[SLOT_GEAR].attr0 = ATTR0_HIDE; /* REGULAR + hide (footgun 3) */
|
|
112
609
|
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
int main(void) {
|
|
613
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
614
|
+
* Init order: tiles/palettes → oam_init → irq_init + II_VBLANK →
|
|
615
|
+
* TTE init → DISPCNT last. VBlankIntrWait() HANGS FOREVER without the
|
|
616
|
+
* vblank IRQ registered (the #1 "frozen on frame 1" cause), and
|
|
617
|
+
* enabling DISPCNT layers before their tiles/maps exist flashes garbage.
|
|
618
|
+
* TTE owns BG1 (CBB 2 / SBB 30) — keep other layers off those blocks.
|
|
619
|
+
* requires: nothing prior; this IS the boot. */
|
|
620
|
+
tonccpy(&tile_mem[4][TILE_PLAYER], tile_player, sizeof(tile_player));
|
|
621
|
+
tonccpy(&tile_mem[4][TILE_COIN], tile_coin, sizeof(tile_coin));
|
|
622
|
+
gear_build_tiles();
|
|
623
|
+
pal_obj_bank[0][1] = RGB15(31, 31, 31); /* player eyes white */
|
|
624
|
+
pal_obj_bank[0][3] = RGB15(28, 8, 8); /* player red */
|
|
625
|
+
pal_obj_bank[1][2] = RGB15(28, 24, 6); /* coin gold */
|
|
626
|
+
pal_obj_bank[1][4] = RGB15(31, 31, 18); /* coin shine */
|
|
113
627
|
|
|
114
|
-
|
|
628
|
+
build_level(); /* regular BG0: tiles + 64x32 map */
|
|
629
|
+
oam_init(obj_buffer, 128); /* hides all 128, matrices = identity */
|
|
115
630
|
|
|
116
|
-
/* IRQ setup — required for VBlankIntrWait() to function. */
|
|
117
631
|
irq_init(NULL);
|
|
118
632
|
irq_add(II_VBLANK, NULL);
|
|
119
633
|
|
|
120
|
-
sfx_init();
|
|
634
|
+
sfx_init(); /* APU on; music loop ticks below */
|
|
121
635
|
|
|
122
|
-
/*
|
|
123
|
-
*
|
|
124
|
-
* starting around tile 0 of its char-block). */
|
|
636
|
+
/* TTE text on BG1 (4bpp char block 2, screenblock 30), priority 0 so
|
|
637
|
+
* text draws over everything. Mode 0 = all four BGs regular/tiled. */
|
|
125
638
|
tte_init_chr4c_default(1, BG_CBB(2) | BG_SBB(30));
|
|
126
|
-
|
|
127
|
-
|
|
639
|
+
REG_BG1CNT |= BG_PRIO(0);
|
|
128
640
|
REG_DISPCNT = DCNT_MODE0 | DCNT_BG0 | DCNT_BG1 | DCNT_OBJ | DCNT_OBJ_1D;
|
|
129
641
|
|
|
130
|
-
/*
|
|
131
|
-
|
|
132
|
-
s32 vx = 0, vy = 0;
|
|
133
|
-
s32 camX = 0;
|
|
134
|
-
|
|
135
|
-
const s32 GRAVITY = 12;
|
|
136
|
-
const s32 MOVE_SPEED = 24;
|
|
137
|
-
const s32 JUMP_VEL = -200;
|
|
138
|
-
const s32 MAX_FALL = 320;
|
|
139
|
-
|
|
140
|
-
u16 prev = 0;
|
|
642
|
+
hiscore = hiscore_load(); /* cartridge SRAM — 0 on first boot */
|
|
643
|
+
enter_title();
|
|
141
644
|
|
|
142
645
|
while (1) {
|
|
646
|
+
/* Idiomatic Tonc heartbeat: wait vblank, poll keys, update, then
|
|
647
|
+
* commit OAM + affine slot while still inside vblank (the whole
|
|
648
|
+
* update is far quicker than the 4.9ms vblank window). */
|
|
143
649
|
VBlankIntrWait();
|
|
144
650
|
key_poll();
|
|
651
|
+
sfx_music_tick(); /* forget this → silent game */
|
|
652
|
+
frame++;
|
|
145
653
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (
|
|
654
|
+
if (state == ST_TITLE) {
|
|
655
|
+
if (key_hit(KEY_START | KEY_A)) enter_play();
|
|
656
|
+
} else if (state == ST_OVER) {
|
|
657
|
+
if (key_hit(KEY_START)) enter_title();
|
|
658
|
+
} else {
|
|
659
|
+
update_play();
|
|
660
|
+
}
|
|
149
661
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
662
|
+
/* The 64x32 map is exactly 512 px wide and WRAPS in hardware, so
|
|
663
|
+
* masking cam_x to 9 bits makes the level loop seamlessly under an
|
|
664
|
+
* ever-growing camera. */
|
|
665
|
+
if (state == ST_PLAY) REG_BG0HOFS = (u16)(cam_x & 511);
|
|
666
|
+
else REG_BG0HOFS = 0;
|
|
153
667
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
vy = JUMP_VEL;
|
|
157
|
-
sfx_tone(1, 1500, 6); /* boing */
|
|
158
|
-
}
|
|
159
|
-
prev = now;
|
|
160
|
-
|
|
161
|
-
vy += GRAVITY;
|
|
162
|
-
if (vy > MAX_FALL) vy = MAX_FALL;
|
|
163
|
-
if (grounded && vy > 0) vy = 0;
|
|
164
|
-
|
|
165
|
-
/* Horizontal — clamp to the world (it scrolls now, no wrap). */
|
|
166
|
-
px += vx;
|
|
167
|
-
if (px < 0) px = 0;
|
|
168
|
-
if (px > ((WORLD_W - 8) << 4)) px = (WORLD_W - 8) << 4;
|
|
169
|
-
|
|
170
|
-
/* Vertical with platform-stop. */
|
|
171
|
-
s32 np = py + vy;
|
|
172
|
-
s16 npy = np >> 4;
|
|
173
|
-
/* THE fall-through-the-floor fix: this used to be additionally
|
|
174
|
-
* gated on blocked_below(), which only matches when a platform
|
|
175
|
-
* top is within ONE pixel of the feet — but falls reach 20 px/
|
|
176
|
-
* frame, so the (correct) crossing test below almost never got
|
|
177
|
-
* to run and the player tunnelled through every platform. The
|
|
178
|
-
* crossing test alone is the right check. */
|
|
179
|
-
if (vy > 0) {
|
|
180
|
-
for (int i = 0; i < N_PLATFORMS; i++) {
|
|
181
|
-
const Rect *p = &platforms[i];
|
|
182
|
-
if (ipy + 8 <= p->y && npy + 8 >= p->y
|
|
183
|
-
&& ipx + 8 > p->x && ipx < p->x + p->w) {
|
|
184
|
-
py = (p->y - 8) << 4;
|
|
185
|
-
vy = 0;
|
|
186
|
-
sfx_tone(2, 800, 3); /* thud */
|
|
187
|
-
goto done_y;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
py = np;
|
|
192
|
-
if (py > (160 << 4)) { py = 0; vy = 0; }
|
|
193
|
-
done_y:
|
|
194
|
-
|
|
195
|
-
/* Camera follows player, centered, clamped to the world. Write the
|
|
196
|
-
* BG0 horizontal scroll offset. BG1 (HUD) is left un-scrolled. */
|
|
197
|
-
camX = (px >> 4) - (SCREEN_W / 2 - 4);
|
|
198
|
-
if (camX < 0) camX = 0;
|
|
199
|
-
if (camX > WORLD_W - SCREEN_W) camX = WORLD_W - SCREEN_W;
|
|
200
|
-
REG_BG0HOFS = camX;
|
|
201
|
-
|
|
202
|
-
/* Player sprite drawn in SCREEN space = world - camera. */
|
|
203
|
-
obj_set_attr(&obj_buffer[0],
|
|
204
|
-
ATTR0_SQUARE,
|
|
205
|
-
ATTR1_SIZE_8,
|
|
206
|
-
ATTR2_PALBANK(0) | TILE_PLAYER);
|
|
207
|
-
obj_set_pos(&obj_buffer[0], (px >> 4) - camX, py >> 4);
|
|
208
|
-
oam_copy(oam_mem, obj_buffer, 1);
|
|
668
|
+
stage_sprites();
|
|
669
|
+
oam_copy(oam_mem, obj_buffer, 128); /* sprites AND affine slot 0 */
|
|
209
670
|
}
|
|
210
671
|
return 0;
|
|
211
672
|
}
|