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,56 +1,66 @@
|
|
|
1
|
-
/* ── shmup.c — Game Boy Advance
|
|
1
|
+
/* ── shmup.c — Game Boy Advance vertical shooter (complete example game) ─────
|
|
2
2
|
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* -
|
|
7
|
-
*
|
|
8
|
-
* -
|
|
3
|
+
* A COMPLETE, working game — title screen, score + persistent hi-score
|
|
4
|
+
* (cartridge SRAM), music + SFX, waves of enemies, and the GBA's signature
|
|
5
|
+
* hardware feature shown BOTH ways it exists:
|
|
6
|
+
* - an AFFINE BACKGROUND: the playfield backdrop is a vortex on BG2 that
|
|
7
|
+
* rotates and pulses (Mode 1, REG_BG2PA..PD matrix + BG2X/Y reference)
|
|
8
|
+
* - an AFFINE SPRITE: the wave boss is a 32x32 OBJ that spins and
|
|
9
|
+
* scale-pulses as it attacks (OAM affine parameter slot 0, double-size)
|
|
9
10
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
|
|
12
|
+
* very different one. The markers tell you what's what:
|
|
13
|
+
* HARDWARE IDIOM (load-bearing) — dodges a documented GBA footgun; reshape
|
|
14
|
+
* your gameplay around it (see TROUBLESHOOTING before changing).
|
|
15
|
+
* GAME LOGIC (clay) — enemy patterns, scoring, tuning, art: reshape freely.
|
|
15
16
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
17
|
+
* What depends on what:
|
|
18
|
+
* gba_sfx.{h,c} — PSG sound: sfx_tone/sfx_noise one-shots + the music loop
|
|
19
|
+
* (sfx_music_tick once per frame — forget it and the game is silent).
|
|
20
|
+
* libtonc (the build links it) — VBlankIntrWait/key_poll/OAM/TTE.
|
|
21
|
+
*
|
|
22
|
+
* HANDHELD, SO SINGLE-PLAYER ONLY (honest note): 2P on GBA means a link
|
|
23
|
+
* cable between two units — a second emulator instance this environment
|
|
24
|
+
* can't provide. Title is press-start, no mode select.
|
|
25
|
+
*
|
|
26
|
+
* Frame budget: ARM7TDMI at 16.78MHz with this object count (1+6+6+boss)
|
|
27
|
+
* doesn't come close to a full frame; the affine math is a handful of
|
|
28
|
+
* multiplies per frame, not per pixel — the PPU does the per-pixel work.
|
|
19
29
|
*/
|
|
20
30
|
|
|
21
31
|
#include <tonc.h>
|
|
22
32
|
#include "gba_sfx.h"
|
|
23
33
|
|
|
34
|
+
/* The title screen renders this — examples({op:'fork'}) stamps your game's
|
|
35
|
+
* name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
|
|
36
|
+
#define GAME_TITLE "GYRE GUNNER"
|
|
37
|
+
|
|
38
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
39
|
+
* Object pools — fixed slots, no allocation. Sprite slot discipline (128 OAM
|
|
40
|
+
* entries total, we use 14):
|
|
41
|
+
* slot 0 → player
|
|
42
|
+
* slot 1..6 → bullets
|
|
43
|
+
* slot 7..12 → enemies
|
|
44
|
+
* slot 13 → boss (AFFINE — uses OAM affine parameter slot 0; see the
|
|
45
|
+
* affine-sprite idiom below for why slot CHOICE matters)
|
|
46
|
+
*/
|
|
24
47
|
#define MAX_BULLETS 6
|
|
25
48
|
#define MAX_ENEMIES 6
|
|
49
|
+
#define SLOT_PLAYER 0
|
|
50
|
+
#define SLOT_BULLET 1
|
|
51
|
+
#define SLOT_ENEMY 7
|
|
52
|
+
#define SLOT_BOSS 13
|
|
26
53
|
|
|
27
|
-
#define TILE_BLANK 0
|
|
28
54
|
#define TILE_SHIP 1
|
|
29
55
|
#define TILE_BULLET 2
|
|
30
56
|
#define TILE_ENEMY 3
|
|
57
|
+
#define TILE_BOSS 16 /* 32x32 4bpp = 16 tiles, ids 16..31 */
|
|
31
58
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
* path that isn't wired in this build — it garbles the output AND wedges the
|
|
35
|
-
* game loop when called per-frame, GBA-1). We build the string ourselves and
|
|
36
|
-
* use tte_write, which processes the #{P:x,y} position command but does NO
|
|
37
|
-
* format conversion → safe every frame. */
|
|
38
|
-
static void draw_score(int x, unsigned v) {
|
|
39
|
-
char buf[24];
|
|
40
|
-
int i, n = 0;
|
|
41
|
-
/* "#{P:<x>,8}" position command, then 5 decimal digits. */
|
|
42
|
-
buf[n++]='#'; buf[n++]='{'; buf[n++]='P'; buf[n++]=':';
|
|
43
|
-
if (x >= 100) buf[n++] = '0' + (x/100)%10;
|
|
44
|
-
if (x >= 10) buf[n++] = '0' + (x/10)%10;
|
|
45
|
-
buf[n++] = '0' + x%10;
|
|
46
|
-
buf[n++]=','; buf[n++]='8'; buf[n++]='}';
|
|
47
|
-
for (i = 4; i >= 0; i--) { buf[n+i] = '0' + (v % 10); v /= 10; }
|
|
48
|
-
n += 5; buf[n] = 0;
|
|
49
|
-
tte_write(buf);
|
|
50
|
-
}
|
|
59
|
+
#define START_LIVES 3
|
|
60
|
+
#define WAVE_KILLS 10 /* kills before the wave boss appears */
|
|
51
61
|
|
|
52
|
-
/* 4bpp tiles (8 rows × 32 bits each = 32 bytes). Each nibble is a
|
|
53
|
-
* palette index. Index 0 = transparent. */
|
|
62
|
+
/* 4bpp sprite tiles (8 rows × 32 bits each = 32 bytes). Each nibble is a
|
|
63
|
+
* palette index within the sprite's palbank. Index 0 = transparent. */
|
|
54
64
|
static const u32 tile_ship[8] = {
|
|
55
65
|
0x00011000, 0x00011000, 0x00111100, 0x00111100,
|
|
56
66
|
0x01111110, 0x01111110, 0x11111111, 0x11000011,
|
|
@@ -64,202 +74,577 @@ static const u32 tile_enemy[8] = {
|
|
|
64
74
|
0x33333333, 0x03333330, 0x30000003, 0x03000030,
|
|
65
75
|
};
|
|
66
76
|
|
|
67
|
-
/* ── Starfield backdrop tiles (4bpp) ─────────────────────────────────
|
|
68
|
-
* Two space-coloured fill tiles (palette indices 4 and 5) laid in
|
|
69
|
-
* vertical bands so the whole BG is filled — NOT a flat blank backdrop.
|
|
70
|
-
* A handful of star pixels (index 6) are punched into each tile so the
|
|
71
|
-
* field reads as space rather than a solid block. */
|
|
72
|
-
static const u32 tile_star_a[8] = {
|
|
73
|
-
0x44464444, 0x44444444, 0x46444444, 0x44444644,
|
|
74
|
-
0x44444444, 0x64444444, 0x44444444, 0x44446444,
|
|
75
|
-
};
|
|
76
|
-
static const u32 tile_star_b[8] = {
|
|
77
|
-
0x55555555, 0x55655555, 0x55555555, 0x55555565,
|
|
78
|
-
0x65555555, 0x55555555, 0x55556555, 0x55555555,
|
|
79
|
-
};
|
|
80
|
-
|
|
81
77
|
typedef struct { s16 x, y; u16 alive; } Obj;
|
|
82
78
|
|
|
83
79
|
static OBJ_ATTR obj_buffer[128];
|
|
80
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
81
|
+
* OAM AFFINE SLOT LAYOUT. There is no separate affine-matrix memory: the 32
|
|
82
|
+
* OBJ_AFFINE parameter sets live INTERLEAVED inside OAM itself, in the
|
|
83
|
+
* 16-bit "fill" field of every OBJ_ATTR (4 sprites × 8 bytes carry one
|
|
84
|
+
* 8-byte matrix between them — pa in sprite 4n, pb in 4n+1, pc in 4n+2,
|
|
85
|
+
* pd in 4n+3). Casting the shadow-OAM buffer to OBJ_AFFINE* is the whole
|
|
86
|
+
* trick: obj_aff_buffer[k] aliases the fill words of sprites 4k..4k+3, and
|
|
87
|
+
* one oam_copy() of the full buffer commits sprites AND matrices together.
|
|
88
|
+
* Consequences you must respect:
|
|
89
|
+
* - oam_init() already set all 32 matrices to identity (pa=pd=0x0100).
|
|
90
|
+
* - NEVER memset OBJ_ATTRs to 0 — that zeroes the interleaved matrices
|
|
91
|
+
* (pa=0 means "scale by infinity": every affine sprite vanishes).
|
|
92
|
+
* - Matrix slot k is INDEPENDENT of which sprite uses it (attr1 AFF_ID
|
|
93
|
+
* picks any of the 32) — but the bytes live under sprites 4k..4k+3.
|
|
94
|
+
* requires: obj_buffer staged with oam_init(), committed with oam_copy(). */
|
|
95
|
+
static OBJ_AFFINE *const obj_aff_buffer = (OBJ_AFFINE *)obj_buffer;
|
|
84
96
|
|
|
85
97
|
static Obj player;
|
|
86
98
|
static Obj bullets[MAX_BULLETS];
|
|
87
99
|
static Obj enemies[MAX_ENEMIES];
|
|
88
|
-
static u16 score;
|
|
100
|
+
static u16 score, hiscore;
|
|
101
|
+
static u8 lives;
|
|
102
|
+
static u8 wave; /* 1-based; bumps each boss defeat */
|
|
103
|
+
static u8 kills; /* kills this wave (boss gate) */
|
|
89
104
|
static u16 spawn_timer;
|
|
105
|
+
static u16 frame; /* free-running frame counter (drives the vortex) */
|
|
90
106
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
107
|
+
/* Boss state — the affine sprite showcase. */
|
|
108
|
+
static u8 boss_active;
|
|
109
|
+
static s16 boss_x, boss_y; /* CENTER of the boss, in screen pixels */
|
|
110
|
+
static u8 boss_hp;
|
|
111
|
+
static u16 boss_theta; /* rotation angle: full circle = 0x10000 */
|
|
112
|
+
static u16 boss_pulse; /* scale-pulse phase */
|
|
113
|
+
|
|
114
|
+
/* Game states — the shell every example shares: title → play → game over. */
|
|
115
|
+
#define ST_TITLE 0
|
|
116
|
+
#define ST_PLAY 1
|
|
117
|
+
#define ST_OVER 2
|
|
118
|
+
static u8 state;
|
|
119
|
+
|
|
120
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
121
|
+
* PERSISTENT SRAM at 0x0E000000. Two footguns, both fatal-but-silent:
|
|
122
|
+
* 1. The SRAM bus is 8 BITS WIDE. Byte reads/writes only — a u16/u32
|
|
123
|
+
* access doesn't fault, it just reads the same byte mirrored (and a
|
|
124
|
+
* wide write stores one byte), so your data "almost" round-trips and
|
|
125
|
+
* then the checksum never matches. Every access below is via vu8.
|
|
126
|
+
* 2. Emulators and flashcarts detect the SAVE TYPE by scanning the ROM
|
|
127
|
+
* image for a marker string. Without "SRAM_V" in the ROM, mGBA gives
|
|
128
|
+
* the cart NO save memory at all and writes to 0x0E000000 vanish.
|
|
129
|
+
* The aligned, (used)-attributed const below plants that marker —
|
|
130
|
+
* delete it and persistence dies even though this code is untouched.
|
|
131
|
+
* Layout: 'V' 'X' score-lo score-hi checksum (xor ^ 0xA5) — magic+checksum
|
|
132
|
+
* so a fresh (0xFF-filled) cart reads as "no record" instead of garbage.
|
|
133
|
+
* requires: nothing else — self-contained; safe to transplant whole. */
|
|
134
|
+
#define SRAM_BYTE ((volatile u8 *)0x0E000000)
|
|
135
|
+
__attribute__((used, aligned(4))) static const char sram_type_marker[] = "SRAM_V113";
|
|
136
|
+
|
|
137
|
+
static u16 hiscore_load(void) {
|
|
138
|
+
u8 lo, hi;
|
|
139
|
+
if (SRAM_BYTE[0] != 'V' || SRAM_BYTE[1] != 'X') return 0;
|
|
140
|
+
lo = SRAM_BYTE[2];
|
|
141
|
+
hi = SRAM_BYTE[3];
|
|
142
|
+
if (SRAM_BYTE[4] != (u8)(lo ^ hi ^ 0xA5)) return 0;
|
|
143
|
+
return (u16)(lo | (hi << 8));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
static void hiscore_save(u16 v) {
|
|
147
|
+
SRAM_BYTE[0] = 'V';
|
|
148
|
+
SRAM_BYTE[1] = 'X';
|
|
149
|
+
SRAM_BYTE[2] = (u8)v;
|
|
150
|
+
SRAM_BYTE[3] = (u8)(v >> 8);
|
|
151
|
+
SRAM_BYTE[4] = (u8)((u8)v ^ (u8)(v >> 8) ^ 0xA5);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* ── GAME LOGIC (clay) — TTE text helpers ────────────────────────────────────
|
|
155
|
+
* Draw right-aligned decimal digits at pixel (x,y) WITHOUT tte_printf. The
|
|
156
|
+
* bundled libtonc's tte_printf with a %d conversion is broken (it routes
|
|
157
|
+
* through a vsnprintf path that isn't wired in this build — it garbles
|
|
158
|
+
* output AND wedges the loop when called per-frame, GBA-1). We build the
|
|
159
|
+
* string ourselves and use tte_write, which processes the #{P:x,y} position
|
|
160
|
+
* command but does NO format conversion → safe every frame. */
|
|
161
|
+
static void draw_num(int x, int y, unsigned v, int digits) {
|
|
162
|
+
char buf[24];
|
|
163
|
+
int i, n = 0;
|
|
164
|
+
buf[n++] = '#'; buf[n++] = '{'; buf[n++] = 'P'; buf[n++] = ':';
|
|
165
|
+
if (x >= 100) buf[n++] = (char)('0' + (x / 100) % 10);
|
|
166
|
+
if (x >= 10) buf[n++] = (char)('0' + (x / 10) % 10);
|
|
167
|
+
buf[n++] = (char)('0' + x % 10);
|
|
168
|
+
buf[n++] = ',';
|
|
169
|
+
if (y >= 100) buf[n++] = (char)('0' + (y / 100) % 10);
|
|
170
|
+
if (y >= 10) buf[n++] = (char)('0' + (y / 10) % 10);
|
|
171
|
+
buf[n++] = (char)('0' + y % 10);
|
|
172
|
+
buf[n++] = '}';
|
|
173
|
+
for (i = digits - 1; i >= 0; i--) { buf[n + i] = (char)('0' + (v % 10)); v /= 10; }
|
|
174
|
+
n += digits; buf[n] = 0;
|
|
175
|
+
tte_write(buf);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
179
|
+
* AFFINE BACKGROUND (BG2, Mode 1) — the GBA's "Mode 7": one background the
|
|
180
|
+
* PPU rotates/scales per frame for free. This block owns four matrix
|
|
181
|
+
* registers and one reference point:
|
|
182
|
+
*
|
|
183
|
+
* REG_BG2PA..PD — a 2x2 matrix in 8.8 FIXED POINT (256 == 1.0) that maps
|
|
184
|
+
* SCREEN pixels → TEXTURE pixels: tex = P · (screen - origin) + ref.
|
|
185
|
+
* Because it maps screen→texture (the INVERSE of "how is the image
|
|
186
|
+
* transformed"), a matrix that SAMPLES texture 2px per screen px makes
|
|
187
|
+
* the image look HALF size: bigger pa = smaller image. To zoom IN by z,
|
|
188
|
+
* write 1/z; to rotate the image one way, write the matrix of the other.
|
|
189
|
+
* REG_BG2X/Y — the texture point sampled at screen pixel (0,0), in 20.8
|
|
190
|
+
* fixed point. Without compensation the bg rotates around the screen's
|
|
191
|
+
* TOP-LEFT. To pivot around screen center (cx,cy)=(120,80) anchored at
|
|
192
|
+
* texture point (tx,ty): BG2X = (tx<<8) - (pa*cx + pb*cy) (same shape
|
|
193
|
+
* for Y with pc/pd) — i.e. "walk back from the anchor by half a screen
|
|
194
|
+
* through the matrix".
|
|
195
|
+
*
|
|
196
|
+
* The math, spelled out (libtonc's bg_aff_rotscale does the same):
|
|
197
|
+
* lu_sin/lu_cos take a u16 angle (full circle = 0x10000) and return 4.12
|
|
198
|
+
* fixed → >>4 converts to 8.8. For rotation θ and zoom z (8.8):
|
|
199
|
+
* inv = 65536/z (8.8 reciprocal: 1/z)
|
|
200
|
+
* pa = cos·inv>>8 pb = -sin·inv>>8
|
|
201
|
+
* pc = sin·inv>>8 pd = cos·inv>>8
|
|
202
|
+
*
|
|
203
|
+
* Footguns this block already dodges:
|
|
204
|
+
* - These 6 registers are WRITE-ONLY. You cannot read-modify-update;
|
|
205
|
+
* keep your angle/zoom in variables (boss_theta-style) and rewrite ALL
|
|
206
|
+
* of them every frame.
|
|
207
|
+
* - Affine BGs are ALWAYS 8bpp, and the map is 1 BYTE per tile (no flip
|
|
208
|
+
* bits, no palbank — plain tile index), unlike regular BGs' u16 entries.
|
|
209
|
+
* - VRAM IGNORES BYTE WRITES (a u8 store writes the byte TWICE into the
|
|
210
|
+
* 16-bit lane). Building tiles/map in a work-RAM staging buffer and
|
|
211
|
+
* tonccpy()ing them over is the idiom — tonccpy is VRAM-safe.
|
|
212
|
+
* - BG_WRAP makes the 256x256 texture tile forever; without it everything
|
|
213
|
+
* outside the map edge renders as tile 0.
|
|
214
|
+
* requires: DCNT_MODE1 (BG2 affine there), BG2CNT pointing CBB 1 / SBB 26,
|
|
215
|
+
* vortex_apply() called every frame, BG palette indices 224..228 (bank 14
|
|
216
|
+
* — bank 15 belongs to TTE; see the palette footgun at vortex_build). */
|
|
217
|
+
static void vortex_apply(u16 theta, u32 zoom_q8) {
|
|
218
|
+
s32 inv = (s32)(65536u / zoom_q8); /* 8.8 ── 1/zoom */
|
|
219
|
+
s32 cc = ((lu_cos(theta) >> 4) * inv) >> 8; /* 8.8 ── cosθ/zoom */
|
|
220
|
+
s32 ss = ((lu_sin(theta) >> 4) * inv) >> 8; /* 8.8 ── sinθ/zoom */
|
|
221
|
+
REG_BG2PA = (s16)cc; REG_BG2PB = (s16)-ss;
|
|
222
|
+
REG_BG2PC = (s16)ss; REG_BG2PD = (s16)cc;
|
|
223
|
+
/* Pivot: texture center (128,128) shows at screen center (120,80). */
|
|
224
|
+
REG_BG2X = (128 << 8) - (cc * 120 + (-ss) * 80);
|
|
225
|
+
REG_BG2Y = (128 << 8) - (ss * 120 + cc * 80);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/* ── GAME LOGIC (clay) — the vortex ART (the idiom above is the machinery;
|
|
229
|
+
* this is just what the texture looks like — replace at will).
|
|
230
|
+
* 8bpp tiles are 64 bytes, 1 byte per pixel, row-major. We stage 5 tiles +
|
|
231
|
+
* the 32x32 one-byte-per-entry map in work RAM, then tonccpy to VRAM
|
|
232
|
+
* (CBB 1 tiles, SBB 26 map) per the byte-write footgun above. The texture
|
|
233
|
+
* needs ANGULAR content (spiral arms) or rotation is invisible, and RADIAL
|
|
234
|
+
* content (rings) or the zoom pulse is invisible.
|
|
235
|
+
* PALETTE FOOTGUN: an 8bpp BG indexes the FULL 256-color BG palette, and
|
|
236
|
+
* tte_init_chr4c_default OWNS BANK 15 (indices 240-255: ink 241 = yellow,
|
|
237
|
+
* shadow 242 = orange). Park 8bpp art colors in bank 14 (224..) or your
|
|
238
|
+
* backdrop turns ink-yellow the moment TTE initialises. */
|
|
239
|
+
#define VC 224 /* vortex colors live at 224..228 — clear of TTE's bank 15 */
|
|
240
|
+
static void vortex_build(void) {
|
|
241
|
+
static u8 tiles[5][64];
|
|
242
|
+
static u8 vmap[1024];
|
|
243
|
+
int x, y, t;
|
|
244
|
+
|
|
245
|
+
pal_bg_mem[VC + 0] = RGB15(2, 2, 8); /* deep blue */
|
|
246
|
+
pal_bg_mem[VC + 1] = RGB15(4, 3, 12); /* indigo */
|
|
247
|
+
pal_bg_mem[VC + 2] = RGB15(8, 18, 26); /* cyan glow */
|
|
248
|
+
pal_bg_mem[VC + 3] = RGB15(13, 5, 22); /* violet */
|
|
249
|
+
pal_bg_mem[VC + 4] = RGB15(26, 28, 31); /* star white */
|
|
250
|
+
|
|
251
|
+
for (y = 0; y < 8; y++)
|
|
252
|
+
for (x = 0; x < 8; x++) {
|
|
253
|
+
tiles[0][y * 8 + x] = 0; /* void */
|
|
254
|
+
tiles[1][y * 8 + x] = (u8)(((x * 3 + y * 5) % 11) ? VC : VC + 1); /* band A */
|
|
255
|
+
tiles[2][y * 8 + x] = (u8)(((x + y * 3) % 9) ? VC + 1 : VC + 3); /* band B */
|
|
256
|
+
tiles[3][y * 8 + x] = (u8)(((x - 4) * (x - 4) + (y - 4) * (y - 4) < 9) ? VC + 2 : VC + 3); /* arm blob */
|
|
257
|
+
tiles[4][y * 8 + x] = (u8)((x == 4 || y == 4) && (x + y > 5 && x + y < 12) ? VC + 4 : VC); /* star */
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/* Map: concentric rings of bands A/B (radial content for the pulse)... */
|
|
261
|
+
for (y = 0; y < 32; y++)
|
|
262
|
+
for (x = 0; x < 32; x++) {
|
|
263
|
+
int dx = 2 * (x - 16) + 1, dy = 2 * (y - 16) + 1; /* center-ish */
|
|
264
|
+
int r2 = dx * dx + dy * dy; /* 2..2048 */
|
|
265
|
+
u8 tile = (u8)(((r2 >> 7) & 1) ? 1 : 2);
|
|
266
|
+
if (((x * 7 + y * 13) % 29) == 0) tile = 4; /* stars */
|
|
267
|
+
vmap[y * 32 + x] = tile;
|
|
268
|
+
}
|
|
269
|
+
/* ...plus two trailing spiral arms (angular content for the rotation). */
|
|
270
|
+
for (t = 0; t < 56; t++) {
|
|
271
|
+
u16 th = (u16)(t * 1400); /* ~1.2 turns over the arm */
|
|
272
|
+
s32 rq8 = 512 + t * 60; /* radius 2.0→15 tiles, 8.8 */
|
|
273
|
+
s32 ax = 16 + ((rq8 * (lu_cos(th) >> 4)) >> 16);
|
|
274
|
+
s32 ay = 16 + ((rq8 * (lu_sin(th) >> 4)) >> 16);
|
|
275
|
+
if (ax >= 0 && ax < 32 && ay >= 0 && ay < 32) vmap[ay * 32 + ax] = 3;
|
|
276
|
+
ax = 16 + ((rq8 * (lu_cos((u16)(th + 0x8000)) >> 4)) >> 16);
|
|
277
|
+
ay = 16 + ((rq8 * (lu_sin((u16)(th + 0x8000)) >> 4)) >> 16);
|
|
278
|
+
if (ax >= 0 && ax < 32 && ay >= 0 && ay < 32) vmap[ay * 32 + ax] = 3;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
tonccpy(&tile8_mem[1][0], tiles, sizeof(tiles)); /* tiles → charblock 1 */
|
|
282
|
+
tonccpy(se_mem[26], vmap, sizeof(vmap)); /* map → screenblock 26 */
|
|
283
|
+
REG_BG2CNT = BG_CBB(1) | BG_SBB(26) | BG_AFF_32x32 | BG_WRAP | BG_PRIO(3);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
287
|
+
* AFFINE SPRITE (the boss) — same 8.8 screen→texture matrix as the affine
|
|
288
|
+
* BG, but stored in OAM affine slot 0 (see the slot-layout idiom at
|
|
289
|
+
* obj_aff_buffer). Three OBJ-specific footguns this block dodges:
|
|
290
|
+
* 1. attr0 mode bits: ATTR0_AFF (01) turns affine ON; ATTR0_AFF_DBL (11)
|
|
291
|
+
* is affine + DOUBLE-SIZE. Without double-size the sprite is clipped
|
|
292
|
+
* to its original WxH box — a rotated 32x32 has its corners CUT OFF
|
|
293
|
+
* (≈29% of the diagonal) and a zoomed-up one is cropped to 32x32.
|
|
294
|
+
* Double-size renders into a 64x64 window so rotation/zoom≤2x fits.
|
|
295
|
+
* 2. Double-size MOVES THE SPRITE: attr0/attr1 x/y are the top-left of
|
|
296
|
+
* the RENDER WINDOW, so the visual center sits at (x+32, y+32) for a
|
|
297
|
+
* 32x32 sprite — position it by center and subtract 32 (a plain
|
|
298
|
+
* sprite would subtract 16). Forks that toggle DBL must re-anchor.
|
|
299
|
+
* 3. ATTR0_HIDE does NOT hide an affine sprite — mode bits 01/11 reuse
|
|
300
|
+
* the hide bit. To hide the boss, drop attr0 back to a REGULAR hidden
|
|
301
|
+
* object (ATTR0_HIDE alone), as boss_stage() does below.
|
|
302
|
+
* requires: OAM affine slot 0 free (sprites 0..3's fill words — fine here,
|
|
303
|
+
* they're regular objects whose fill is untouched), obj_buffer committed
|
|
304
|
+
* by oam_copy() every frame, boss tiles at OBJ tile 16 (4bpp 32x32, 1D). */
|
|
305
|
+
static void boss_stage(void) {
|
|
306
|
+
OBJ_ATTR *o = &obj_buffer[SLOT_BOSS];
|
|
307
|
+
if (!boss_active) {
|
|
308
|
+
o->attr0 = ATTR0_HIDE; /* REGULAR mode + hide (footgun 3) */
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
/* zoom pulse: 1.0 ± 0.45 from the sine LUT (4.12 → ±~115 in 8.8) */
|
|
312
|
+
u32 zoom = (u32)(256 + (lu_sin(boss_pulse) >> 5));
|
|
313
|
+
s32 inv = (s32)(65536u / zoom);
|
|
314
|
+
s32 cc = ((lu_cos(boss_theta) >> 4) * inv) >> 8;
|
|
315
|
+
s32 ss = ((lu_sin(boss_theta) >> 4) * inv) >> 8;
|
|
316
|
+
obj_aff_buffer[0].pa = (s16)cc; obj_aff_buffer[0].pb = (s16)-ss;
|
|
317
|
+
obj_aff_buffer[0].pc = (s16)ss; obj_aff_buffer[0].pd = (s16)cc;
|
|
318
|
+
|
|
319
|
+
o->attr0 = (u16)(ATTR0_AFF_DBL | ATTR0_SQUARE | ATTR0_4BPP
|
|
320
|
+
| ((boss_y - 32) & 0x00FF)); /* window top */
|
|
321
|
+
o->attr1 = (u16)(ATTR1_SIZE_32 | ATTR1_AFF_ID(0)
|
|
322
|
+
| ((boss_x - 32) & 0x01FF)); /* window left */
|
|
323
|
+
o->attr2 = (u16)(ATTR2_PALBANK(4) | TILE_BOSS);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/* ── GAME LOGIC (clay) — boss ART: a spiked disc with ONE cyan spike (the
|
|
327
|
+
* asymmetry makes the spin readable; a symmetric disc looks static).
|
|
328
|
+
* Drawn procedurally into a 32x32 4bpp staging buffer laid out exactly as
|
|
329
|
+
* OBJ VRAM wants it in 1D mapping: 16 consecutive 8x8 tiles, row-major
|
|
330
|
+
* within the sprite, 2 pixels per byte (low nibble = left pixel). */
|
|
331
|
+
static void boss_build_tiles(void) {
|
|
332
|
+
static u32 tiles[16][8];
|
|
333
|
+
int x, y;
|
|
334
|
+
for (y = 0; y < 32; y++)
|
|
335
|
+
for (x = 0; x < 32; x++) {
|
|
336
|
+
int dx = x - 16, dy = y - 16;
|
|
337
|
+
int r2 = dx * dx + dy * dy;
|
|
338
|
+
int c = 0;
|
|
339
|
+
if (r2 < 16) c = 3; /* core */
|
|
340
|
+
else if (r2 < 100) c = (r2 >= 49 && r2 < 81) ? 2 : 1; /* body+ring */
|
|
341
|
+
if (dy >= -2 && dy <= 2 && dx > 8 && dx < 16) c = 4; /* CYAN spike → */
|
|
342
|
+
if (dy >= -2 && dy <= 2 && dx < -8 && dx > -16) c = 2;
|
|
343
|
+
if (dx >= -2 && dx <= 2 && (dy > 8 ? dy < 16 : dy > -16 && dy < -8)) c = 2;
|
|
344
|
+
if (c) {
|
|
345
|
+
int t = (y / 8) * 4 + (x / 8);
|
|
346
|
+
tiles[t][y % 8] |= (u32)c << (4 * (x % 8));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
tonccpy(&tile_mem[4][TILE_BOSS], tiles, sizeof(tiles));
|
|
350
|
+
pal_obj_bank[4][1] = RGB15(16, 6, 26); /* violet body */
|
|
351
|
+
pal_obj_bank[4][2] = RGB15(28, 10, 8); /* ember spikes */
|
|
352
|
+
pal_obj_bank[4][3] = RGB15(31, 30, 24); /* hot core */
|
|
353
|
+
pal_obj_bank[4][4] = RGB15(8, 30, 30); /* THE cyan spike (spin marker) */
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/* ── GAME LOGIC (clay — reshape freely) ─────────────────────────────────── */
|
|
357
|
+
static u8 rng_state = 0xA5;
|
|
358
|
+
static u8 rand8(void) { /* Galois LFSR, period 255 */
|
|
359
|
+
u8 lsb = (u8)(rng_state & 1);
|
|
360
|
+
rng_state >>= 1;
|
|
361
|
+
if (lsb) rng_state ^= 0xB8;
|
|
362
|
+
return rng_state;
|
|
94
363
|
}
|
|
95
364
|
|
|
96
365
|
static void fire_bullet(void) {
|
|
97
|
-
|
|
366
|
+
int i;
|
|
367
|
+
for (i = 0; i < MAX_BULLETS; i++)
|
|
98
368
|
if (!bullets[i].alive) {
|
|
99
369
|
bullets[i].x = player.x;
|
|
100
370
|
bullets[i].y = player.y - 8;
|
|
101
371
|
bullets[i].alive = 1;
|
|
372
|
+
sfx_tone(1, 1900, 4); /* pew (ch1; music owns ch2) */
|
|
102
373
|
return;
|
|
103
374
|
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/* Galois LFSR (taps $B8), period 255 -- real per-spawn randomness.
|
|
108
|
-
* The old code derived the spawn column from spawn_timer, but the caller
|
|
109
|
-
* resets spawn_timer just before calling here, so it was CONSTANT and
|
|
110
|
-
* every enemy spawned in the same left column/lane. */
|
|
111
|
-
static u8 rng_state = 0xA5;
|
|
112
|
-
static u8 rand8(void) {
|
|
113
|
-
u8 lsb = (u8)(rng_state & 1);
|
|
114
|
-
rng_state >>= 1;
|
|
115
|
-
if (lsb) rng_state ^= 0xB8;
|
|
116
|
-
return rng_state;
|
|
117
375
|
}
|
|
118
376
|
|
|
119
377
|
static void spawn_enemy(void) {
|
|
120
|
-
|
|
378
|
+
int i;
|
|
379
|
+
for (i = 0; i < MAX_ENEMIES; i++)
|
|
121
380
|
if (!enemies[i].alive) {
|
|
122
|
-
/* cheap deterministic x scatter */
|
|
123
381
|
enemies[i].x = rand8() % (240 - 16) + 8;
|
|
124
382
|
enemies[i].y = -8;
|
|
125
383
|
enemies[i].alive = 1;
|
|
126
384
|
return;
|
|
127
385
|
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
static int aabb_hit(const Obj *a, const Obj *b) {
|
|
389
|
+
return (a->x < b->x + 8) && (a->x + 8 > b->x)
|
|
390
|
+
&& (a->y < b->y + 8) && (a->y + 8 > b->y);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/* ── GAME LOGIC (clay) — HUD / screens (TTE on BG1, priority 0) ── */
|
|
394
|
+
static void draw_hud_labels(void) {
|
|
395
|
+
tte_erase_screen();
|
|
396
|
+
tte_write("#{P:8,4}SC");
|
|
397
|
+
tte_write("#{P:96,4}HI");
|
|
398
|
+
tte_write("#{P:168,4}W");
|
|
399
|
+
tte_write("#{P:208,4}x");
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
static void draw_hud_numbers(void) {
|
|
403
|
+
tte_erase_rect(28, 4, 70, 12); draw_num(28, 4, score, 5);
|
|
404
|
+
tte_erase_rect(116, 4, 158, 12); draw_num(116, 4, hiscore, 5);
|
|
405
|
+
tte_erase_rect(178, 4, 196, 12); draw_num(178, 4, wave, 2);
|
|
406
|
+
tte_erase_rect(218, 4, 228, 12); draw_num(218, 4, lives, 1);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
static void enter_title(void) {
|
|
410
|
+
state = ST_TITLE;
|
|
411
|
+
tte_erase_screen();
|
|
412
|
+
tte_write("#{P:60,40}" GAME_TITLE);
|
|
413
|
+
tte_write("#{P:76,80}PRESS START");
|
|
414
|
+
tte_write("#{P:88,100}HI");
|
|
415
|
+
draw_num(112, 100, hiscore, 5);
|
|
416
|
+
tte_write("#{P:48,128}DPAD MOVE - A FIRE");
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
static void enter_play(void) {
|
|
420
|
+
int i;
|
|
421
|
+
state = ST_PLAY;
|
|
422
|
+
player.x = 116; player.y = 130; player.alive = 1;
|
|
423
|
+
for (i = 0; i < MAX_BULLETS; i++) bullets[i].alive = 0;
|
|
424
|
+
for (i = 0; i < MAX_ENEMIES; i++) enemies[i].alive = 0;
|
|
425
|
+
score = 0; lives = START_LIVES; wave = 1; kills = 0;
|
|
426
|
+
spawn_timer = 0;
|
|
427
|
+
boss_active = 0;
|
|
428
|
+
draw_hud_labels();
|
|
429
|
+
draw_hud_numbers();
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
static void enter_over(void) {
|
|
433
|
+
state = ST_OVER;
|
|
434
|
+
if (score > hiscore) {
|
|
435
|
+
hiscore = score;
|
|
436
|
+
hiscore_save(hiscore); /* byte-wise SRAM write — see the idiom */
|
|
437
|
+
draw_hud_numbers();
|
|
128
438
|
}
|
|
439
|
+
tte_write("#{P:84,64}GAME OVER");
|
|
440
|
+
tte_write("#{P:76,84}PRESS START");
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
static void boss_enter(void) {
|
|
444
|
+
boss_active = 1;
|
|
445
|
+
boss_x = 120; boss_y = -20; /* descends into view */
|
|
446
|
+
boss_hp = (u8)(6 + wave * 2);
|
|
447
|
+
if (boss_hp > 20) boss_hp = 20;
|
|
448
|
+
boss_theta = 0; boss_pulse = 0;
|
|
449
|
+
sfx_noise(30);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
static void boss_defeat(void) {
|
|
453
|
+
boss_active = 0;
|
|
454
|
+
if (score < 65000u) score += 250;
|
|
455
|
+
wave++; kills = 0;
|
|
456
|
+
draw_hud_numbers();
|
|
457
|
+
sfx_noise(24);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
static void lose_life(void) {
|
|
461
|
+
sfx_noise(12);
|
|
462
|
+
if (lives > 0) lives--;
|
|
463
|
+
draw_hud_numbers();
|
|
464
|
+
player.x = 116; player.y = 130;
|
|
465
|
+
if (lives == 0) enter_over();
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/* ── GAME LOGIC (clay) — one ST_PLAY tick ── */
|
|
469
|
+
static void update_play(void) {
|
|
470
|
+
int i, j;
|
|
471
|
+
|
|
472
|
+
if (key_held(KEY_LEFT) && player.x > 8) player.x -= 2;
|
|
473
|
+
if (key_held(KEY_RIGHT) && player.x < 240 - 16) player.x += 2;
|
|
474
|
+
if (key_held(KEY_UP) && player.y > 20) player.y -= 2;
|
|
475
|
+
if (key_held(KEY_DOWN) && player.y < 160 - 16) player.y += 2;
|
|
476
|
+
if (key_hit(KEY_A)) fire_bullet();
|
|
477
|
+
|
|
478
|
+
for (i = 0; i < MAX_BULLETS; i++) {
|
|
479
|
+
if (!bullets[i].alive) continue;
|
|
480
|
+
bullets[i].y -= 4;
|
|
481
|
+
if (bullets[i].y < -8) bullets[i].alive = 0;
|
|
482
|
+
}
|
|
483
|
+
for (i = 0; i < MAX_ENEMIES; i++) {
|
|
484
|
+
if (!enemies[i].alive) continue;
|
|
485
|
+
enemies[i].y += 1 + (wave >> 2);
|
|
486
|
+
if (enemies[i].y > 160) enemies[i].alive = 0;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/* Spawner: steady waves; during a boss the boss is the spawner. */
|
|
490
|
+
if (!boss_active) {
|
|
491
|
+
u16 period = (u16)(28 > 12 + wave * 2 ? 28 - wave * 2 : 12);
|
|
492
|
+
if (++spawn_timer >= period && kills < WAVE_KILLS) { spawn_timer = 0; spawn_enemy(); }
|
|
493
|
+
if (kills >= WAVE_KILLS) {
|
|
494
|
+
u8 field_clear = 1;
|
|
495
|
+
for (i = 0; i < MAX_ENEMIES; i++) if (enemies[i].alive) field_clear = 0;
|
|
496
|
+
if (field_clear) boss_enter();
|
|
497
|
+
}
|
|
498
|
+
} else {
|
|
499
|
+
/* Boss attack pattern: spin faster than the backdrop, pulse scale,
|
|
500
|
+
* strafe a sine path while drifting down, shed minions. */
|
|
501
|
+
boss_theta = (u16)(boss_theta + 0x0140); /* ~1.8°/frame */
|
|
502
|
+
boss_pulse = (u16)(boss_pulse + 0x0120);
|
|
503
|
+
if (boss_y < 56) boss_y++; /* entrance dive */
|
|
504
|
+
else boss_x = (s16)(120 + ((76 * lu_sin((u16)(frame << 7))) >> 12));
|
|
505
|
+
if (++spawn_timer >= 90) { spawn_timer = 0; spawn_enemy(); }
|
|
506
|
+
|
|
507
|
+
/* Bullets vs boss: 28x28 box around the boss CENTER. Collision is
|
|
508
|
+
* the UNROTATED box on purpose — honest simplification; rotating
|
|
509
|
+
* hitboxes buys little for a round boss. */
|
|
510
|
+
for (i = 0; i < MAX_BULLETS; i++) {
|
|
511
|
+
if (!bullets[i].alive) continue;
|
|
512
|
+
if (bullets[i].x + 4 > boss_x - 14 && bullets[i].x + 4 < boss_x + 14 &&
|
|
513
|
+
bullets[i].y + 4 > boss_y - 14 && bullets[i].y + 4 < boss_y + 14) {
|
|
514
|
+
bullets[i].alive = 0;
|
|
515
|
+
sfx_tone(1, 900, 3);
|
|
516
|
+
if (--boss_hp == 0) { boss_defeat(); break; }
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
/* Boss vs player (same box vs the 8x8 ship). */
|
|
520
|
+
if (boss_active &&
|
|
521
|
+
player.x + 8 > boss_x - 14 && player.x < boss_x + 14 &&
|
|
522
|
+
player.y + 8 > boss_y - 14 && player.y < boss_y + 14) {
|
|
523
|
+
lose_life();
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/* Bullets vs enemies. */
|
|
528
|
+
for (i = 0; i < MAX_BULLETS; i++) {
|
|
529
|
+
if (!bullets[i].alive) continue;
|
|
530
|
+
for (j = 0; j < MAX_ENEMIES; j++) {
|
|
531
|
+
if (!enemies[j].alive) continue;
|
|
532
|
+
if (aabb_hit(&bullets[i], &enemies[j])) {
|
|
533
|
+
bullets[i].alive = 0;
|
|
534
|
+
enemies[j].alive = 0;
|
|
535
|
+
if (score < 65000u) score += 10;
|
|
536
|
+
kills++;
|
|
537
|
+
sfx_noise(6);
|
|
538
|
+
draw_hud_numbers();
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
/* Enemies vs player. */
|
|
544
|
+
for (j = 0; j < MAX_ENEMIES && state == ST_PLAY; j++) {
|
|
545
|
+
if (!enemies[j].alive) continue;
|
|
546
|
+
if (aabb_hit(&enemies[j], &player)) {
|
|
547
|
+
enemies[j].alive = 0;
|
|
548
|
+
lose_life();
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/* ── GAME LOGIC (clay) — stage the regular sprites (boss has its own idiom
|
|
554
|
+
* block). Inactive slots park offscreen (y=200) instead of HIDE so the loop
|
|
555
|
+
* stays branch-light; either works for REGULAR sprites. ── */
|
|
556
|
+
static void stage_sprites(void) {
|
|
557
|
+
int i;
|
|
558
|
+
int px = (state == ST_PLAY) ? player.x : 250, py = (state == ST_PLAY) ? player.y : 200;
|
|
559
|
+
obj_set_attr(&obj_buffer[SLOT_PLAYER], ATTR0_SQUARE, ATTR1_SIZE_8,
|
|
560
|
+
ATTR2_PALBANK(0) | TILE_SHIP);
|
|
561
|
+
obj_set_pos(&obj_buffer[SLOT_PLAYER], px, py);
|
|
562
|
+
for (i = 0; i < MAX_BULLETS; i++) {
|
|
563
|
+
obj_set_attr(&obj_buffer[SLOT_BULLET + i], ATTR0_SQUARE, ATTR1_SIZE_8,
|
|
564
|
+
ATTR2_PALBANK(1) | TILE_BULLET);
|
|
565
|
+
obj_set_pos(&obj_buffer[SLOT_BULLET + i], bullets[i].x,
|
|
566
|
+
(state == ST_PLAY && bullets[i].alive) ? bullets[i].y : 200);
|
|
567
|
+
}
|
|
568
|
+
for (i = 0; i < MAX_ENEMIES; i++) {
|
|
569
|
+
obj_set_attr(&obj_buffer[SLOT_ENEMY + i], ATTR0_SQUARE, ATTR1_SIZE_8,
|
|
570
|
+
ATTR2_PALBANK(2) | TILE_ENEMY);
|
|
571
|
+
obj_set_pos(&obj_buffer[SLOT_ENEMY + i], enemies[i].x,
|
|
572
|
+
(state == ST_PLAY && enemies[i].alive) ? enemies[i].y : 200);
|
|
573
|
+
}
|
|
574
|
+
boss_stage();
|
|
129
575
|
}
|
|
130
576
|
|
|
131
577
|
int main(void) {
|
|
132
|
-
/* ──
|
|
578
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
579
|
+
* Init order: tiles/palettes → oam_init → irq_init + II_VBLANK →
|
|
580
|
+
* TTE init → DISPCNT last. VBlankIntrWait() HANGS FOREVER without the
|
|
581
|
+
* vblank IRQ registered (the #1 "frozen on frame 1" cause), and
|
|
582
|
+
* enabling DISPCNT layers before their tiles/maps exist flashes
|
|
583
|
+
* garbage. TTE owns BG1 (CBB 2 / SBB 30) — keep other layers off
|
|
584
|
+
* those blocks. requires: nothing prior; this IS the boot. */
|
|
133
585
|
tonccpy(&tile_mem[4][TILE_SHIP], tile_ship, sizeof(tile_ship));
|
|
134
586
|
tonccpy(&tile_mem[4][TILE_BULLET], tile_bullet, sizeof(tile_bullet));
|
|
135
587
|
tonccpy(&tile_mem[4][TILE_ENEMY], tile_enemy, sizeof(tile_enemy));
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
pal_obj_bank[
|
|
140
|
-
pal_obj_bank[1][2] = CLR_YELLOW; /* bullets */
|
|
141
|
-
pal_obj_bank[2][3] = CLR_RED; /* enemies */
|
|
142
|
-
|
|
143
|
-
/* ── Starfield backdrop on BG0 ───────────────────────────────────
|
|
144
|
-
* Fill the whole screen with a banded space backdrop + scattered
|
|
145
|
-
* stars so the playfield doesn't read as a blank black screen. BG
|
|
146
|
-
* palette indices 4/5 = the two space bands, 6 = star colour. Tile
|
|
147
|
-
* data → char-block 0, map → screen-block 28 (clear of TTE on
|
|
148
|
-
* char-block 2 / screen-block 30). BG0 sits at the lowest priority
|
|
149
|
-
* so the ship/bullets/enemies draw in front of it. */
|
|
588
|
+
boss_build_tiles();
|
|
589
|
+
pal_obj_bank[0][1] = CLR_WHITE; /* ship */
|
|
590
|
+
pal_obj_bank[1][2] = CLR_YELLOW; /* bullets */
|
|
591
|
+
pal_obj_bank[2][3] = CLR_RED; /* enemies */
|
|
150
592
|
pal_bg_mem[0] = CLR_BLACK;
|
|
151
|
-
pal_bg_mem[4] = RGB15(1, 2, 7); /* dark space band */
|
|
152
|
-
pal_bg_mem[5] = RGB15(2, 3, 10); /* lighter band */
|
|
153
|
-
pal_bg_mem[6] = RGB15(28, 28, 31); /* stars */
|
|
154
|
-
tonccpy(&tile_mem[0][4], tile_star_a, sizeof(tile_star_a));
|
|
155
|
-
tonccpy(&tile_mem[0][5], tile_star_b, sizeof(tile_star_b));
|
|
156
|
-
REG_BG0CNT = BG_CBB(0) | BG_SBB(28) | BG_REG_32x32 | BG_4BPP | BG_PRIO(3);
|
|
157
|
-
{
|
|
158
|
-
SCR_ENTRY *map = se_mem[28];
|
|
159
|
-
for (int ty = 0; ty < 32; ty++)
|
|
160
|
-
for (int tx = 0; tx < 32; tx++)
|
|
161
|
-
map[ty * 32 + tx] = SE_BUILD(4 + ((ty >> 1) & 1), 0, 0, 0);
|
|
162
|
-
}
|
|
163
593
|
|
|
164
|
-
|
|
594
|
+
vortex_build(); /* affine BG2: tiles+map+BG2CNT */
|
|
595
|
+
oam_init(obj_buffer, 128); /* hides all 128, matrices = identity */
|
|
165
596
|
|
|
166
|
-
/* IRQ setup — required for VBlankIntrWait() to function. */
|
|
167
597
|
irq_init(NULL);
|
|
168
598
|
irq_add(II_VBLANK, NULL);
|
|
169
599
|
|
|
170
|
-
sfx_init();
|
|
171
|
-
|
|
172
|
-
player.x = 116; player.y = 130; player.alive = 1;
|
|
173
|
-
for (int i = 0; i < MAX_BULLETS; i++) bullets[i].alive = 0;
|
|
174
|
-
for (int i = 0; i < MAX_ENEMIES; i++) enemies[i].alive = 0;
|
|
175
|
-
score = 0;
|
|
176
|
-
spawn_timer = 0;
|
|
600
|
+
sfx_init(); /* APU on; music loop ticks below */
|
|
177
601
|
|
|
178
|
-
/* TTE on BG1
|
|
179
|
-
*
|
|
180
|
-
* dependency. BG1 priority 0 → score/hint text in front. */
|
|
602
|
+
/* TTE text on BG1 (4bpp char block 2, screenblock 30), priority 0 so
|
|
603
|
+
* text draws over everything. Mode 1 = BG0/BG1 regular, BG2 AFFINE. */
|
|
181
604
|
tte_init_chr4c_default(1, BG_CBB(2) | BG_SBB(30));
|
|
182
605
|
REG_BG1CNT |= BG_PRIO(0);
|
|
183
|
-
REG_DISPCNT =
|
|
184
|
-
tte_write("#{P:8,8}SCORE 00000");
|
|
185
|
-
tte_write("#{P:8,144}D-PAD MOVE A FIRE");
|
|
606
|
+
REG_DISPCNT = DCNT_MODE1 | DCNT_BG1 | DCNT_BG2 | DCNT_OBJ | DCNT_OBJ_1D;
|
|
186
607
|
|
|
187
|
-
|
|
608
|
+
hiscore = hiscore_load(); /* cartridge SRAM — 0 on first boot */
|
|
609
|
+
enter_title();
|
|
188
610
|
|
|
189
611
|
while (1) {
|
|
612
|
+
/* Idiomatic Tonc heartbeat: wait vblank, poll keys, update, then
|
|
613
|
+
* commit OAM + affine registers while still inside vblank (the
|
|
614
|
+
* whole update is far quicker than the 4.9ms vblank window). */
|
|
190
615
|
VBlankIntrWait();
|
|
191
616
|
key_poll();
|
|
617
|
+
sfx_music_tick(); /* forget this → silent game */
|
|
618
|
+
frame++;
|
|
192
619
|
|
|
193
|
-
if (
|
|
194
|
-
|
|
195
|
-
if (
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if ((now & KEY_A) && !(prev & KEY_A)) {
|
|
200
|
-
fire_bullet();
|
|
201
|
-
sfx_tone(2, 1900, 4); /* pew */
|
|
202
|
-
}
|
|
203
|
-
prev = now;
|
|
204
|
-
|
|
205
|
-
for (int i = 0; i < MAX_BULLETS; i++) {
|
|
206
|
-
if (!bullets[i].alive) continue;
|
|
207
|
-
bullets[i].y -= 4;
|
|
208
|
-
if (bullets[i].y < -8) bullets[i].alive = 0;
|
|
209
|
-
}
|
|
210
|
-
for (int i = 0; i < MAX_ENEMIES; i++) {
|
|
211
|
-
if (!enemies[i].alive) continue;
|
|
212
|
-
enemies[i].y += 1;
|
|
213
|
-
if (enemies[i].y > 160) enemies[i].alive = 0;
|
|
620
|
+
if (state == ST_TITLE) {
|
|
621
|
+
if (key_hit(KEY_START | KEY_A)) enter_play();
|
|
622
|
+
} else if (state == ST_OVER) {
|
|
623
|
+
if (key_hit(KEY_START)) enter_title();
|
|
624
|
+
} else {
|
|
625
|
+
update_play();
|
|
214
626
|
}
|
|
215
|
-
if (++spawn_timer >= 28) { spawn_timer = 0; spawn_enemy(); }
|
|
216
627
|
|
|
217
|
-
/*
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
628
|
+
/* The vortex breathes with the game: gentle on the title, driving
|
|
629
|
+
* during play, frantic while the boss is up. (Affine BG idiom —
|
|
630
|
+
* rewrite ALL the write-only registers every frame.) */
|
|
631
|
+
{
|
|
632
|
+
u16 vth; u32 vzoom;
|
|
633
|
+
if (state == ST_PLAY && boss_active) {
|
|
634
|
+
vth = (u16)(frame * 0x00C0);
|
|
635
|
+
vzoom = (u32)(256 + (lu_sin((u16)(frame * 0x0180)) >> 5));
|
|
636
|
+
} else if (state == ST_PLAY) {
|
|
637
|
+
vth = (u16)(frame * 0x0050);
|
|
638
|
+
vzoom = (u32)(256 + (lu_sin((u16)(frame * 0x0060)) >> 6));
|
|
639
|
+
} else {
|
|
640
|
+
vth = (u16)(frame * 0x0030);
|
|
641
|
+
vzoom = (u32)(256 + (lu_sin((u16)(frame * 0x0040)) >> 6));
|
|
229
642
|
}
|
|
643
|
+
vortex_apply(vth, vzoom);
|
|
230
644
|
}
|
|
231
645
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
* Hide inactive slots by parking them offscreen (y = 160). */
|
|
235
|
-
obj_set_attr(&obj_buffer[0],
|
|
236
|
-
ATTR0_SQUARE,
|
|
237
|
-
ATTR1_SIZE_8,
|
|
238
|
-
ATTR2_PALBANK(0) | TILE_SHIP);
|
|
239
|
-
obj_set_pos(&obj_buffer[0], player.x, player.y);
|
|
240
|
-
|
|
241
|
-
for (int i = 0; i < MAX_BULLETS; i++) {
|
|
242
|
-
int by = bullets[i].alive ? bullets[i].y : 200;
|
|
243
|
-
obj_set_attr(&obj_buffer[1 + i],
|
|
244
|
-
ATTR0_SQUARE,
|
|
245
|
-
ATTR1_SIZE_8,
|
|
246
|
-
ATTR2_PALBANK(1) | TILE_BULLET);
|
|
247
|
-
obj_set_pos(&obj_buffer[1 + i], bullets[i].x, by);
|
|
248
|
-
}
|
|
249
|
-
for (int i = 0; i < MAX_ENEMIES; i++) {
|
|
250
|
-
int ey = enemies[i].alive ? enemies[i].y : 200;
|
|
251
|
-
obj_set_attr(&obj_buffer[7 + i],
|
|
252
|
-
ATTR0_SQUARE,
|
|
253
|
-
ATTR1_SIZE_8,
|
|
254
|
-
ATTR2_PALBANK(2) | TILE_ENEMY);
|
|
255
|
-
obj_set_pos(&obj_buffer[7 + i], enemies[i].x, ey);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
oam_copy(oam_mem, obj_buffer, 128);
|
|
259
|
-
|
|
260
|
-
/* Score: 5 digits via draw_score (NOT tte_printf — see GBA-1). */
|
|
261
|
-
tte_erase_rect(8 + 6*8, 8, 8 + 11*8, 16);
|
|
262
|
-
draw_score(8 + 6*8, score);
|
|
646
|
+
stage_sprites();
|
|
647
|
+
oam_copy(oam_mem, obj_buffer, 128); /* sprites AND affine slot 0 */
|
|
263
648
|
}
|
|
264
649
|
return 0;
|
|
265
650
|
}
|