romdevtools 0.28.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +53 -43
- package/CHANGELOG.md +91 -0
- package/README.md +3 -3
- package/examples/README.md +7 -7
- package/examples/atari2600/templates/platformer.asm +1225 -332
- package/examples/atari2600/templates/puzzle.asm +1056 -0
- package/examples/atari2600/templates/racing.asm +906 -275
- package/examples/atari2600/templates/shmup.asm +1031 -239
- package/examples/atari2600/templates/sports.asm +1135 -253
- package/examples/atari7800/templates/platformer.c +991 -156
- package/examples/atari7800/templates/puzzle.c +1091 -148
- package/examples/atari7800/templates/racing.c +952 -124
- package/examples/atari7800/templates/shmup.c +812 -134
- package/examples/atari7800/templates/sports.c +820 -184
- package/examples/c64/templates/platformer.c +879 -164
- package/examples/c64/templates/puzzle.c +855 -178
- package/examples/c64/templates/racing.c +873 -97
- package/examples/c64/templates/shmup.c +757 -161
- package/examples/c64/templates/sports.c +755 -100
- package/examples/gb/templates/platformer.c +841 -179
- package/examples/gb/templates/puzzle.c +986 -246
- package/examples/gb/templates/racing.c +754 -174
- package/examples/gb/templates/shmup.c +673 -175
- package/examples/gb/templates/sports.c +790 -159
- package/examples/gba/templates/platformer.c +626 -165
- package/examples/gba/templates/puzzle.c +519 -269
- package/examples/gba/templates/racing.c +511 -206
- package/examples/gba/templates/shmup.c +564 -179
- package/examples/gba/templates/sports.c +454 -174
- package/examples/gbc/templates/platformer.c +944 -180
- package/examples/gbc/templates/puzzle.c +363 -109
- package/examples/gbc/templates/racing.c +884 -180
- package/examples/gbc/templates/shmup.c +821 -185
- package/examples/gbc/templates/sports.c +870 -162
- package/examples/genesis/templates/platformer.c +747 -129
- package/examples/genesis/templates/puzzle.c +694 -261
- package/examples/genesis/templates/racing.c +726 -203
- package/examples/genesis/templates/shmup.c +535 -142
- package/examples/genesis/templates/sports.c +495 -158
- package/examples/gg/templates/platformer.c +880 -215
- package/examples/gg/templates/puzzle.c +875 -216
- package/examples/gg/templates/racing.c +915 -172
- package/examples/gg/templates/shmup.c +714 -191
- package/examples/gg/templates/sports.c +732 -129
- package/examples/lynx/templates/platformer.c +604 -69
- package/examples/lynx/templates/puzzle.c +498 -158
- package/examples/lynx/templates/racing.c +538 -102
- package/examples/lynx/templates/shmup.c +458 -131
- package/examples/lynx/templates/sports.c +496 -72
- package/examples/msx/platformer/main.c +649 -162
- package/examples/msx/puzzle/main.c +742 -240
- package/examples/msx/racing/main.c +669 -178
- package/examples/msx/shmup/main.c +460 -178
- package/examples/msx/sports/main.c +592 -126
- package/examples/nes/templates/platformer.c +589 -171
- package/examples/nes/templates/puzzle.c +563 -242
- package/examples/nes/templates/racing.c +502 -208
- package/examples/nes/templates/shmup.c +339 -145
- package/examples/nes/templates/sports.c +341 -183
- package/examples/pce/platformer/main.c +874 -205
- package/examples/pce/puzzle/main.c +802 -287
- package/examples/pce/racing/main.c +783 -208
- package/examples/pce/shmup/main.c +638 -212
- package/examples/pce/sports/main.c +586 -169
- package/examples/porting-across-platforms/README.md +1 -1
- package/examples/sms/templates/platformer.c +762 -177
- package/examples/sms/templates/puzzle.c +752 -212
- package/examples/sms/templates/racing.c +808 -145
- package/examples/sms/templates/shmup.c +599 -162
- package/examples/sms/templates/sports.c +630 -122
- package/examples/snes/templates/music_demo.c +7 -0
- package/examples/snes/templates/platformer-data.asm +123 -24
- package/examples/snes/templates/platformer-hdr.asm +57 -0
- package/examples/snes/templates/platformer.c +586 -165
- package/examples/snes/templates/puzzle-data.asm +116 -21
- package/examples/snes/templates/puzzle-hdr.asm +57 -0
- package/examples/snes/templates/puzzle.c +614 -235
- package/examples/snes/templates/racing-data.asm +390 -32
- package/examples/snes/templates/racing-hdr.asm +57 -0
- package/examples/snes/templates/racing.c +807 -196
- package/examples/snes/templates/shmup-data.asm +87 -29
- package/examples/snes/templates/shmup-hdr.asm +57 -0
- package/examples/snes/templates/shmup.c +459 -198
- package/examples/snes/templates/sports-data.asm +48 -2
- package/examples/snes/templates/sports-hdr.asm +57 -0
- package/examples/snes/templates/sports.c +414 -163
- package/package.json +12 -12
- package/src/cores/wasm/bluemsx_libretro.js +1 -1
- package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
- package/src/cores/wasm/fceumm_libretro.js +1 -1
- package/src/cores/wasm/fceumm_libretro.wasm +0 -0
- package/src/cores/wasm/gambatte_libretro.js +1 -1
- package/src/cores/wasm/gambatte_libretro.wasm +0 -0
- package/src/cores/wasm/geargrafx_libretro.js +1 -1
- package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
- package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
- package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
- package/src/cores/wasm/handy_libretro.js +1 -1
- package/src/cores/wasm/handy_libretro.wasm +0 -0
- package/src/cores/wasm/mgba_libretro.js +1 -1
- package/src/cores/wasm/mgba_libretro.wasm +0 -0
- package/src/cores/wasm/prosystem_libretro.js +1 -1
- package/src/cores/wasm/prosystem_libretro.wasm +0 -0
- package/src/cores/wasm/snes9x_libretro.js +1 -1
- package/src/cores/wasm/snes9x_libretro.wasm +0 -0
- package/src/cores/wasm/stella2014_libretro.js +1 -1
- package/src/cores/wasm/stella2014_libretro.wasm +0 -0
- package/src/cores/wasm/vice_x64_libretro.js +1 -1
- package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
- package/src/host/LibretroHost.js +84 -8
- package/src/http/tool-registry.js +11 -11
- package/src/mcp/tools/cheats.js +2 -1
- package/src/mcp/tools/frame.js +3 -2
- package/src/mcp/tools/index.js +3 -3
- package/src/mcp/tools/input.js +5 -4
- package/src/mcp/tools/lifecycle.js +6 -4
- package/src/mcp/tools/memory.js +131 -24
- package/src/mcp/tools/platform-docs.js +1 -1
- package/src/mcp/tools/preview-tile.js +6 -2
- package/src/mcp/tools/project.js +1098 -130
- package/src/mcp/tools/record.js +6 -7
- package/src/mcp/tools/rom-id.js +5 -1
- package/src/mcp/tools/run-until.js +12 -4
- package/src/mcp/tools/snippets.js +6 -6
- package/src/mcp/tools/sprite-pipeline.js +14 -2
- package/src/mcp/tools/state.js +2 -1
- package/src/mcp/tools/tile-inspect.js +8 -1
- package/src/mcp/tools/toolchain.js +12 -1
- package/src/mcp/tools/watch-memory.js +53 -10
- package/src/observer/bus.js +73 -0
- package/src/observer/livestream.html +4 -2
- package/src/observer/tool-wrap.js +17 -14
- package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +32 -3
- package/src/platforms/atari7800/MENTAL_MODEL.md +5 -5
- package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
- package/src/platforms/c64/MENTAL_MODEL.md +11 -4
- package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
- package/src/platforms/gb/MENTAL_MODEL.md +3 -3
- package/src/platforms/gb/TROUBLESHOOTING.md +61 -8
- package/src/platforms/gb/lib/c/README.md +10 -11
- package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
- package/src/platforms/gb/lib/c/patch-header.js +13 -3
- package/src/platforms/gba/MENTAL_MODEL.md +4 -4
- package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
- package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
- package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
- package/src/platforms/gbc/MENTAL_MODEL.md +4 -4
- package/src/platforms/gbc/TROUBLESHOOTING.md +4 -4
- package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gbc/lib/c/README.md +10 -11
- package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
- package/src/platforms/gbc/lib/c/patch-header.js +13 -3
- package/src/platforms/genesis/MENTAL_MODEL.md +3 -3
- package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
- package/src/platforms/gg/MENTAL_MODEL.md +4 -4
- package/src/platforms/gg/TROUBLESHOOTING.md +3 -3
- package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gg/lib/c/joypad_read.c +29 -0
- package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
- package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
- package/src/platforms/msx/MENTAL_MODEL.md +5 -5
- package/src/platforms/msx/TROUBLESHOOTING.md +2 -2
- package/src/platforms/msx/lib/c/msx_hw.h +1 -0
- package/src/platforms/msx/lib/c/msx_vdp.c +25 -0
- package/src/platforms/nes/MENTAL_MODEL.md +2 -2
- package/src/platforms/nes/lib/c/nes_runtime.c +149 -34
- package/src/platforms/nes/lib/c/nes_runtime.h +34 -1
- package/src/platforms/pce/MENTAL_MODEL.md +5 -5
- package/src/platforms/pce/TROUBLESHOOTING.md +1 -1
- package/src/platforms/pce/lib/c/pce_hw.h +11 -0
- package/src/platforms/pce/lib/c/pce_video.c +32 -0
- package/src/platforms/sms/MENTAL_MODEL.md +6 -6
- package/src/platforms/snes/MENTAL_MODEL.md +2 -2
- package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
- package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
- package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
- package/src/toolchains/index.js +27 -11
|
@@ -1,224 +1,617 @@
|
|
|
1
|
-
/* ── shmup.c — Genesis
|
|
1
|
+
/* ── shmup.c — Genesis vertical shooter (complete example game) ──────────────
|
|
2
2
|
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
3
|
+
* PULSAR RAMPART — a COMPLETE, working game: title screen, 1P mode and 2P
|
|
4
|
+
* SIMULTANEOUS CO-OP (two ships on screen at once, P2 on CONTROLLER 2,
|
|
5
|
+
* sharing one arcade-style life pool and one score), fixed-slot bullet +
|
|
6
|
+
* enemy object pools, a wave spawner, persistent hi-score (cartridge SRAM),
|
|
7
|
+
* music + SFX, and the Genesis's signature trick for vertical shooters:
|
|
8
|
+
* PER-COLUMN VERTICAL SCROLL (VSCROLL_COLUMN) — a depth-banded starfield
|
|
9
|
+
* where every 16-px column falls at its own speed, under a hardware-fixed
|
|
10
|
+
* WINDOW-plane HUD.
|
|
9
11
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* slot 7..12 → enemies
|
|
16
|
-
* total 13 < 80 → no flicker even when all alive
|
|
12
|
+
* THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
|
|
13
|
+
* very different one. The markers tell you what's what:
|
|
14
|
+
* HARDWARE IDIOM (load-bearing) — dodges a documented Genesis footgun;
|
|
15
|
+
* reshape your gameplay around it (see TROUBLESHOOTING before changing).
|
|
16
|
+
* GAME LOGIC (clay) — enemy patterns, scoring, tuning, art: reshape freely.
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* What depends on what:
|
|
19
|
+
* genesis_sfx.{h,c} — PSG sound wrapper (tones + noise + a background
|
|
20
|
+
* melody loop). For full FM music, see the xgm2_demo template
|
|
21
|
+
* (XGM2_loadDriver + XGM2_play + a .xgc blob incbin'd via a data.s
|
|
22
|
+
* sibling) — we use the PSG path here so the shooter stays a
|
|
23
|
+
* single-file game; the swap is three lines plus the data.s sibling.
|
|
24
|
+
* rom_header.c (SGDK) — the Sega header at $100. Its 'RA' block at $1B0
|
|
25
|
+
* DECLARES the cartridge SRAM that hiscore_load/save below depend on
|
|
26
|
+
* (see the SRAM idiom). The build assembles it automatically.
|
|
27
|
+
*
|
|
28
|
+
* SAT discipline (kept from the minimal scaffold this grew from): the
|
|
29
|
+
* Genesis shows up to 80 sprites/frame in H40, and the cheap way to never
|
|
30
|
+
* flicker is FIXED SLOT RANGES — no "find a free SAT entry" mid-frame:
|
|
31
|
+
* slot 0 → player 1 ship
|
|
32
|
+
* slot 1 → player 2 ship (parked off-screen in 1P mode)
|
|
33
|
+
* slot 2..7 → bullets (6, shared pool — both ships fire into it)
|
|
34
|
+
* slot 8..13 → enemies (6)
|
|
35
|
+
* total 14 < 80 → no flicker even when everything is alive
|
|
36
|
+
*
|
|
37
|
+
* Frame budget (NTSC, 60 fps): the whole update — 2 ships, 6 bullets,
|
|
38
|
+
* 6 enemies, worst-case 6x6 + 6x2 AABB checks — plus 20 vscroll words and
|
|
39
|
+
* 14 SAT entries queued for vblank DMA is a tiny fraction of the 68000's
|
|
40
|
+
* frame. The vblank DMA budget (~7 KB/frame in H40) is the real ceiling
|
|
41
|
+
* on Genesis; we use < 200 bytes of it.
|
|
21
42
|
*/
|
|
22
43
|
|
|
23
44
|
#include <genesis.h>
|
|
24
45
|
#include "genesis_sfx.h"
|
|
25
46
|
|
|
26
|
-
|
|
27
|
-
|
|
47
|
+
/* The title screen renders this — examples({op:'fork'}) stamps your game's
|
|
48
|
+
* name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
|
|
49
|
+
#define GAME_TITLE "PULSAR RAMPART"
|
|
28
50
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
52
|
+
* CONTROLLER MAPPING — two layers, both bite:
|
|
53
|
+
*
|
|
54
|
+
* On the pad: SGDK's JOY_readJoypad(JOY_1/JOY_2) returns BUTTON_A/B/C/
|
|
55
|
+
* START/UP/DOWN/LEFT/RIGHT as a bitmask. Fire is BUTTON_A or BUTTON_C
|
|
56
|
+
* (real Genesis games map action buttons generously — thumbs rest on C).
|
|
57
|
+
*
|
|
58
|
+
* Driving this game HEADLESSLY through an emulator (libretro/gpgx): the
|
|
59
|
+
* core maps Genesis A/B/C onto libretro Y/B/A. So setInput({y:true})
|
|
60
|
+
* presses GENESIS A (fire/start here), setInput({b:true}) presses GENESIS
|
|
61
|
+
* B (2P select), and setInput({a:true}) presses GENESIS C — NOT Genesis A.
|
|
62
|
+
* Getting this wrong looks like "the game ignores input". START is start.
|
|
63
|
+
*/
|
|
64
|
+
#define BTN_FIRE (BUTTON_A | BUTTON_C)
|
|
65
|
+
|
|
66
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
67
|
+
* Tile art. Genesis tiles are 4bpp: each u32 row = 8 pixels, one hex nibble
|
|
68
|
+
* per pixel = a colour index into the tile's palette line (0 = transparent).
|
|
69
|
+
* Palette lines: PAL0 = P1 ship + font, PAL1 = backdrop + bullets,
|
|
70
|
+
* PAL2 = enemies, PAL3 = P2 ship. The two ships share ONE tile — P2 is a
|
|
71
|
+
* palette swap (same pattern, different palette line in the sprite attr),
|
|
72
|
+
* the classic Genesis way to get a second player for free. */
|
|
73
|
+
#define T_SHIP (TILE_USER_INDEX + 0) /* sprite: both ships (pal swap) */
|
|
74
|
+
#define T_BULLET (TILE_USER_INDEX + 1) /* sprite: player shot */
|
|
75
|
+
#define T_ENEMY (TILE_USER_INDEX + 2) /* sprite: descending raider */
|
|
76
|
+
#define T_NEB1 (TILE_USER_INDEX + 3) /* plane B: nebula weave A */
|
|
77
|
+
#define T_NEB2 (TILE_USER_INDEX + 4) /* plane B: nebula weave B */
|
|
78
|
+
#define T_STAR (TILE_USER_INDEX + 5) /* plane B: bright star on weave */
|
|
79
|
+
|
|
80
|
+
static const u32 tile_ship[8] = { /* dart hull, cockpit, twin flame */
|
|
81
|
+
0x00011000, 0x00122100, 0x00122100, 0x01111110,
|
|
82
|
+
0x11111111, 0x11111111, 0x01300310, 0x00300300,
|
|
52
83
|
};
|
|
53
84
|
static const u32 tile_bullet[8] = {
|
|
54
85
|
0x00022000, 0x00022000, 0x00222200, 0x00222200,
|
|
55
86
|
0x00222200, 0x00222200, 0x00022000, 0x00022000,
|
|
56
87
|
};
|
|
57
|
-
static const u32 tile_enemy[8]
|
|
88
|
+
static const u32 tile_enemy[8] = {
|
|
58
89
|
0x33000033, 0x03333330, 0x33333333, 0x33033033,
|
|
59
90
|
0x33333333, 0x03333330, 0x30000003, 0x03000030,
|
|
60
91
|
};
|
|
92
|
+
/* Two distinct DARK nebula blocks are checkerboarded across plane B so no
|
|
93
|
+
* single colour dominates the screen (a flat backdrop reads as "blank" to
|
|
94
|
+
* both humans and render-health checks) — and each tile's rows DIFFER, so
|
|
95
|
+
* vertical motion is visible (a flat colour shifted N px looks identical
|
|
96
|
+
* to itself). Kept dark on purpose: the window HUD floats over plane B
|
|
97
|
+
* (see the WINDOW idiom) and white text must stay readable on it. */
|
|
98
|
+
static const u32 tile_neb1[8] = {
|
|
99
|
+
0x44444444, 0x44455444, 0x44444444, 0x54444445,
|
|
100
|
+
0x44444444, 0x44455444, 0x44444444, 0x54444445,
|
|
101
|
+
};
|
|
102
|
+
static const u32 tile_neb2[8] = {
|
|
103
|
+
0x55555555, 0x55544555, 0x55555555, 0x45555554,
|
|
104
|
+
0x55555555, 0x55544555, 0x55555555, 0x45555554,
|
|
105
|
+
};
|
|
106
|
+
static const u32 tile_star[8] = { /* nebula base + a bright cross */
|
|
107
|
+
0x44444444, 0x44464444, 0x44666444, 0x44464444,
|
|
108
|
+
0x44444444, 0x44444464, 0x44444444, 0x46444444,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
112
|
+
* Object pools — fixed slots, no allocation. Pool sizes mirror the SAT slot
|
|
113
|
+
* map in the header comment: change one, change the other. */
|
|
114
|
+
#define MAX_BULLETS 6
|
|
115
|
+
#define MAX_ENEMIES 6
|
|
116
|
+
#define START_LIVES 3
|
|
117
|
+
#define SCREEN_W 320 /* H40 mode */
|
|
118
|
+
#define FIELD_TOP 16 /* HUD_ROWS * 8 — nothing flies above this */
|
|
119
|
+
#define HUD_ROWS 2 /* window rows reserved for the HUD */
|
|
61
120
|
|
|
62
121
|
typedef struct { s16 x, y; bool alive; } Obj;
|
|
63
122
|
|
|
64
|
-
static Obj
|
|
65
|
-
static Obj
|
|
66
|
-
static Obj
|
|
67
|
-
static
|
|
68
|
-
static
|
|
123
|
+
static Obj ships[2]; /* index 0 = P1 (pad 1), 1 = P2 (pad 2) */
|
|
124
|
+
static Obj bullets[MAX_BULLETS];
|
|
125
|
+
static Obj enemies[MAX_ENEMIES];
|
|
126
|
+
static u8 fire_cd[2]; /* per-ship autofire cooldown */
|
|
127
|
+
static u8 two_player; /* mode chosen on the title screen */
|
|
128
|
+
static u8 lives; /* SHARED pool in co-op (arcade style) */
|
|
129
|
+
static u16 score; /* shared in co-op too — one team, one number */
|
|
130
|
+
static u16 hiscore;
|
|
131
|
+
static u16 spawn_timer;
|
|
132
|
+
static u16 cam; /* starfield fall counter. NEVER reset and *
|
|
133
|
+
* never wrapped by hand: plane B is 256 px *
|
|
134
|
+
* tall, the VDP masks vscroll to the plane, *
|
|
135
|
+
* and 65536 is a multiple of 256 (and of *
|
|
136
|
+
* 256*2/4/8), so plain u16 overflow keeps *
|
|
137
|
+
* every depth band seamless forever. */
|
|
69
138
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
139
|
+
/* Game states — the shell every example shares: title → play → game over. */
|
|
140
|
+
#define ST_TITLE 0
|
|
141
|
+
#define ST_PLAY 1
|
|
142
|
+
#define ST_OVER 2
|
|
143
|
+
static u8 state;
|
|
144
|
+
static u16 prev_pad;
|
|
145
|
+
|
|
146
|
+
/* ── GAME LOGIC (clay) — spawn spread ───────────────────────────────────────
|
|
147
|
+
* Cheap LCG, advanced once per spawn. Seeded free-running (NOT from
|
|
148
|
+
* spawn_timer, which is always the same value at the spawn call → every
|
|
149
|
+
* enemy would descend in one column). */
|
|
150
|
+
static u16 spawn_seed = 0xC0DE;
|
|
151
|
+
|
|
152
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
153
|
+
* CARTRIDGE SRAM — the Genesis battery-save mechanism, three parts:
|
|
154
|
+
*
|
|
155
|
+
* 1. The ROM HEADER declares it: bytes $1B0.. hold 'R','A', a type word
|
|
156
|
+
* ($F820 = battery-backed, byte-wide on ODD addresses — the classic
|
|
157
|
+
* cart wiring), then start/end addresses $200000/$20FFFF. SGDK's
|
|
158
|
+
* rom_header.c (assembled into every build) already declares exactly
|
|
159
|
+
* this — no linker work needed. Emulators allocate the save RAM by
|
|
160
|
+
* READING THIS HEADER; no 'RA' block = writes to $200000+ go nowhere.
|
|
161
|
+
* 2. The MAPPER GATE: writing 1 to $A130F1 banks SRAM into $200000+,
|
|
162
|
+
* 0 banks the ROM back in. SGDK's SRAM_enable()/SRAM_disable() do
|
|
163
|
+
* this. ALWAYS disable after access — on carts >2 MB the SRAM window
|
|
164
|
+
* shadows ROM, and leaving it enabled corrupts later ROM fetches.
|
|
165
|
+
* 3. ODD-BYTE ADDRESSING: SRAM_readByte/writeByte(offset) access 68k
|
|
166
|
+
* address $200001 + offset*2. Headlessly, the emulator's save_ram
|
|
167
|
+
* region interleaves with dead even bytes: SGDK offset k lives at
|
|
168
|
+
* save_ram[k*2 + 1] (the even bytes read back $FF).
|
|
169
|
+
*
|
|
170
|
+
* Hi-score record layout (SGDK offsets): 0='H' 1='S' 2=lo 3=hi
|
|
171
|
+
* 4=checksum(lo^hi^$A5). Fresh SRAM is all $FF — the magic+checksum
|
|
172
|
+
* rejects it (and any corruption) so first boot shows 0, not 65535.
|
|
173
|
+
*
|
|
174
|
+
* Emulator note (verified against gpgx): the core sizes its save_ram
|
|
175
|
+
* region by scanning for the last non-$FF byte, so the region reads as
|
|
176
|
+
* EMPTY until the first write below lands — that's why hiscore_init runs
|
|
177
|
+
* at the very top of main(). Real hardware and .srm-restoring frontends
|
|
178
|
+
* have no such wrinkle. */
|
|
179
|
+
static u16 hiscore_load(void) {
|
|
180
|
+
u8 m0, m1, lo, hi, ck;
|
|
181
|
+
SRAM_enableRO();
|
|
182
|
+
m0 = SRAM_readByte(0);
|
|
183
|
+
m1 = SRAM_readByte(1);
|
|
184
|
+
lo = SRAM_readByte(2);
|
|
185
|
+
hi = SRAM_readByte(3);
|
|
186
|
+
ck = SRAM_readByte(4);
|
|
187
|
+
SRAM_disable();
|
|
188
|
+
if (m0 == 'H' && m1 == 'S' && ck == (u8)(lo ^ hi ^ 0xA5))
|
|
189
|
+
return ((u16)hi << 8) | lo;
|
|
190
|
+
return 0;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
static void hiscore_save(u16 sc) {
|
|
194
|
+
u8 lo = (u8)sc, hi = (u8)(sc >> 8);
|
|
195
|
+
SRAM_enable();
|
|
196
|
+
SRAM_writeByte(0, 'H');
|
|
197
|
+
SRAM_writeByte(1, 'S');
|
|
198
|
+
SRAM_writeByte(2, lo);
|
|
199
|
+
SRAM_writeByte(3, hi);
|
|
200
|
+
SRAM_writeByte(4, (u8)(lo ^ hi ^ 0xA5));
|
|
201
|
+
SRAM_disable();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/* Format-on-first-boot: if the magic is absent (fresh battery), write a
|
|
205
|
+
* valid zero record immediately so the save file exists from frame one. */
|
|
206
|
+
static void hiscore_init(void) {
|
|
207
|
+
hiscore = hiscore_load();
|
|
208
|
+
if (hiscore == 0) hiscore_save(0);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
212
|
+
* PER-COLUMN VERTICAL SCROLL — the Genesis signature for vertical shooters.
|
|
213
|
+
* The VDP can scroll plane A and plane B vertically in INDEPENDENT 2-CELL
|
|
214
|
+
* (16-px) COLUMNS: VSRAM holds one scroll word per plane per column, and
|
|
215
|
+
*
|
|
216
|
+
* VDP_setScrollingMode(HSCROLL_PLANE, VSCROLL_COLUMN)
|
|
217
|
+
*
|
|
218
|
+
* switches the VDP from one-vscroll-per-plane to one-per-column (20
|
|
219
|
+
* columns cover the 320-px H40 screen). Per frame we queue one 20-word
|
|
220
|
+
* table for plane B, banding the columns into three depths:
|
|
221
|
+
*
|
|
222
|
+
* far bands = -(cam / 8) (barely falls)
|
|
223
|
+
* mid bands = -(cam / 4)
|
|
224
|
+
* near bands = -(cam / 2) (streams past)
|
|
225
|
+
*
|
|
226
|
+
* Three speeds from ONE plane — this is how real carts faked deep space
|
|
227
|
+
* on a two-plane VDP. NEGATIVE values because the vscroll offset slides
|
|
228
|
+
* the plane UP; a "falling" starfield means the plane content slides DOWN.
|
|
229
|
+
*
|
|
230
|
+
* Requires: VSCROLL_COLUMN mode set BEFORE the first table write; plane
|
|
231
|
+
* A's 20 entries written too (once, all zero here — in column mode the
|
|
232
|
+
* VDP reads BOTH planes' columns from VSRAM, and garbage there shears
|
|
233
|
+
* your title text); DMA_QUEUE so the VSRAM write lands in vblank
|
|
234
|
+
* (SYS_doVBlankProcess flushes the queue); the value arrays static
|
|
235
|
+
* (the queue reads them AT FLUSH TIME — stack arrays are gone by then,
|
|
236
|
+
* shipping garbage).
|
|
237
|
+
* Hardware wrinkle (real VDP + accurate emulators): in column mode the
|
|
238
|
+
* LEFTMOST column can fetch a mixed scroll value when the plane is ALSO
|
|
239
|
+
* h-scrolled mid-column. We keep hscroll at 0, so it never bites here —
|
|
240
|
+
* if you add horizontal motion, scroll plane B by whole 16-px steps or
|
|
241
|
+
* live with a 1-column fringe. */
|
|
242
|
+
#define VS_COLS 20
|
|
243
|
+
static s16 vsA[VS_COLS]; /* stays all-zero: plane A fixed */
|
|
244
|
+
static s16 vsB[VS_COLS];
|
|
245
|
+
static void apply_starfield(void) {
|
|
246
|
+
u16 i;
|
|
247
|
+
for (i = 0; i < VS_COLS; i++) {
|
|
248
|
+
u16 band = i & 3; /* 0 = far, 1 = near, 2/3 = mid */
|
|
249
|
+
vsB[i] = (band == 1) ? -(s16)(cam >> 1)
|
|
250
|
+
: (band == 0) ? -(s16)(cam >> 3)
|
|
251
|
+
: -(s16)(cam >> 2);
|
|
252
|
+
}
|
|
253
|
+
VDP_setVerticalScrollTile(BG_B, 0, vsB, VS_COLS, DMA_QUEUE);
|
|
73
254
|
}
|
|
74
255
|
|
|
75
|
-
/*
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
256
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
257
|
+
* WINDOW-PLANE HUD — the fixed status bar. The window is a third tilemap
|
|
258
|
+
* that REPLACES plane A wherever it's shown and IGNORES ALL SCROLLING —
|
|
259
|
+
* a hardware-fixed HUD with zero per-frame cost, even while every plane-B
|
|
260
|
+
* column under it falls at its own speed. Three footguns:
|
|
261
|
+
* - The window only lives at screen edges (top/bottom N rows or left/
|
|
262
|
+
* right N columns) — it cannot float mid-screen.
|
|
263
|
+
* - It replaces plane A ONLY: plane B still renders BEHIND its
|
|
264
|
+
* transparent pixels (our scrolling starfield shows through around
|
|
265
|
+
* the HUD glyphs — that's why the backdrop tiles stay dark), and
|
|
266
|
+
* sprites still render OVER it: nothing in the game flies above
|
|
267
|
+
* y=16, and bullets despawn at the HUD line instead of crossing it.
|
|
268
|
+
* - Window size before window text: VDP_setWindowOnTop first. */
|
|
269
|
+
static void hud_init(void) {
|
|
270
|
+
VDP_setWindowOnTop(HUD_ROWS);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/* ── GAME LOGIC (clay) — HUD text (window plane, redrawn only on change) ── */
|
|
274
|
+
static void draw_u16(VDPPlane plane, u16 v, u16 x, u16 y) {
|
|
275
|
+
char buf[8];
|
|
276
|
+
uintToStr(v, buf, 5);
|
|
277
|
+
VDP_drawTextBG(plane, buf, x, y);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
static void draw_hud(void) {
|
|
281
|
+
char b[4];
|
|
282
|
+
VDP_drawTextBG(WINDOW, "LV", 1, 0);
|
|
283
|
+
b[0] = 'x'; b[1] = '0' + lives; b[2] = 0;
|
|
284
|
+
VDP_drawTextBG(WINDOW, b, 4, 0);
|
|
285
|
+
VDP_drawTextBG(WINDOW, "SC", 8, 0);
|
|
286
|
+
draw_u16(WINDOW, score, 11, 0);
|
|
287
|
+
VDP_drawTextBG(WINDOW, "HI", 18, 0);
|
|
288
|
+
draw_u16(WINDOW, hiscore, 21, 0);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
static void draw_hud_title(void) {
|
|
292
|
+
VDP_clearTextAreaBG(WINDOW, 0, 0, 40, HUD_ROWS);
|
|
293
|
+
VDP_drawTextBG(WINDOW, "HI", 18, 0);
|
|
294
|
+
draw_u16(WINDOW, hiscore, 21, 0);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/* ── GAME LOGIC (clay) — paint plane B once at boot, never again ──────────
|
|
298
|
+
* The frame loop only ever touches VSRAM (the column scroll table) — ZERO
|
|
299
|
+
* tilemap writes per frame. Rewriting tilemaps in the loop is the #1
|
|
300
|
+
* "choppy movement" bug; hardware scroll is free. */
|
|
301
|
+
static void paint_backdrop(void) {
|
|
302
|
+
u16 cx, cy;
|
|
303
|
+
for (cy = 0; cy < 32; cy++) {
|
|
304
|
+
for (cx = 0; cx < 64; cx++) {
|
|
305
|
+
u16 t = ((cx ^ cy) & 1) ? T_NEB2 : T_NEB1;
|
|
306
|
+
if (((cx * 7 + cy * 13) & 31) == 0) t = T_STAR;
|
|
307
|
+
VDP_setTileMapXY(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, t), cx, cy);
|
|
308
|
+
}
|
|
82
309
|
}
|
|
83
|
-
buf[5] = 0;
|
|
84
|
-
VDP_drawText(buf, 32, 1);
|
|
85
310
|
}
|
|
86
311
|
|
|
87
|
-
|
|
88
|
-
|
|
312
|
+
/* ── GAME LOGIC (clay) — the title screen (text on plane A, vscroll 0) ── */
|
|
313
|
+
static void paint_title(void) {
|
|
314
|
+
VDP_clearPlane(BG_A, TRUE);
|
|
315
|
+
VDP_drawTextBG(BG_A, GAME_TITLE, (40 - (sizeof(GAME_TITLE) - 1)) / 2, 8);
|
|
316
|
+
VDP_drawTextBG(BG_A, "1P START - A", 14, 14);
|
|
317
|
+
VDP_drawTextBG(BG_A, "2P CO-OP - B", 14, 16);
|
|
318
|
+
VDP_drawTextBG(BG_A, "D-PAD MOVES - A FIRES", 9, 21);
|
|
319
|
+
draw_hud_title();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/* ── GAME LOGIC (clay) — the game-over results screen ── */
|
|
323
|
+
static void paint_over(void) {
|
|
324
|
+
VDP_clearPlane(BG_A, TRUE);
|
|
325
|
+
VDP_drawTextBG(BG_A, "GAME OVER", 15, 8);
|
|
326
|
+
VDP_drawTextBG(BG_A, "SC", 13, 12);
|
|
327
|
+
draw_u16(BG_A, score, 17, 12);
|
|
328
|
+
VDP_drawTextBG(BG_A, "HI", 13, 17);
|
|
329
|
+
draw_u16(BG_A, hiscore, 17, 17);
|
|
330
|
+
VDP_drawTextBG(BG_A, "START - TITLE", 13, 21);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/* ── GAME LOGIC (clay) — pools ── */
|
|
334
|
+
static void fire_bullet(u8 p) {
|
|
335
|
+
u16 i;
|
|
336
|
+
for (i = 0; i < MAX_BULLETS; i++) {
|
|
89
337
|
if (!bullets[i].alive) {
|
|
90
|
-
bullets[i].x =
|
|
91
|
-
bullets[i].y =
|
|
338
|
+
bullets[i].x = ships[p].x;
|
|
339
|
+
bullets[i].y = ships[p].y - 8;
|
|
92
340
|
bullets[i].alive = TRUE;
|
|
341
|
+
sfx_tone(0, 1568, 4); /* pew (G6) */
|
|
93
342
|
return;
|
|
94
343
|
}
|
|
95
344
|
}
|
|
96
345
|
}
|
|
97
346
|
|
|
98
|
-
static u16 spawn_seed; /* free-running, advances every spawn (NOT spawn_timer,
|
|
99
|
-
* which is always 28 at the spawn call → one column) */
|
|
100
347
|
static void spawn_enemy(void) {
|
|
101
|
-
|
|
348
|
+
u16 i;
|
|
349
|
+
for (i = 0; i < MAX_ENEMIES; i++) {
|
|
102
350
|
if (!enemies[i].alive) {
|
|
103
|
-
/* Cheap LCG-ish spread across the playfield so enemies don't all
|
|
104
|
-
* descend in a single column. */
|
|
105
351
|
spawn_seed = (u16)(spawn_seed * 1103 + 12345);
|
|
106
|
-
enemies[i].x = (s16)((spawn_seed >> 4) % (
|
|
107
|
-
|
|
352
|
+
enemies[i].x = (s16)((spawn_seed >> 4) % (SCREEN_W - 16) + 8);
|
|
353
|
+
/* Pop in just BELOW the HUD line — sprites render OVER the
|
|
354
|
+
* window plane, so an enemy gliding in from y=-8 would crawl
|
|
355
|
+
* across the HUD text (see the WINDOW idiom). */
|
|
356
|
+
enemies[i].y = FIELD_TOP + 2;
|
|
108
357
|
enemies[i].alive = TRUE;
|
|
109
358
|
return;
|
|
110
359
|
}
|
|
111
360
|
}
|
|
112
361
|
}
|
|
113
362
|
|
|
363
|
+
static bool aabb_hit(Obj* a, Obj* b) {
|
|
364
|
+
return (a->x < b->x + 8) && (a->x + 8 > b->x)
|
|
365
|
+
&& (a->y < b->y + 8) && (a->y + 8 > b->y);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/* ── GAME LOGIC (clay) — start a run ── */
|
|
369
|
+
static void start_game(u8 players) {
|
|
370
|
+
u16 i;
|
|
371
|
+
two_player = players;
|
|
372
|
+
ships[0].x = players ? 120 : 156; ships[0].y = 200; ships[0].alive = TRUE;
|
|
373
|
+
ships[1].x = 184; ships[1].y = 200; ships[1].alive = players;
|
|
374
|
+
fire_cd[0] = fire_cd[1] = 0;
|
|
375
|
+
for (i = 0; i < MAX_BULLETS; i++) bullets[i].alive = FALSE;
|
|
376
|
+
for (i = 0; i < MAX_ENEMIES; i++) enemies[i].alive = FALSE;
|
|
377
|
+
lives = START_LIVES;
|
|
378
|
+
score = 0;
|
|
379
|
+
spawn_timer = 0;
|
|
380
|
+
prev_pad = 0xFFFF; /* swallow the held title button */
|
|
381
|
+
VDP_clearPlane(BG_A, TRUE); /* the playfield is open space */
|
|
382
|
+
draw_hud();
|
|
383
|
+
sfx_tone(0, 523, 10); /* start jingle (C5) */
|
|
384
|
+
state = ST_PLAY;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
static void game_over(void) {
|
|
388
|
+
if (score > hiscore) {
|
|
389
|
+
hiscore = score;
|
|
390
|
+
hiscore_save(hiscore); /* battery SRAM — see the SRAM idiom */
|
|
391
|
+
}
|
|
392
|
+
state = ST_OVER;
|
|
393
|
+
prev_pad = 0xFFFF;
|
|
394
|
+
draw_hud(); /* refresh the window HUD — HI may have just changed */
|
|
395
|
+
paint_over();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/* ── GAME LOGIC (clay) — per-ship update. p = 0 reads pad 1, p = 1 reads
|
|
399
|
+
* pad 2: simultaneous co-op is literally "the same update twice with a
|
|
400
|
+
* different joypad index" on Genesis — both pads sit on the same I/O chip
|
|
401
|
+
* and JOY_readJoypad(JOY_2) costs the same as JOY_1. ── */
|
|
402
|
+
static void update_ship(u8 p) {
|
|
403
|
+
u16 pad;
|
|
404
|
+
if (!ships[p].alive) return;
|
|
405
|
+
pad = JOY_readJoypad(p ? JOY_2 : JOY_1);
|
|
406
|
+
if ((pad & BUTTON_LEFT) && ships[p].x > 8) ships[p].x -= 2;
|
|
407
|
+
if ((pad & BUTTON_RIGHT) && ships[p].x < SCREEN_W - 16) ships[p].x += 2;
|
|
408
|
+
if ((pad & BUTTON_UP) && ships[p].y > FIELD_TOP + 8) ships[p].y -= 2;
|
|
409
|
+
if ((pad & BUTTON_DOWN) && ships[p].y < 208) ships[p].y += 2;
|
|
410
|
+
if ((pad & BTN_FIRE) && fire_cd[p] == 0) {
|
|
411
|
+
fire_bullet(p);
|
|
412
|
+
fire_cd[p] = 8; /* autofire while held, 8f apart */
|
|
413
|
+
}
|
|
414
|
+
if (fire_cd[p] > 0) --fire_cd[p];
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/* ── GAME LOGIC (clay) — stage this frame's sprites into the fixed SAT
|
|
418
|
+
* slots (the map from the header comment). Hidden sprites park at
|
|
419
|
+
* y = -16 (above the screen). NEVER hide with x = -128..0 — a SAT x of 0
|
|
420
|
+
* is the VDP's sprite-masking trigger and silently blanks every
|
|
421
|
+
* lower-priority sprite on those scanlines. ── */
|
|
422
|
+
#define HIDE_Y (-16)
|
|
423
|
+
static void stage_sprites(void) {
|
|
424
|
+
u16 i;
|
|
425
|
+
u8 play = (state == ST_PLAY);
|
|
426
|
+
for (i = 0; i < 2; i++) {
|
|
427
|
+
u8 vis = play && ships[i].alive;
|
|
428
|
+
/* P2 = same tile, different palette line: the classic pal swap. */
|
|
429
|
+
VDP_setSprite(i, ships[i].x, vis ? ships[i].y : (s16)HIDE_Y,
|
|
430
|
+
SPRITE_SIZE(1, 1),
|
|
431
|
+
TILE_ATTR_FULL(i ? PAL3 : PAL0, 1, 0, 0, T_SHIP));
|
|
432
|
+
}
|
|
433
|
+
for (i = 0; i < MAX_BULLETS; i++) {
|
|
434
|
+
u8 vis = play && bullets[i].alive;
|
|
435
|
+
VDP_setSprite(2 + i, bullets[i].x, vis ? bullets[i].y : (s16)HIDE_Y,
|
|
436
|
+
SPRITE_SIZE(1, 1),
|
|
437
|
+
TILE_ATTR_FULL(PAL1, 1, 0, 0, T_BULLET));
|
|
438
|
+
}
|
|
439
|
+
for (i = 0; i < MAX_ENEMIES; i++) {
|
|
440
|
+
u8 vis = play && enemies[i].alive;
|
|
441
|
+
VDP_setSprite(8 + i, enemies[i].x, vis ? enemies[i].y : (s16)HIDE_Y,
|
|
442
|
+
SPRITE_SIZE(1, 1),
|
|
443
|
+
TILE_ATTR_FULL(PAL2, 1, 0, 0, T_ENEMY));
|
|
444
|
+
}
|
|
445
|
+
/* ── HARDWARE IDIOM (load-bearing) — CHAIN the sprite list before
|
|
446
|
+
* uploading. VDP_setSprite does NOT set the SAT link byte, and link 0
|
|
447
|
+
* means "end of list": skip this and the VDP draws sprite 0 only.
|
|
448
|
+
* VDP_linkSprites(0, 14) links slots 0..13; the queued DMA flushes
|
|
449
|
+
* the 14 SAT entries during vblank. ── */
|
|
450
|
+
VDP_linkSprites(0, 14);
|
|
451
|
+
VDP_updateSprites(14, DMA_QUEUE);
|
|
452
|
+
}
|
|
453
|
+
|
|
114
454
|
int main(bool hard) {
|
|
455
|
+
u16 i, pad, fresh;
|
|
115
456
|
(void)hard;
|
|
116
457
|
|
|
117
|
-
/*
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
/*
|
|
124
|
-
|
|
458
|
+
/* SRAM first — before any VDP work. The save file then exists within
|
|
459
|
+
* the game's first frames of life, which is what lets a frontend (or
|
|
460
|
+
* a headless host) see a non-empty save_ram region as early as
|
|
461
|
+
* possible (see the SRAM idiom note on gpgx's size scan). */
|
|
462
|
+
hiscore_init();
|
|
463
|
+
|
|
464
|
+
/* ── HARDWARE IDIOM (load-bearing — see TROUBLESHOOTING) ──
|
|
465
|
+
* Init order: scrolling MODE before scroll VALUES, tiles + palettes
|
|
466
|
+
* before tilemaps that reference them, window size before window text.
|
|
467
|
+
* SGDK's boot already did the dangerous part (VDP regs, Z80, vblank
|
|
468
|
+
* int) — keep VDP_setScrollingMode FIRST here so every later
|
|
469
|
+
* apply_starfield() writes the VSRAM layout the VDP actually reads,
|
|
470
|
+
* and seed plane A's column entries (all zero) right after: in
|
|
471
|
+
* VSCROLL_COLUMN mode the VDP fetches plane A's scroll per column
|
|
472
|
+
* too, and uninitialised entries shear the title text. */
|
|
473
|
+
VDP_setScrollingMode(HSCROLL_PLANE, VSCROLL_COLUMN);
|
|
474
|
+
VDP_setVerticalScrollTile(BG_A, 0, vsA, VS_COLS, DMA_QUEUE);
|
|
475
|
+
hud_init();
|
|
125
476
|
|
|
126
|
-
|
|
477
|
+
/* Palettes: PAL0 P1 ship + HUD text, PAL1 backdrop + bullets,
|
|
478
|
+
* PAL2 enemies, PAL3 P2 ship (the pal-swap line).
|
|
479
|
+
* Colours are BGR, 3 bits per channel: 0x0BGR with E = full. */
|
|
480
|
+
PAL_setColor( 1, 0x0EEE); /* P1 hull white */
|
|
481
|
+
PAL_setColor( 2, 0x0EA0); /* P1 cockpit teal */
|
|
482
|
+
PAL_setColor( 3, 0x004E); /* engine flame orange */
|
|
483
|
+
PAL_setColor(15, 0x0EEE); /* font white (index 15 = SGDK font) */
|
|
484
|
+
PAL_setColor(16 + 2, 0x00EE); /* bullet yellow */
|
|
485
|
+
PAL_setColor(16 + 4, 0x0411); /* nebula deep blue */
|
|
486
|
+
PAL_setColor(16 + 5, 0x0204); /* nebula dark plum */
|
|
487
|
+
PAL_setColor(16 + 6, 0x0CEE); /* star bright */
|
|
488
|
+
PAL_setColor(32 + 3, 0x022E); /* enemy red */
|
|
489
|
+
PAL_setColor(48 + 1, 0x04E4); /* P2 hull green */
|
|
490
|
+
PAL_setColor(48 + 2, 0x0AEA); /* P2 cockpit pale green */
|
|
491
|
+
PAL_setColor(48 + 3, 0x004E); /* engine flame orange */
|
|
127
492
|
|
|
128
|
-
VDP_loadTileData(tile_blank, T_BLANK, 1, DMA);
|
|
129
493
|
VDP_loadTileData(tile_ship, T_SHIP, 1, DMA);
|
|
130
494
|
VDP_loadTileData(tile_bullet, T_BULLET, 1, DMA);
|
|
131
495
|
VDP_loadTileData(tile_enemy, T_ENEMY, 1, DMA);
|
|
132
|
-
VDP_loadTileData(
|
|
133
|
-
VDP_loadTileData(
|
|
134
|
-
|
|
135
|
-
/* Fill the far plane (BG_B) with the space backdrop so the screen
|
|
136
|
-
* isn't an empty black void; sprinkle denser star clusters for
|
|
137
|
-
* variety. Sprites (ship/bullets/enemies) always draw above the
|
|
138
|
-
* planes, so the gameplay reads on top of this with no priority
|
|
139
|
-
* juggling. */
|
|
140
|
-
for (u16 cy = 0; cy < 28; cy++)
|
|
141
|
-
for (u16 cx = 0; cx < 40; cx++)
|
|
142
|
-
VDP_setTileMapXY(BG_B,
|
|
143
|
-
TILE_ATTR_FULL(PAL1, 0, 0, 0,
|
|
144
|
-
((cx ^ cy) & 1) ? T_STARS : T_SPACE),
|
|
145
|
-
cx, cy);
|
|
146
|
-
|
|
147
|
-
player.x = 152; player.y = 180; player.alive = TRUE;
|
|
148
|
-
for (u16 i = 0; i < MAX_BULLETS; i++) bullets[i].alive = FALSE;
|
|
149
|
-
for (u16 i = 0; i < MAX_ENEMIES; i++) enemies[i].alive = FALSE;
|
|
150
|
-
score = 0;
|
|
151
|
-
spawn_timer = 0;
|
|
496
|
+
VDP_loadTileData(tile_neb1, T_NEB1, 1, DMA);
|
|
497
|
+
VDP_loadTileData(tile_neb2, T_NEB2, 1, DMA);
|
|
498
|
+
VDP_loadTileData(tile_star, T_STAR, 1, DMA);
|
|
152
499
|
|
|
153
|
-
|
|
154
|
-
|
|
500
|
+
paint_backdrop(); /* plane B: painted once, scrolled forever */
|
|
501
|
+
sfx_init(); /* PSG: sfx channels + background melody */
|
|
155
502
|
|
|
156
|
-
|
|
503
|
+
state = ST_TITLE;
|
|
504
|
+
cam = 0;
|
|
505
|
+
apply_starfield();
|
|
506
|
+
paint_title();
|
|
157
507
|
|
|
158
508
|
while (TRUE) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
509
|
+
if (state == ST_TITLE) {
|
|
510
|
+
/* ── GAME LOGIC (clay) — title: A = 1P, B = 2P co-op ──
|
|
511
|
+
* The starfield keeps falling so the title sells the
|
|
512
|
+
* column-banded depth while the plane-A text holds still
|
|
513
|
+
* (its VSRAM columns stay 0 — only plane B's get cam). */
|
|
514
|
+
cam += 2;
|
|
515
|
+
apply_starfield();
|
|
516
|
+
stage_sprites();
|
|
517
|
+
pad = JOY_readJoypad(JOY_1);
|
|
518
|
+
fresh = pad & ~prev_pad;
|
|
519
|
+
if (fresh & (BUTTON_A | BUTTON_C | BUTTON_START)) start_game(0);
|
|
520
|
+
else if (fresh & BUTTON_B) start_game(1);
|
|
521
|
+
prev_pad = pad;
|
|
522
|
+
sfx_update();
|
|
523
|
+
SYS_doVBlankProcess();
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (state == ST_OVER) {
|
|
528
|
+
/* Results screen; START or A returns to the title. The
|
|
529
|
+
* starfield never stops — motion on every screen for free. */
|
|
530
|
+
cam += 2;
|
|
531
|
+
apply_starfield();
|
|
532
|
+
stage_sprites();
|
|
533
|
+
pad = JOY_readJoypad(JOY_1);
|
|
534
|
+
fresh = pad & ~prev_pad;
|
|
535
|
+
if (fresh & (BUTTON_START | BUTTON_A | BUTTON_C)) {
|
|
536
|
+
state = ST_TITLE;
|
|
537
|
+
prev_pad = 0xFFFF; /* swallow the held START */
|
|
538
|
+
paint_title();
|
|
539
|
+
} else {
|
|
540
|
+
prev_pad = pad;
|
|
541
|
+
}
|
|
542
|
+
sfx_update();
|
|
543
|
+
SYS_doVBlankProcess();
|
|
544
|
+
continue;
|
|
168
545
|
}
|
|
169
|
-
prev = pad;
|
|
170
546
|
|
|
171
|
-
|
|
547
|
+
/* ── ST_PLAY ──────────────────────────────────────────────────── */
|
|
548
|
+
stage_sprites();
|
|
549
|
+
cam += 2;
|
|
550
|
+
apply_starfield();
|
|
551
|
+
|
|
552
|
+
/* ── GAME LOGIC (clay) from here down ── */
|
|
553
|
+
update_ship(0);
|
|
554
|
+
if (two_player) update_ship(1);
|
|
555
|
+
|
|
556
|
+
for (i = 0; i < MAX_BULLETS; i++) {
|
|
172
557
|
if (!bullets[i].alive) continue;
|
|
173
558
|
bullets[i].y -= 4;
|
|
174
|
-
|
|
559
|
+
/* Despawn AT the HUD line, not off-screen: sprites draw OVER
|
|
560
|
+
* the window plane (see the WINDOW idiom). */
|
|
561
|
+
if (bullets[i].y < FIELD_TOP + 2) bullets[i].alive = FALSE;
|
|
175
562
|
}
|
|
176
|
-
for (
|
|
563
|
+
for (i = 0; i < MAX_ENEMIES; i++) {
|
|
177
564
|
if (!enemies[i].alive) continue;
|
|
178
565
|
enemies[i].y += 1;
|
|
179
|
-
if (enemies[i].y > 224) enemies[i].alive = FALSE;
|
|
566
|
+
if (enemies[i].y > 224) enemies[i].alive = FALSE; /* slipped *
|
|
567
|
+
* past — no penalty, the *
|
|
568
|
+
* pressure IS the penalty */
|
|
180
569
|
}
|
|
181
570
|
if (++spawn_timer >= 28) {
|
|
182
571
|
spawn_timer = 0;
|
|
183
572
|
spawn_enemy();
|
|
184
573
|
}
|
|
185
574
|
|
|
186
|
-
/*
|
|
187
|
-
for (
|
|
575
|
+
/* Bullets × enemies. */
|
|
576
|
+
for (i = 0; i < MAX_BULLETS; i++) {
|
|
577
|
+
u16 j;
|
|
188
578
|
if (!bullets[i].alive) continue;
|
|
189
|
-
for (
|
|
579
|
+
for (j = 0; j < MAX_ENEMIES; j++) {
|
|
190
580
|
if (!enemies[j].alive) continue;
|
|
191
581
|
if (aabb_hit(&bullets[i], &enemies[j])) {
|
|
192
582
|
bullets[i].alive = FALSE;
|
|
193
583
|
enemies[j].alive = FALSE;
|
|
194
584
|
if (score < 65500u) score += 10;
|
|
195
|
-
sfx_noise(8);
|
|
585
|
+
sfx_noise(8); /* explosion */
|
|
586
|
+
draw_hud();
|
|
196
587
|
break;
|
|
197
588
|
}
|
|
198
589
|
}
|
|
199
590
|
}
|
|
200
591
|
|
|
201
|
-
/*
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
592
|
+
/* Enemies × ships: shared life pool (arcade co-op). */
|
|
593
|
+
for (i = 0; i < MAX_ENEMIES; i++) {
|
|
594
|
+
u16 p;
|
|
595
|
+
if (!enemies[i].alive) continue;
|
|
596
|
+
for (p = 0; p < 2; p++) {
|
|
597
|
+
if (!ships[p].alive) continue;
|
|
598
|
+
if (aabb_hit(&enemies[i], &ships[p])) {
|
|
599
|
+
enemies[i].alive = FALSE;
|
|
600
|
+
sfx_noise(14);
|
|
601
|
+
if (lives > 0) --lives;
|
|
602
|
+
draw_hud();
|
|
603
|
+
if (lives == 0) {
|
|
604
|
+
game_over();
|
|
605
|
+
} else {
|
|
606
|
+
/* respawn knockback to the start column */
|
|
607
|
+
ships[p].y = 200;
|
|
608
|
+
ships[p].x = p ? 184 : (two_player ? 120 : 156);
|
|
609
|
+
}
|
|
610
|
+
break;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
if (state != ST_PLAY) break;
|
|
213
614
|
}
|
|
214
|
-
/* CHAIN the sprite linked list before uploading: VDP_setSprite does NOT
|
|
215
|
-
* set the link byte, and the SAT link bytes init to 0 (= "end of list"),
|
|
216
|
-
* so the VDP's sprite walk stops after slot 0 → only ONE sprite draws.
|
|
217
|
-
* VDP_linkSprites(0, N) links slots 0..N-1 so all N render. */
|
|
218
|
-
VDP_linkSprites(0, 1 + MAX_BULLETS + MAX_ENEMIES);
|
|
219
|
-
VDP_updateSprites(1 + MAX_BULLETS + MAX_ENEMIES, DMA);
|
|
220
|
-
|
|
221
|
-
render_score();
|
|
222
615
|
|
|
223
616
|
sfx_update();
|
|
224
617
|
SYS_doVBlankProcess();
|