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,114 +1,289 @@
|
|
|
1
|
-
/* ── shmup/main.c — MSX vertical
|
|
1
|
+
/* ── shmup/main.c — MSX vertical shooter (complete example game) ─────────────
|
|
2
2
|
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* A COMPLETE, working game — title screen, 1P and 2P co-op modes (MSX has two
|
|
4
|
+
* joystick ports), lives, score + session hi-score, music + SFX on the
|
|
5
|
+
* AY-3-8910 PSG, and the MSX's signature trick: SCREEN-2 PER-ROW COLOR
|
|
6
|
+
* (the color table gives every 8x1 pixel row of every tile its own
|
|
7
|
+
* foreground/background pair, in THREE independent screen thirds — used here
|
|
8
|
+
* for a depth-banded starfield, a HUD band in its own colors, and an 8-color
|
|
9
|
+
* gradient inside a single tile).
|
|
5
10
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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 MSX footgun; reshape
|
|
14
|
+
* your gameplay around it (see TROUBLESHOOTING before changing).
|
|
15
|
+
* GAME LOGIC (clay) — enemy patterns, scoring, tuning, art: reshape freely.
|
|
11
16
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
17
|
+
* What depends on what:
|
|
18
|
+
* msx_hw.h / msx_vdp.c — VDP + PSG + joystick helpers (direct Z80 ports;
|
|
19
|
+
* the PSG functions carry a DI/EI guard against the BIOS KEYINT race —
|
|
20
|
+
* read msx_vdp.c before adding your own PSG pokes).
|
|
21
|
+
* msx_crt0.s — the $4000 "AB" cart header + static-init copy. Load-bearing;
|
|
22
|
+
* INIT must never return, so main() ends in for(;;).
|
|
15
23
|
*
|
|
16
|
-
*
|
|
24
|
+
* Controls: joystick PORT 1 (or keyboard cursors+space) flies ship 1,
|
|
25
|
+
* trigger A fires. PORT 2 flies ship 2 in co-op. On the title screen
|
|
26
|
+
* trigger A starts 1P; trigger B (or player 2's trigger) starts 2P co-op.
|
|
17
27
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* -
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* - vsync() one game step per VDP frame (interrupt-free)
|
|
28
|
+
* Hi-score honesty: the bundled bluemsx core build does NOT expose a battery
|
|
29
|
+
* save path (retro_get_memory(SAVE_RAM) is unimplemented for MSX carts), so
|
|
30
|
+
* the hi-score lives in plain RAM: it survives title↔game cycles but NOT a
|
|
31
|
+
* power cycle. Never fake persistence — if you need real saves, that's a
|
|
32
|
+
* future core round (SRAM-mapper cart types like ASCII8-SRAM exist; the
|
|
33
|
+
* core just doesn't surface their RAM yet).
|
|
25
34
|
*/
|
|
26
35
|
#include "msx_hw.h"
|
|
27
36
|
|
|
28
|
-
/*
|
|
37
|
+
/* The title screen renders this — examples({op:'fork'}) stamps your game's
|
|
38
|
+
* name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
|
|
39
|
+
#define GAME_TITLE "NEBULA WARDEN"
|
|
40
|
+
|
|
41
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
42
|
+
* Interrupt-free vblank sync: poll VDP status S#0 bit 7 (port 0x99). Reading
|
|
43
|
+
* the port ALSO clears the flag, so one read per frame = one game step per
|
|
44
|
+
* frame. We deliberately do NOT use the BIOS JIFFY counter here: this poll
|
|
45
|
+
* works even with interrupts masked, and never depends on the BIOS ISR
|
|
46
|
+
* keeping pace. (The BIOS KEYINT also reads S#0 — on rare frames it eats the
|
|
47
|
+
* flag first and this loop just waits for the next one; a one-frame hiccup,
|
|
48
|
+
* never a hang.) */
|
|
29
49
|
__sfr __at 0x99 VDPSTATUS;
|
|
30
50
|
static void vsync(void) {
|
|
31
|
-
(void)VDPSTATUS;
|
|
51
|
+
(void)VDPSTATUS; /* throw away a possibly-stale flag */
|
|
32
52
|
while (!(VDPSTATUS & 0x80)) {
|
|
33
53
|
}
|
|
34
54
|
}
|
|
35
55
|
|
|
36
|
-
/*
|
|
37
|
-
*
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
/*
|
|
43
|
-
#define
|
|
44
|
-
#define
|
|
45
|
-
#define
|
|
46
|
-
#define
|
|
47
|
-
#define
|
|
48
|
-
#define
|
|
49
|
-
#define
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
/*
|
|
57
|
-
/*
|
|
58
|
-
/*
|
|
59
|
-
/*
|
|
60
|
-
/*
|
|
61
|
-
/*
|
|
62
|
-
/*
|
|
63
|
-
/*
|
|
64
|
-
/*
|
|
65
|
-
/*
|
|
66
|
-
/*
|
|
67
|
-
/*
|
|
68
|
-
/*
|
|
69
|
-
/*
|
|
70
|
-
/*
|
|
71
|
-
/*
|
|
72
|
-
/*
|
|
73
|
-
/*
|
|
74
|
-
/*
|
|
75
|
-
/*
|
|
56
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
57
|
+
* Tile font: index 0 = space, 1-26 = A-Z, 27-36 = 0-9, 37 = dash, then the
|
|
58
|
+
* starfield tiles. One 8x8 pattern = 8 bytes, one bit per pixel; set bits
|
|
59
|
+
* draw in the tile's FOREGROUND color, clear bits in its BACKGROUND color
|
|
60
|
+
* (both come from the screen-2 color table — see the idiom block below). */
|
|
61
|
+
#define T_SPACE 0
|
|
62
|
+
#define T_A 1 /* 'A'..'Z' = T_A + (c - 'A') */
|
|
63
|
+
#define T_0 27 /* '0'..'9' = T_0 + (c - '0') */
|
|
64
|
+
#define T_DASH 37
|
|
65
|
+
#define T_FIELD 38 /* empty space cell (pattern all 0 = all bg) */
|
|
66
|
+
#define T_STAR1 39 /* faint single-pixel star */
|
|
67
|
+
#define T_STAR2 40 /* bright cross star */
|
|
68
|
+
#define T_NEBULA 41 /* the per-8x1-row gradient tile (see below) */
|
|
69
|
+
#define NUM_TILES 42
|
|
70
|
+
|
|
71
|
+
static const uint8_t font[NUM_TILES][8] = {
|
|
72
|
+
/* SPACE */ {0,0,0,0,0,0,0,0},
|
|
73
|
+
/* 1 A */ {0x38,0x6C,0xC6,0xC6,0xFE,0xC6,0xC6,0x00},
|
|
74
|
+
/* 2 B */ {0xFC,0xC6,0xC6,0xFC,0xC6,0xC6,0xFC,0x00},
|
|
75
|
+
/* 3 C */ {0x7C,0xC6,0xC0,0xC0,0xC0,0xC6,0x7C,0x00},
|
|
76
|
+
/* 4 D */ {0xF8,0xCC,0xC6,0xC6,0xC6,0xCC,0xF8,0x00},
|
|
77
|
+
/* 5 E */ {0xFE,0xC0,0xC0,0xF8,0xC0,0xC0,0xFE,0x00},
|
|
78
|
+
/* 6 F */ {0xFE,0xC0,0xC0,0xF8,0xC0,0xC0,0xC0,0x00},
|
|
79
|
+
/* 7 G */ {0x7C,0xC6,0xC0,0xCE,0xC6,0xC6,0x7C,0x00},
|
|
80
|
+
/* 8 H */ {0xC6,0xC6,0xC6,0xFE,0xC6,0xC6,0xC6,0x00},
|
|
81
|
+
/* 9 I */ {0x7E,0x18,0x18,0x18,0x18,0x18,0x7E,0x00},
|
|
82
|
+
/* 10 J */ {0x1E,0x06,0x06,0x06,0xC6,0xC6,0x7C,0x00},
|
|
83
|
+
/* 11 K */ {0xC6,0xCC,0xD8,0xF0,0xD8,0xCC,0xC6,0x00},
|
|
84
|
+
/* 12 L */ {0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xFE,0x00},
|
|
85
|
+
/* 13 M */ {0xC6,0xEE,0xFE,0xD6,0xC6,0xC6,0xC6,0x00},
|
|
86
|
+
/* 14 N */ {0xC6,0xE6,0xF6,0xDE,0xCE,0xC6,0xC6,0x00},
|
|
87
|
+
/* 15 O */ {0x7C,0xC6,0xC6,0xC6,0xC6,0xC6,0x7C,0x00},
|
|
88
|
+
/* 16 P */ {0xFC,0xC6,0xC6,0xFC,0xC0,0xC0,0xC0,0x00},
|
|
89
|
+
/* 17 Q */ {0x7C,0xC6,0xC6,0xC6,0xD6,0xCC,0x76,0x00},
|
|
90
|
+
/* 18 R */ {0xFC,0xC6,0xC6,0xFC,0xD8,0xCC,0xC6,0x00},
|
|
91
|
+
/* 19 S */ {0x7C,0xC0,0xC0,0x78,0x0C,0x0C,0xF8,0x00},
|
|
92
|
+
/* 20 T */ {0x7E,0x18,0x18,0x18,0x18,0x18,0x18,0x00},
|
|
93
|
+
/* 21 U */ {0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0x7C,0x00},
|
|
94
|
+
/* 22 V */ {0xC6,0xC6,0xC6,0xC6,0x6C,0x38,0x10,0x00},
|
|
95
|
+
/* 23 W */ {0xC6,0xC6,0xC6,0xD6,0xFE,0xEE,0xC6,0x00},
|
|
96
|
+
/* 24 X */ {0xC6,0x6C,0x38,0x10,0x38,0x6C,0xC6,0x00},
|
|
97
|
+
/* 25 Y */ {0x66,0x66,0x66,0x3C,0x18,0x18,0x18,0x00},
|
|
98
|
+
/* 26 Z */ {0xFE,0x0C,0x18,0x30,0x60,0xC0,0xFE,0x00},
|
|
99
|
+
/* 27 0 */ {0x7C,0xCE,0xDE,0xF6,0xE6,0xC6,0x7C,0x00},
|
|
100
|
+
/* 28 1 */ {0x18,0x38,0x18,0x18,0x18,0x18,0x7E,0x00},
|
|
101
|
+
/* 29 2 */ {0x7C,0xC6,0x06,0x1C,0x70,0xC0,0xFE,0x00},
|
|
102
|
+
/* 30 3 */ {0x7C,0xC6,0x06,0x3C,0x06,0xC6,0x7C,0x00},
|
|
103
|
+
/* 31 4 */ {0x1C,0x3C,0x6C,0xCC,0xFE,0x0C,0x0C,0x00},
|
|
104
|
+
/* 32 5 */ {0xFE,0xC0,0xFC,0x06,0x06,0xC6,0x7C,0x00},
|
|
105
|
+
/* 33 6 */ {0x3C,0x60,0xC0,0xFC,0xC6,0xC6,0x7C,0x00},
|
|
106
|
+
/* 34 7 */ {0xFE,0x06,0x0C,0x18,0x30,0x30,0x30,0x00},
|
|
107
|
+
/* 35 8 */ {0x7C,0xC6,0xC6,0x7C,0xC6,0xC6,0x7C,0x00},
|
|
108
|
+
/* 36 9 */ {0x7C,0xC6,0xC6,0x7E,0x06,0x0C,0x78,0x00},
|
|
109
|
+
/* 37 - */ {0x00,0x00,0x00,0x7E,0x00,0x00,0x00,0x00},
|
|
110
|
+
/* 38 FIELD (all bg) */ {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
|
|
111
|
+
/* 39 STAR1 (one pixel) */ {0x00,0x00,0x00,0x10,0x00,0x00,0x00,0x00},
|
|
112
|
+
/* 40 STAR2 (cross) */ {0x00,0x10,0x10,0x7C,0x10,0x10,0x00,0x00},
|
|
113
|
+
/* 41 NEBULA (solid fg — its COLOR bytes paint the gradient) */
|
|
114
|
+
{0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF},
|
|
76
115
|
};
|
|
77
116
|
|
|
78
|
-
/*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
117
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
118
|
+
* SCREEN-2 PER-ROW COLOR — the MSX's signature background trick.
|
|
119
|
+
*
|
|
120
|
+
* Screen 2 (GRAPHIC II) is NOT "one color byte per tile" like most consoles:
|
|
121
|
+
*
|
|
122
|
+
* 1. The 256x192 screen is THREE INDEPENDENT THIRDS of 8 rows each
|
|
123
|
+
* (name-table rows 0-7, 8-15, 16-23). Each third has its OWN 2KB
|
|
124
|
+
* pattern table slice and its OWN 2KB color table slice:
|
|
125
|
+
* patterns: VRAM_PATTERN + third*0x800, colors: VRAM_COLOR + third*0x800
|
|
126
|
+
* The SAME tile index can look completely different in each third —
|
|
127
|
+
* we exploit exactly that for the depth-banded starfield below.
|
|
128
|
+
*
|
|
129
|
+
* 2. Within a tile, the color table holds EIGHT bytes — one per 8x1 pixel
|
|
130
|
+
* row — each packing (foreground<<4)|background from the fixed TMS9918
|
|
131
|
+
* palette. So one tile can carry an 8-color vertical gradient
|
|
132
|
+
* (T_NEBULA's whole "glow horizon" is a single tile, colors only).
|
|
133
|
+
*
|
|
134
|
+
* Requires: the screen-2 table layout set by msx_set_screen2() (R3=0xFF,
|
|
135
|
+
* R4=0x03 — the "thirds" configuration; some games set R3/R4 so all thirds
|
|
136
|
+
* SHARE one slice, which saves VRAM but kills this trick), and pattern +
|
|
137
|
+
* color uploads to EVERY third a tile is used in. Upload with the display
|
|
138
|
+
* idle or accept a partial frame: tile N's slot is pattern[N*8] / color[N*8].
|
|
139
|
+
*
|
|
140
|
+
* Depth scheme taught here (TMS9918 fixed palette: 1 black, 4 dark blue,
|
|
141
|
+
* 5 light blue, 7 cyan, 11 light yellow, 14 gray, 15 white):
|
|
142
|
+
* third 0 (top) = deep space: black field, gray stars — far, dim
|
|
143
|
+
* third 1 (middle) = mid space: dark blue, yellow stars — closer
|
|
144
|
+
* third 2 (bottom) = near space: light blue, white stars — closest
|
|
145
|
+
* ...and the HUD text band (row 0, third 0) gets its OWN colors, distinct
|
|
146
|
+
* from everything below it, without costing any extra tiles. */
|
|
147
|
+
static const uint8_t col_text[3] = { 0xF4, 0xB4, 0x15 }; /* HUD white-on-blue; title yellow-on-blue; bottom black-on-lightblue */
|
|
148
|
+
static const uint8_t col_field[3] = { 0x11, 0x44, 0x55 }; /* the three depth bands (bg shows: pattern is all 0) */
|
|
149
|
+
static const uint8_t col_star1[3] = { 0xE1, 0xB4, 0xF5 }; /* faint star per band: gray/yellow/white on its band bg */
|
|
150
|
+
static const uint8_t col_star2[3] = { 0xF1, 0xF4, 0xB5 }; /* bright star per band */
|
|
151
|
+
/* T_NEBULA: 8 DIFFERENT color bytes inside ONE tile = an 8-pixel-row glow
|
|
152
|
+
* gradient (dark blue → light blue → cyan → white and back down to black).
|
|
153
|
+
* The pattern is solid 0xFF so only the fg nibbles show. */
|
|
154
|
+
static const uint8_t col_nebula[8] = { 0x45,0x55,0x75,0xF5,0x75,0x55,0x45,0x15 };
|
|
155
|
+
|
|
156
|
+
static void load_tiles(void) {
|
|
157
|
+
uint8_t third, i;
|
|
158
|
+
uint16_t patbase, colbase;
|
|
159
|
+
for (third = 0; third < 3; third++) {
|
|
160
|
+
patbase = (uint16_t)(VRAM_PATTERN + ((uint16_t)third << 11));
|
|
161
|
+
colbase = (uint16_t)(VRAM_COLOR + ((uint16_t)third << 11));
|
|
162
|
+
for (i = 0; i < NUM_TILES; i++) {
|
|
163
|
+
uint8_t col;
|
|
164
|
+
/* pattern bits are the same in every third — only COLOR varies */
|
|
165
|
+
msx_vram_write((uint16_t)(patbase + ((uint16_t)i << 3)), font[i], 8);
|
|
166
|
+
if (i == T_NEBULA) { /* the one per-pixel-row gradient */
|
|
167
|
+
msx_vram_write((uint16_t)(colbase + ((uint16_t)i << 3)), col_nebula, 8);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (i == T_FIELD) col = col_field[third];
|
|
171
|
+
else if (i == T_STAR1) col = col_star1[third];
|
|
172
|
+
else if (i == T_STAR2) col = col_star2[third];
|
|
173
|
+
else col = col_text[third];
|
|
174
|
+
msx_fill_vram((uint16_t)(colbase + ((uint16_t)i << 3)), 8, col);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/* ── GAME LOGIC (clay — reshape freely) — name-table drawing helpers ────────
|
|
180
|
+
* Screen 2 VRAM writes are safe at any point in the frame at C speed: the
|
|
181
|
+
* TMS9918 needs ~29 Z80 cycles between VRAM accesses during active display,
|
|
182
|
+
* and SDCC-compiled loops are slower than that. (Hand-tuned asm OTIR bursts
|
|
183
|
+
* are the thing that outruns the VDP — see TROUBLESHOOTING.) */
|
|
184
|
+
static void put_tile(uint8_t col, uint8_t row, uint8_t tile) {
|
|
185
|
+
msx_vram_write((uint16_t)(VRAM_NAME + (uint16_t)row * 32 + col), &tile, 1);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
static void draw_text(uint8_t col, uint8_t row, const char *s) {
|
|
189
|
+
uint8_t buf[32];
|
|
190
|
+
uint8_t n = 0;
|
|
191
|
+
while (*s && n < 32) {
|
|
192
|
+
char c = *s++;
|
|
193
|
+
if (c >= 'A' && c <= 'Z') buf[n] = (uint8_t)(T_A + c - 'A');
|
|
194
|
+
else if (c >= '0' && c <= '9') buf[n] = (uint8_t)(T_0 + c - '0');
|
|
195
|
+
else if (c == '-') buf[n] = T_DASH;
|
|
196
|
+
else buf[n] = T_SPACE;
|
|
197
|
+
n++;
|
|
198
|
+
}
|
|
199
|
+
msx_vram_write((uint16_t)(VRAM_NAME + (uint16_t)row * 32 + col), buf, n);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
static void draw_num4(uint8_t col, uint8_t row, uint16_t v) {
|
|
203
|
+
uint8_t buf[4];
|
|
204
|
+
buf[0] = (uint8_t)(T_0 + (v / 1000) % 10);
|
|
205
|
+
buf[1] = (uint8_t)(T_0 + (v / 100) % 10);
|
|
206
|
+
buf[2] = (uint8_t)(T_0 + (v / 10) % 10);
|
|
207
|
+
buf[3] = (uint8_t)(T_0 + v % 10);
|
|
208
|
+
msx_vram_write((uint16_t)(VRAM_NAME + (uint16_t)row * 32 + col), buf, 4);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/* Paint the full 32x24 starfield. The TILE INDICES are the same everywhere —
|
|
212
|
+
* the three depth bands come ENTIRELY from the per-third color tables (the
|
|
213
|
+
* screen-2 idiom above). Row 0 is the HUD band; row 23 is the one-tile
|
|
214
|
+
* nebula gradient. */
|
|
215
|
+
static void paint_starfield(void) {
|
|
216
|
+
uint8_t row, col, h;
|
|
217
|
+
uint8_t buf[32];
|
|
218
|
+
msx_fill_vram(VRAM_NAME, 32, T_SPACE); /* row 0: HUD band */
|
|
219
|
+
for (row = 1; row < 23; row++) {
|
|
220
|
+
for (col = 0; col < 32; col++) {
|
|
221
|
+
h = (uint8_t)((row * 7 + col * 5) & 15); /* cheap static hash */
|
|
222
|
+
if (h == 0) buf[col] = T_STAR1;
|
|
223
|
+
else if (h == 8) buf[col] = T_STAR2;
|
|
224
|
+
else buf[col] = T_FIELD;
|
|
225
|
+
}
|
|
226
|
+
msx_vram_write((uint16_t)(VRAM_NAME + (uint16_t)row * 32), buf, 32);
|
|
227
|
+
}
|
|
228
|
+
msx_fill_vram((uint16_t)(VRAM_NAME + 23u * 32), 32, T_NEBULA);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* ── GAME LOGIC (clay — reshape freely) — sprites ────────────────────────────
|
|
232
|
+
* 8x8 one-color hardware sprites. Plane layout (lower plane = on top):
|
|
233
|
+
* 0-1 ships, 2-7 bullets, 8-12 enemies. */
|
|
89
234
|
static const uint8_t spr_ship[8] = {0x18,0x3C,0x7E,0x7E,0xFF,0xFF,0xDB,0x81};
|
|
90
235
|
static const uint8_t spr_bullet[8] = {0x18,0x3C,0x3C,0x3C,0x3C,0x3C,0x18,0x00};
|
|
91
236
|
static const uint8_t spr_enemy[8] = {0x81,0x42,0x24,0x18,0x18,0x24,0x42,0x81};
|
|
92
|
-
|
|
93
237
|
#define PAT_SHIP 0
|
|
94
238
|
#define PAT_BULLET 1
|
|
95
239
|
#define PAT_ENEMY 2
|
|
96
|
-
|
|
97
|
-
/*
|
|
98
|
-
#define
|
|
99
|
-
#define
|
|
100
|
-
|
|
101
|
-
|
|
240
|
+
#define COL_SHIP1 15 /* white */
|
|
241
|
+
#define COL_SHIP2 3 /* light green */
|
|
242
|
+
#define COL_BULLET 11 /* light yellow*/
|
|
243
|
+
#define COL_ENEMY 9 /* light red */
|
|
244
|
+
|
|
245
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
246
|
+
* Sprite limits + the Y=208 terminator:
|
|
247
|
+
* - A sprite Y of 0xD0 (208) tells the TMS9918 to STOP SCANNING the
|
|
248
|
+
* attribute table — every higher-numbered plane vanishes, not just that
|
|
249
|
+
* one. (msx_clear_sprites parks ALL planes at 0xD0, which is fine at the
|
|
250
|
+
* END of the list.) To hide ONE sprite mid-list, park it OFFSCREEN at
|
|
251
|
+
* PARK_Y (192 = first line below the display) — never at 0xD0.
|
|
252
|
+
* (On MSX2's V9938 sprite mode 2 the terminator moves to 0xD8 and 0xD0
|
|
253
|
+
* is "just offscreen" — code that leans on that breaks on MSX1.)
|
|
254
|
+
* - Per scanline the TMS9918 draws only 4 sprites (V9938: 8); the rest drop
|
|
255
|
+
* out for that line. Pools here are sized so a worst-case pileup is rare;
|
|
256
|
+
* if you raise MAX_* counts, expect flicker on crowded rows. */
|
|
257
|
+
#define PARK_Y 192
|
|
258
|
+
|
|
259
|
+
#define MAX_BULLETS 6
|
|
260
|
+
#define MAX_ENEMIES 5
|
|
261
|
+
|
|
262
|
+
/* ── GAME LOGIC (clay — reshape freely) — game state ─────────────────────── */
|
|
102
263
|
typedef struct { uint8_t x, y, alive; } Obj;
|
|
103
264
|
|
|
104
|
-
static Obj
|
|
265
|
+
static Obj ships[2];
|
|
266
|
+
static uint8_t fire_cd[2];
|
|
105
267
|
static Obj bullets[MAX_BULLETS];
|
|
106
268
|
static Obj enemies[MAX_ENEMIES];
|
|
269
|
+
static uint8_t two_player; /* mode chosen on the title screen */
|
|
270
|
+
static uint8_t lives; /* shared pool in co-op (arcade style) */
|
|
107
271
|
static uint16_t score;
|
|
272
|
+
static uint16_t hiscore; /* SESSION-ONLY: plain RAM. The bundled
|
|
273
|
+
* bluemsx build exposes no SAVE_RAM region,
|
|
274
|
+
* so there is nothing battery-backed to
|
|
275
|
+
* write — survives title↔game cycles, not a
|
|
276
|
+
* power cycle (honest, not faked). */
|
|
108
277
|
static uint8_t spawn_timer;
|
|
109
278
|
static uint16_t rng;
|
|
110
|
-
static uint8_t blip;
|
|
111
279
|
|
|
280
|
+
#define ST_TITLE 0
|
|
281
|
+
#define ST_PLAY 1
|
|
282
|
+
#define ST_OVER 2
|
|
283
|
+
static uint8_t state;
|
|
284
|
+
static uint8_t prev_t1, prev_t2; /* trigger edge detection across states */
|
|
285
|
+
|
|
286
|
+
/* xorshift16 PRNG — a few dozen cycles, no tables. */
|
|
112
287
|
static uint8_t next_rand(void) {
|
|
113
288
|
rng ^= (uint16_t)(rng << 7);
|
|
114
289
|
rng ^= (uint16_t)(rng >> 9);
|
|
@@ -116,148 +291,244 @@ static uint8_t next_rand(void) {
|
|
|
116
291
|
return (uint8_t)(rng & 0xFF);
|
|
117
292
|
}
|
|
118
293
|
|
|
119
|
-
/*
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
294
|
+
/* ── GAME LOGIC (clay — reshape freely) — music + SFX on the AY-3-8910 ──────
|
|
295
|
+
* Channel plan: A = fire blip, B = explosion noise, C = music. The PSG has 3
|
|
296
|
+
* tone channels + ONE shared noise generator, mixed per-channel in reg 7.
|
|
297
|
+
* All register traffic goes through msx_psg_tone/noise/off — they wrap the
|
|
298
|
+
* PSGADDR/PSGWRITE pair in DI/EI because the BIOS KEYINT ISR clobbers the
|
|
299
|
+
* PSG address latch every frame (the bug that once silenced every MSX
|
|
300
|
+
* scaffold — see msx_vdp.c).
|
|
301
|
+
*
|
|
302
|
+
* The tune: one period entry per half-beat, 0 = rest. AY period =
|
|
303
|
+
* 1789773 / (16 * freq) — e.g. A4 (440Hz) -> 254. Ticked once per frame from
|
|
304
|
+
* the main loop; a note advances every 7 frames (~8.5 notes/sec). The lib's
|
|
305
|
+
* built-in demo loop (msx_music_tick) also uses channel C, so we switch it
|
|
306
|
+
* OFF in main() and run this table instead — edit THIS table to rescore. */
|
|
307
|
+
static const uint16_t tune[32] = {
|
|
308
|
+
254, 0, 285, 254, 339, 0, 285, 339, /* A4 G4 A4 E4 G4 E4 (A-minor riff) */
|
|
309
|
+
427, 0, 339, 427, 508, 0, 0, 0, /* C4 E4 C4 A3 rest */
|
|
310
|
+
380, 0, 427, 380, 320, 0, 380, 427, /* D4 C4 D4 F4 D4 C4 */
|
|
311
|
+
508, 0, 427, 339, 285, 0, 0, 0, /* A3 C4 E4 G4 rest */
|
|
312
|
+
};
|
|
313
|
+
static uint8_t music_step, music_timer;
|
|
314
|
+
static uint8_t sfx_fire_t, sfx_boom_t; /* frames left on each SFX channel */
|
|
315
|
+
|
|
316
|
+
static void music_tick(void) {
|
|
317
|
+
if (music_timer == 0) {
|
|
318
|
+
uint16_t p = tune[music_step & 31];
|
|
319
|
+
if (p) msx_psg_tone(2, p, 10);
|
|
320
|
+
else msx_psg_off(2);
|
|
321
|
+
music_step++;
|
|
135
322
|
}
|
|
323
|
+
music_timer++;
|
|
324
|
+
if (music_timer >= 7) music_timer = 0;
|
|
136
325
|
}
|
|
137
326
|
|
|
138
|
-
static void
|
|
139
|
-
|
|
327
|
+
static void sfx_tick(void) {
|
|
328
|
+
if (sfx_fire_t) { sfx_fire_t--; if (!sfx_fire_t) msx_psg_off(0); }
|
|
329
|
+
if (sfx_boom_t) { sfx_boom_t--; if (!sfx_boom_t) msx_psg_noise(1, 0, 0); }
|
|
140
330
|
}
|
|
141
331
|
|
|
142
|
-
/*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
static void
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
332
|
+
/* ── GAME LOGIC (clay — reshape freely) — HUD ──────────────────────────────
|
|
333
|
+
* Row 0 = the HUD band (third 0's text colors make it a distinct strip).
|
|
334
|
+
* SC=score, HI=hi-score, SH=ships(lives). */
|
|
335
|
+
static void draw_hud_labels(void) {
|
|
336
|
+
draw_text(1, 0, "SC");
|
|
337
|
+
draw_text(11, 0, "HI");
|
|
338
|
+
draw_text(21, 0, "SH");
|
|
339
|
+
}
|
|
340
|
+
static void draw_score(void) { draw_num4(4, 0, score); }
|
|
341
|
+
static void draw_hi(void) { draw_num4(14, 0, hiscore); }
|
|
342
|
+
static void draw_lives(void) { put_tile(24, 0, (uint8_t)(T_0 + lives)); }
|
|
343
|
+
|
|
344
|
+
/* ── GAME LOGIC (clay — reshape freely) — screens ──────────────────────────
|
|
345
|
+
* Title rows land in third 1 (yellow-on-blue) and third 2 (the HI line) —
|
|
346
|
+
* the same glyph tiles as the HUD, recolored for free by the thirds idiom. */
|
|
347
|
+
static void paint_title(void) {
|
|
348
|
+
uint8_t len = 0, col;
|
|
349
|
+
const char *p = GAME_TITLE;
|
|
350
|
+
while (*p++) len++;
|
|
351
|
+
col = (uint8_t)((32 - len) / 2);
|
|
352
|
+
paint_starfield();
|
|
353
|
+
draw_text(col, 8, GAME_TITLE);
|
|
354
|
+
draw_text(7, 12, "1P START - FIRE A");
|
|
355
|
+
draw_text(7, 14, "2P CO-OP - FIRE B");
|
|
356
|
+
draw_text(12, 19, "HI 0000"); /* the space blanks the cell between */
|
|
357
|
+
draw_num4(15, 19, hiscore);
|
|
157
358
|
}
|
|
158
359
|
|
|
159
|
-
static void
|
|
160
|
-
|
|
161
|
-
|
|
360
|
+
static void start_game(uint8_t players) {
|
|
361
|
+
uint8_t i;
|
|
362
|
+
two_player = players;
|
|
363
|
+
for (i = 0; i < MAX_BULLETS; i++) bullets[i].alive = 0;
|
|
364
|
+
for (i = 0; i < MAX_ENEMIES; i++) enemies[i].alive = 0;
|
|
365
|
+
ships[0].x = two_player ? 96 : 120; ships[0].y = 160; ships[0].alive = 1;
|
|
366
|
+
ships[1].x = 144; ships[1].y = 160; ships[1].alive = two_player;
|
|
367
|
+
fire_cd[0] = fire_cd[1] = 0;
|
|
368
|
+
lives = 3;
|
|
369
|
+
score = 0;
|
|
370
|
+
spawn_timer = 0;
|
|
371
|
+
paint_starfield();
|
|
372
|
+
draw_hud_labels();
|
|
373
|
+
draw_score(); draw_hi(); draw_lives();
|
|
374
|
+
state = ST_PLAY;
|
|
162
375
|
}
|
|
163
376
|
|
|
164
|
-
static void
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
377
|
+
static void game_over(void) {
|
|
378
|
+
if (score > hiscore) { hiscore = score; draw_hi(); }
|
|
379
|
+
draw_text(11, 11, "GAME OVER");
|
|
380
|
+
draw_text(8, 13, "FIRE FOR TITLE");
|
|
381
|
+
prev_t1 = prev_t2 = 1; /* swallow a fire button still held from play */
|
|
382
|
+
state = ST_OVER;
|
|
169
383
|
}
|
|
170
384
|
|
|
385
|
+
/* ── GAME LOGIC (clay — reshape freely) — combat ─────────────────────────── */
|
|
171
386
|
static uint8_t aabb(Obj *a, Obj *b) {
|
|
172
387
|
return a->x < b->x + 8 && a->x + 8 > b->x
|
|
173
388
|
&& a->y < b->y + 8 && a->y + 8 > b->y;
|
|
174
389
|
}
|
|
175
390
|
|
|
176
|
-
static void fire(
|
|
391
|
+
static void fire(uint8_t p) {
|
|
177
392
|
uint8_t i;
|
|
178
393
|
for (i = 0; i < MAX_BULLETS; i++) {
|
|
179
394
|
if (!bullets[i].alive) {
|
|
180
|
-
bullets[i].x =
|
|
181
|
-
bullets[i].y = (uint8_t)(
|
|
395
|
+
bullets[i].x = ships[p].x;
|
|
396
|
+
bullets[i].y = (uint8_t)(ships[p].y - 8);
|
|
182
397
|
bullets[i].alive = 1;
|
|
183
|
-
msx_psg_tone(0,
|
|
184
|
-
|
|
398
|
+
msx_psg_tone(0, 0x080, 12); /* high blip on channel A */
|
|
399
|
+
sfx_fire_t = 4;
|
|
185
400
|
return;
|
|
186
401
|
}
|
|
187
402
|
}
|
|
188
403
|
}
|
|
189
404
|
|
|
190
|
-
static void
|
|
405
|
+
static void spawn_enemy(void) {
|
|
191
406
|
uint8_t i;
|
|
192
407
|
for (i = 0; i < MAX_ENEMIES; i++) {
|
|
193
408
|
if (!enemies[i].alive) {
|
|
194
409
|
enemies[i].x = (uint8_t)(8 + (next_rand() % 232));
|
|
195
|
-
enemies[i].y =
|
|
410
|
+
enemies[i].y = 12;
|
|
196
411
|
enemies[i].alive = 1;
|
|
197
412
|
return;
|
|
198
413
|
}
|
|
199
414
|
}
|
|
200
415
|
}
|
|
201
416
|
|
|
417
|
+
/* Per-player input + movement. Stick mapping: P1 = joystick port 1 with the
|
|
418
|
+
* keyboard cursors (stick 0) as fallback; P2 = joystick port 2. GTSTCK
|
|
419
|
+
* returns 0=center then 1-8 clockwise from up. */
|
|
420
|
+
static void update_ship(uint8_t p) {
|
|
421
|
+
uint8_t dir, trig;
|
|
422
|
+
if (!ships[p].alive) return;
|
|
423
|
+
if (p == 0) {
|
|
424
|
+
dir = msx_read_joystick(1);
|
|
425
|
+
if (dir == STICK_CENTER) dir = msx_read_joystick(0);
|
|
426
|
+
trig = (uint8_t)(gttrig(1) || gttrig(0)); /* port-1 trig A or SPACE */
|
|
427
|
+
} else {
|
|
428
|
+
dir = msx_read_joystick(2);
|
|
429
|
+
trig = gttrig(2); /* port-2 trigger A */
|
|
430
|
+
}
|
|
431
|
+
if ((dir == STICK_LEFT || dir == STICK_UL || dir == STICK_DL)
|
|
432
|
+
&& ships[p].x > 4) ships[p].x = (uint8_t)(ships[p].x - 2);
|
|
433
|
+
if ((dir == STICK_RIGHT || dir == STICK_UR || dir == STICK_DR)
|
|
434
|
+
&& ships[p].x < 248) ships[p].x = (uint8_t)(ships[p].x + 2);
|
|
435
|
+
if ((dir == STICK_UP || dir == STICK_UL || dir == STICK_UR)
|
|
436
|
+
&& ships[p].y > 24) ships[p].y = (uint8_t)(ships[p].y - 2);
|
|
437
|
+
if ((dir == STICK_DOWN || dir == STICK_DL || dir == STICK_DR)
|
|
438
|
+
&& ships[p].y < 168) ships[p].y = (uint8_t)(ships[p].y + 2);
|
|
439
|
+
if (trig && fire_cd[p] == 0) { fire(p); fire_cd[p] = 10; }
|
|
440
|
+
if (fire_cd[p]) fire_cd[p]--;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/* Push every object to its sprite plane. Dead objects park at PARK_Y
|
|
444
|
+
* (offscreen), NEVER 0xD0 — see the sprite idiom block above. */
|
|
445
|
+
static void push_sprites(void) {
|
|
446
|
+
uint8_t i;
|
|
447
|
+
msx_set_sprite(0, ships[0].x, ships[0].alive ? ships[0].y : PARK_Y,
|
|
448
|
+
PAT_SHIP, COL_SHIP1);
|
|
449
|
+
msx_set_sprite(1, ships[1].x, ships[1].alive ? ships[1].y : PARK_Y,
|
|
450
|
+
PAT_SHIP, COL_SHIP2);
|
|
451
|
+
for (i = 0; i < MAX_BULLETS; i++)
|
|
452
|
+
msx_set_sprite((uint8_t)(2 + i), bullets[i].x,
|
|
453
|
+
bullets[i].alive ? bullets[i].y : PARK_Y,
|
|
454
|
+
PAT_BULLET, COL_BULLET);
|
|
455
|
+
for (i = 0; i < MAX_ENEMIES; i++)
|
|
456
|
+
msx_set_sprite((uint8_t)(8 + i), enemies[i].x,
|
|
457
|
+
enemies[i].alive ? enemies[i].y : PARK_Y,
|
|
458
|
+
PAT_ENEMY, COL_ENEMY);
|
|
459
|
+
}
|
|
460
|
+
|
|
202
461
|
void main(void) {
|
|
203
|
-
uint8_t i, j,
|
|
462
|
+
uint8_t i, j, t1, t2;
|
|
204
463
|
|
|
464
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
465
|
+
* Init order: set the video mode FIRST (INIGRP also clears VRAM — any
|
|
466
|
+
* upload done before it is wiped), then tiles, then sprites. The crt0's
|
|
467
|
+
* INIT contract means main() must NEVER return — the BIOS has nothing
|
|
468
|
+
* sane to fall back to — hence the for(;;) below. */
|
|
205
469
|
msx_set_screen2();
|
|
206
470
|
msx_clear_sprites();
|
|
207
|
-
|
|
208
|
-
draw_starfield();
|
|
209
|
-
draw_label();
|
|
210
|
-
|
|
471
|
+
load_tiles();
|
|
211
472
|
msx_vram_write((uint16_t)(VRAM_SPRPAT + PAT_SHIP * 8), spr_ship, 8);
|
|
212
473
|
msx_vram_write((uint16_t)(VRAM_SPRPAT + PAT_BULLET * 8), spr_bullet, 8);
|
|
213
474
|
msx_vram_write((uint16_t)(VRAM_SPRPAT + PAT_ENEMY * 8), spr_enemy, 8);
|
|
214
475
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
score = 0;
|
|
219
|
-
spawn_timer = 0;
|
|
476
|
+
msx_music(0); /* the lib's demo loop also owns channel C —
|
|
477
|
+
* hand the channel to OUR tune table instead */
|
|
478
|
+
hiscore = 0; /* session hi-score (no SAVE_RAM on this core) */
|
|
220
479
|
rng = 0xACE1;
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
480
|
+
music_step = music_timer = 0;
|
|
481
|
+
sfx_fire_t = sfx_boom_t = 0;
|
|
482
|
+
prev_t1 = prev_t2 = 1; /* swallow a held trigger across state changes */
|
|
483
|
+
state = ST_TITLE;
|
|
484
|
+
paint_title();
|
|
224
485
|
|
|
225
486
|
for (;;) {
|
|
226
487
|
vsync();
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (
|
|
231
|
-
|
|
488
|
+
music_tick();
|
|
489
|
+
sfx_tick();
|
|
490
|
+
|
|
491
|
+
if (state == ST_TITLE) {
|
|
492
|
+
/* ── GAME LOGIC (clay) — title: trig A = 1P; trig B (port-1
|
|
493
|
+
* button 2, gttrig 3) OR player 2's trigger = 2P co-op. */
|
|
494
|
+
t1 = (uint8_t)(gttrig(1) || gttrig(0));
|
|
495
|
+
t2 = (uint8_t)(gttrig(3) || gttrig(2));
|
|
496
|
+
if (t2 && !prev_t2) start_game(1);
|
|
497
|
+
else if (t1 && !prev_t1) start_game(0);
|
|
498
|
+
prev_t1 = t1; prev_t2 = t2;
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
232
501
|
|
|
233
|
-
if (
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
502
|
+
if (state == ST_OVER) {
|
|
503
|
+
/* Freeze the final frame; any fire button returns to the title. */
|
|
504
|
+
t1 = (uint8_t)(gttrig(1) || gttrig(0) || gttrig(2));
|
|
505
|
+
if (t1 && !prev_t1) {
|
|
506
|
+
state = ST_TITLE;
|
|
507
|
+
msx_clear_sprites();
|
|
508
|
+
paint_title();
|
|
509
|
+
}
|
|
510
|
+
prev_t1 = t1; prev_t2 = t1;
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
241
513
|
|
|
242
|
-
|
|
243
|
-
|
|
514
|
+
/* ── ST_PLAY — GAME LOGIC (clay) from here down ─────────────────── */
|
|
515
|
+
update_ship(0);
|
|
516
|
+
if (two_player) update_ship(1);
|
|
244
517
|
|
|
245
|
-
/* advance bullets */
|
|
246
518
|
for (i = 0; i < MAX_BULLETS; i++) {
|
|
247
519
|
if (!bullets[i].alive) continue;
|
|
248
|
-
if (bullets[i].y <
|
|
520
|
+
if (bullets[i].y < 14) { bullets[i].alive = 0; continue; }
|
|
249
521
|
bullets[i].y = (uint8_t)(bullets[i].y - 4);
|
|
250
522
|
}
|
|
251
|
-
/* advance enemies */
|
|
252
523
|
for (i = 0; i < MAX_ENEMIES; i++) {
|
|
253
524
|
if (!enemies[i].alive) continue;
|
|
254
525
|
enemies[i].y = (uint8_t)(enemies[i].y + 1);
|
|
255
526
|
if (enemies[i].y >= 184) enemies[i].alive = 0;
|
|
256
527
|
}
|
|
257
|
-
spawn_timer
|
|
258
|
-
if (spawn_timer >= 28) { spawn_timer = 0;
|
|
528
|
+
spawn_timer++;
|
|
529
|
+
if (spawn_timer >= 28) { spawn_timer = 0; spawn_enemy(); }
|
|
259
530
|
|
|
260
|
-
/*
|
|
531
|
+
/* bullets ↔ enemies */
|
|
261
532
|
for (i = 0; i < MAX_BULLETS; i++) {
|
|
262
533
|
if (!bullets[i].alive) continue;
|
|
263
534
|
for (j = 0; j < MAX_ENEMIES; j++) {
|
|
@@ -265,25 +536,36 @@ void main(void) {
|
|
|
265
536
|
if (aabb(&bullets[i], &enemies[j])) {
|
|
266
537
|
bullets[i].alive = 0;
|
|
267
538
|
enemies[j].alive = 0;
|
|
268
|
-
if (score <
|
|
269
|
-
|
|
270
|
-
|
|
539
|
+
if (score < 9999) score++;
|
|
540
|
+
draw_score();
|
|
541
|
+
msx_psg_noise(1, 12, 13); /* explosion: shared noise */
|
|
542
|
+
sfx_boom_t = 8;
|
|
271
543
|
break;
|
|
272
544
|
}
|
|
273
545
|
}
|
|
274
546
|
}
|
|
275
547
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
548
|
+
/* enemies ↔ ships: shared life pool (arcade co-op) */
|
|
549
|
+
for (j = 0; j < MAX_ENEMIES; j++) {
|
|
550
|
+
if (!enemies[j].alive) continue;
|
|
551
|
+
for (i = 0; i < 2; i++) {
|
|
552
|
+
if (!ships[i].alive) continue;
|
|
553
|
+
if (aabb(&enemies[j], &ships[i])) {
|
|
554
|
+
enemies[j].alive = 0;
|
|
555
|
+
msx_psg_noise(1, 28, 14); /* deeper, longer crunch */
|
|
556
|
+
sfx_boom_t = 20;
|
|
557
|
+
if (lives) lives--;
|
|
558
|
+
draw_lives();
|
|
559
|
+
if (lives == 0) {
|
|
560
|
+
game_over();
|
|
561
|
+
} else {
|
|
562
|
+
ships[i].y = 160; /* respawn knockback */
|
|
563
|
+
ships[i].x = i ? 144 : (two_player ? 96 : 120);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
push_sprites();
|
|
288
570
|
}
|
|
289
571
|
}
|