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,208 +1,545 @@
|
|
|
1
|
-
/* ── sports.c — Genesis
|
|
1
|
+
/* ── sports.c — Genesis versus sports game (complete example game) ───────────
|
|
2
2
|
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
3
|
+
* A COMPLETE, working game — VOLT VOLLEY, a head-to-head court game (Pong
|
|
4
|
+
* lineage): title screen, 1P vs a beatable CPU and 2P simultaneous versus
|
|
5
|
+
* (player 2 on CONTROLLER 2), first-to-5 match flow with a result screen,
|
|
6
|
+
* a hardware-fixed WINDOW-plane HUD, PSG music + SFX, and a battery-backed
|
|
7
|
+
* record (longest win streak vs the CPU) in cartridge SRAM.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* -
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
9
|
+
* THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
|
|
10
|
+
* very different one. The markers tell you what's what:
|
|
11
|
+
* HARDWARE IDIOM (load-bearing) — dodges a documented Genesis footgun;
|
|
12
|
+
* reshape your gameplay around it (see TROUBLESHOOTING before changing).
|
|
13
|
+
* GAME LOGIC (clay) — court art, ball physics, CPU skill, scoring rules:
|
|
14
|
+
* reshape freely.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* What depends on what:
|
|
17
|
+
* genesis_sfx.{h,c} — PSG sound wrapper (tones + noise + a background
|
|
18
|
+
* melody loop). For full FM music, see the xgm2_demo template
|
|
19
|
+
* (XGM2_loadDriver + XGM2_play + a .xgc blob incbin'd via a data.s
|
|
20
|
+
* sibling) — the PSG path keeps this a single-file game.
|
|
21
|
+
* rom_header.c (SGDK) — the Sega header at $100. Its 'RA' block at $1B0
|
|
22
|
+
* DECLARES the cartridge SRAM that record_load/save below depend on
|
|
23
|
+
* (see the SRAM idiom). The build assembles it automatically.
|
|
24
|
+
*
|
|
25
|
+
* Layering: the court (rails + net + floor) lives on plane B, painted ONCE
|
|
26
|
+
* at boot and never touched again. Title/result text lives on plane A, which
|
|
27
|
+
* is cleared during play. The HUD lives on the WINDOW plane — fixed by
|
|
28
|
+
* hardware, zero per-frame cost. Nothing repaints inside the frame loop.
|
|
29
|
+
*
|
|
30
|
+
* Frame budget (NTSC, 60 fps): 2 paddles + 1 ball + 2 paddle AABB tests +
|
|
31
|
+
* 7 SAT entries queued for vblank DMA + the occasional HUD digit — a tiny
|
|
32
|
+
* fraction of the 68000's frame. Plenty of headroom for fancier physics.
|
|
19
33
|
*/
|
|
20
34
|
|
|
21
35
|
#include <genesis.h>
|
|
22
36
|
#include "genesis_sfx.h"
|
|
23
37
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
#define
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
/* The title screen renders this — examples({op:'fork'}) stamps your game's
|
|
39
|
+
* name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
|
|
40
|
+
#define GAME_TITLE "VOLT VOLLEY"
|
|
41
|
+
|
|
42
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
43
|
+
* CONTROLLER MAPPING — two layers, both bite:
|
|
44
|
+
*
|
|
45
|
+
* On the pad: SGDK's JOY_readJoypad(JOY_1/JOY_2) returns BUTTON_A/B/C/
|
|
46
|
+
* START/UP/DOWN/LEFT/RIGHT as a bitmask. The title maps A (or START) to
|
|
47
|
+
* 1P vs CPU and B to 2P versus; C also starts 1P (real Genesis games map
|
|
48
|
+
* action buttons generously — thumbs rest on C).
|
|
49
|
+
*
|
|
50
|
+
* Driving this game HEADLESSLY through an emulator (libretro/gpgx): the
|
|
51
|
+
* core maps Genesis A/B/C onto libretro Y/B/A. So setInput({y:true})
|
|
52
|
+
* presses GENESIS A (1P start here), setInput({b:true}) presses GENESIS
|
|
53
|
+
* B (2P start), and setInput({a:true}) presses GENESIS C — NOT Genesis A.
|
|
54
|
+
* Getting this wrong looks like "the game ignores input". START is start.
|
|
55
|
+
*/
|
|
56
|
+
#define BTN_1P (BUTTON_A | BUTTON_C | BUTTON_START)
|
|
57
|
+
#define BTN_2P (BUTTON_B)
|
|
58
|
+
|
|
59
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
60
|
+
* Tile art. Genesis tiles are 4bpp: each u32 row = 8 pixels, one hex nibble
|
|
61
|
+
* per pixel = a colour index into the tile's palette line (0 = transparent).
|
|
62
|
+
* Sprites + font use PAL0, the P2 paddle PAL1, plane B (the court) PAL2. */
|
|
63
|
+
#define T_PADDLE (TILE_USER_INDEX + 0) /* sprite: 4px-wide paddle column */
|
|
64
|
+
#define T_BALL (TILE_USER_INDEX + 1) /* sprite: the ball */
|
|
65
|
+
#define T_RAIL (TILE_USER_INDEX + 2) /* plane B: top/bottom court rail */
|
|
66
|
+
#define T_NET (TILE_USER_INDEX + 3) /* plane B: dashed centre net */
|
|
67
|
+
#define T_FLOOR (TILE_USER_INDEX + 4) /* plane B: speckled court floor */
|
|
68
|
+
#define T_BAND (TILE_USER_INDEX + 5) /* plane B: flat band behind HUD */
|
|
69
|
+
|
|
70
|
+
static const u32 tile_paddle[8] = { /* colour 1: P1 cyan / P2 red via *
|
|
71
|
+
* the palette LINE in TILE_ATTR */
|
|
72
|
+
0x11110000, 0x11110000, 0x11110000, 0x11110000,
|
|
73
|
+
0x11110000, 0x11110000, 0x11110000, 0x11110000,
|
|
74
|
+
};
|
|
75
|
+
static const u32 tile_ball[8] = { /* volt-yellow ball + highlight */
|
|
76
|
+
0x00444400, 0x04444440, 0x44455444, 0x44455444,
|
|
77
|
+
0x44444444, 0x44444444, 0x04444440, 0x00444400,
|
|
78
|
+
};
|
|
79
|
+
static const u32 tile_rail[8] = {
|
|
40
80
|
0x11111111, 0x11111111, 0x11111111, 0x11111111,
|
|
41
81
|
0x11111111, 0x11111111, 0x11111111, 0x11111111,
|
|
42
82
|
};
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
*
|
|
46
|
-
* down the court's centre column it reads as a dashed Pong net. */
|
|
83
|
+
/* Centre net: a 2px dashed bar. DIM on purpose — title/result text on plane
|
|
84
|
+
* A overlaps the net column, and white-on-white glyphs would be unreadable
|
|
85
|
+
* (plane A glyph backgrounds are transparent, so plane B shows through). */
|
|
47
86
|
static const u32 tile_net[8] = {
|
|
48
|
-
|
|
49
|
-
|
|
87
|
+
0x00022000, 0x00022000, 0x00022000, 0x00000000,
|
|
88
|
+
0x00022000, 0x00022000, 0x00022000, 0x00000000,
|
|
89
|
+
};
|
|
90
|
+
static const u32 tile_floor[8] = { /* sparse speckles so the arena *
|
|
91
|
+
* reads as a court, not a void */
|
|
92
|
+
0x00000000, 0x00300000, 0x00000000, 0x00000003,
|
|
93
|
+
0x00000000, 0x03000000, 0x00000000, 0x00000300,
|
|
94
|
+
};
|
|
95
|
+
static const u32 tile_band[8] = {
|
|
96
|
+
0x55555555, 0x55555555, 0x55555555, 0x55555555,
|
|
97
|
+
0x55555555, 0x55555555, 0x55555555, 0x55555555,
|
|
50
98
|
};
|
|
51
99
|
|
|
52
|
-
/*
|
|
53
|
-
*
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
#define
|
|
57
|
-
#define
|
|
58
|
-
#define
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
100
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
101
|
+
* Court geometry + match rules. The court is framed by plane-B rails on cell
|
|
102
|
+
* rows 2 and 27; COURT_TOP/BOT keep the ball between them. Rows 0-1 sit
|
|
103
|
+
* under the WINDOW HUD (see the window idiom below). */
|
|
104
|
+
#define HUD_ROWS 2 /* window rows reserved for the HUD */
|
|
105
|
+
#define PADDLE_H 24 /* 3 stacked 8px sprites */
|
|
106
|
+
#define PADDLE_W 4
|
|
107
|
+
#define PADDLE_X1 16 /* P1 — left side */
|
|
108
|
+
#define PADDLE_X2 300 /* P2/CPU — right side (320 - 16 - 4) */
|
|
109
|
+
#define COURT_TOP 24 /* first pixel row below the top rail */
|
|
110
|
+
#define COURT_BOT 216 /* first pixel row of the bottom rail */
|
|
111
|
+
#define NET_COL 20 /* cell column of the centre net */
|
|
112
|
+
#define BALL_W 8
|
|
113
|
+
#define BALL_H 8
|
|
114
|
+
#define SCREEN_W 320 /* H40 mode */
|
|
115
|
+
#define WIN_SCORE 5 /* first to 5 takes the match */
|
|
116
|
+
#define P1_SPEED 2 /* px/frame — both humans move at this */
|
|
117
|
+
#define CPU_SPEED 1 /* px/frame — half speed: beatable */
|
|
118
|
+
|
|
119
|
+
static s16 p1y, p2y; /* paddle top Y, pixels */
|
|
120
|
+
static s16 bx, by; /* ball top-left, pixels */
|
|
121
|
+
static s16 bdx, bdy; /* ball velocity (px/frame) */
|
|
122
|
+
static u8 score_p1, score_p2;
|
|
123
|
+
static u8 serve_timer; /* freeze frames between points */
|
|
124
|
+
static u8 two_player; /* title pick: 0 = vs CPU, 1 = 2P versus */
|
|
125
|
+
static u8 streak; /* current 1P-vs-CPU win streak (RAM) */
|
|
126
|
+
static u16 best_streak; /* battery-backed record — see end_match */
|
|
127
|
+
static u8 new_record; /* result screen shows NEW RECORD */
|
|
128
|
+
|
|
129
|
+
/* Game states — the shell every example shares: title → play → game over. */
|
|
130
|
+
#define ST_TITLE 0
|
|
131
|
+
#define ST_PLAY 1
|
|
132
|
+
#define ST_OVER 2
|
|
133
|
+
static u8 state;
|
|
134
|
+
static u16 prev_pad;
|
|
135
|
+
|
|
136
|
+
/* ── GAME LOGIC (clay) — xorshift16 PRNG (a few 68k instructions per call).
|
|
137
|
+
* A versus game NEEDS this: the Genesis is fully deterministic, so without
|
|
138
|
+
* a noise source two fixed strategies lock into an infinite rally loop (the
|
|
139
|
+
* exact same 600-frame cycle, forever — a match that never ends). random8()
|
|
140
|
+
* is ticked once per play frame so identical game states a few seconds
|
|
141
|
+
* apart still diverge, and every paddle return adds a ±1 "spin". */
|
|
142
|
+
static u16 rng = 0xC0A7;
|
|
143
|
+
static u8 random8(void) {
|
|
144
|
+
u16 r = rng;
|
|
145
|
+
r ^= r << 7;
|
|
146
|
+
r ^= r >> 9;
|
|
147
|
+
r ^= r << 8;
|
|
148
|
+
rng = r;
|
|
149
|
+
return (u8)r;
|
|
150
|
+
}
|
|
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
|
+
* Record layout (SGDK offsets): 0='H' 1='S' 2=lo 3=hi 4=checksum
|
|
171
|
+
* (lo^hi^$A5). Fresh SRAM is all $FF — the magic+checksum rejects it (and
|
|
172
|
+
* any corruption) so first boot shows 0, not 65535.
|
|
173
|
+
*
|
|
174
|
+
* Persistence choice: for a VERSUS sports game a raw hi-score is
|
|
175
|
+
* meaningless (every match ends 5-x), so we persist the longest 1P win
|
|
176
|
+
* streak against the CPU — the stat a returning player actually chases.
|
|
177
|
+
* 2P matches never touch it (humans beating each other isn't a record).
|
|
178
|
+
*
|
|
179
|
+
* Emulator note (verified against gpgx): the core sizes its save_ram
|
|
180
|
+
* region by scanning for the last non-$FF byte, so the region reads as
|
|
181
|
+
* EMPTY until the first write below lands — that's why record_init runs
|
|
182
|
+
* at the very top of main(). Real hardware and .srm-restoring frontends
|
|
183
|
+
* have no such wrinkle. */
|
|
184
|
+
static u16 record_load(void) {
|
|
185
|
+
u8 m0, m1, lo, hi, ck;
|
|
186
|
+
SRAM_enableRO();
|
|
187
|
+
m0 = SRAM_readByte(0);
|
|
188
|
+
m1 = SRAM_readByte(1);
|
|
189
|
+
lo = SRAM_readByte(2);
|
|
190
|
+
hi = SRAM_readByte(3);
|
|
191
|
+
ck = SRAM_readByte(4);
|
|
192
|
+
SRAM_disable();
|
|
193
|
+
if (m0 == 'H' && m1 == 'S' && ck == (u8)(lo ^ hi ^ 0xA5))
|
|
194
|
+
return ((u16)hi << 8) | lo;
|
|
195
|
+
return 0;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
static void record_save(u16 v) {
|
|
199
|
+
u8 lo = (u8)v, hi = (u8)(v >> 8);
|
|
200
|
+
SRAM_enable();
|
|
201
|
+
SRAM_writeByte(0, 'H');
|
|
202
|
+
SRAM_writeByte(1, 'S');
|
|
203
|
+
SRAM_writeByte(2, lo);
|
|
204
|
+
SRAM_writeByte(3, hi);
|
|
205
|
+
SRAM_writeByte(4, (u8)(lo ^ hi ^ 0xA5));
|
|
206
|
+
SRAM_disable();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* Format-on-first-boot: if the magic is absent (fresh battery), write a
|
|
210
|
+
* valid zero record immediately so the save file exists from frame one. */
|
|
211
|
+
static void record_init(void) {
|
|
212
|
+
best_streak = record_load();
|
|
213
|
+
if (best_streak == 0) record_save(0);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
217
|
+
* WINDOW-PLANE HUD — the fixed status bar. The window is a third tilemap
|
|
218
|
+
* that REPLACES plane A wherever it's shown and IGNORES ALL SCROLLING —
|
|
219
|
+
* a hardware-fixed HUD with zero per-frame cost. (The NES needs a sprite-0
|
|
220
|
+
* raster trick for this; on Genesis it's one register.)
|
|
221
|
+
* VDP_setWindowOnTop(2) shows it on the top 2 cell rows; text goes in with
|
|
222
|
+
* VDP_drawTextBG(WINDOW, ...). Two footguns:
|
|
223
|
+
* - The window only lives at screen edges (top/bottom N rows or left/
|
|
224
|
+
* right N columns) — it cannot float mid-screen.
|
|
225
|
+
* - It replaces plane A ONLY: plane B and sprites still render behind/
|
|
226
|
+
* over it. We paint plane B's top rows with a flat dark band so HUD
|
|
227
|
+
* text always reads, and nothing in the game flies above y=16
|
|
228
|
+
* (COURT_TOP is 24 — the top rail keeps the ball clear of the HUD). */
|
|
229
|
+
static void hud_init(void) {
|
|
230
|
+
VDP_setWindowOnTop(HUD_ROWS);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/* ── GAME LOGIC (clay) — HUD text (window plane, redrawn only on change) ── */
|
|
234
|
+
static void draw_u16(VDPPlane plane, u16 v, u16 x, u16 y) {
|
|
235
|
+
char buf[8];
|
|
236
|
+
uintToStr(v, buf, 5);
|
|
237
|
+
VDP_drawTextBG(plane, buf, x, y);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
static void draw_scores(void) {
|
|
241
|
+
char b[2] = { 0, 0 };
|
|
242
|
+
b[0] = '0' + score_p1;
|
|
243
|
+
VDP_drawTextBG(WINDOW, b, 5, 0);
|
|
244
|
+
b[0] = '0' + score_p2;
|
|
245
|
+
VDP_drawTextBG(WINDOW, b, 38, 0);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
static void draw_hud_play(void) {
|
|
249
|
+
VDP_clearTextAreaBG(WINDOW, 0, 0, 40, HUD_ROWS);
|
|
250
|
+
VDP_drawTextBG(WINDOW, "P1", 1, 0);
|
|
251
|
+
VDP_drawTextBG(WINDOW, two_player ? " P2" : "CPU", 33, 0);
|
|
252
|
+
VDP_drawTextBG(WINDOW, "BEST", 14, 0);
|
|
253
|
+
draw_u16(WINDOW, best_streak, 19, 0);
|
|
254
|
+
draw_scores();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
static void draw_hud_title(void) {
|
|
258
|
+
VDP_clearTextAreaBG(WINDOW, 0, 0, 40, HUD_ROWS);
|
|
259
|
+
VDP_drawTextBG(WINDOW, "BEST", 14, 0);
|
|
260
|
+
draw_u16(WINDOW, best_streak, 19, 0);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/* ── GAME LOGIC (clay) — paint the court (plane B, ONCE at boot) ──────────
|
|
264
|
+
* Painted once and never touched again — the frame loop does zero tilemap
|
|
265
|
+
* writes (rewriting tilemaps per frame is the #1 "choppy movement" bug). */
|
|
266
|
+
static void paint_court(void) {
|
|
267
|
+
u16 c, r;
|
|
268
|
+
/* Flat dark band behind the window HUD (rows 0-1). */
|
|
269
|
+
VDP_fillTileMapRect(BG_B, TILE_ATTR_FULL(PAL2, 0, 0, 0, T_BAND),
|
|
270
|
+
0, 0, 64, HUD_ROWS);
|
|
271
|
+
for (c = 0; c < 40; c++) {
|
|
272
|
+
VDP_setTileMapXY(BG_B, TILE_ATTR_FULL(PAL2, 0, 0, 0, T_RAIL), c, 2);
|
|
273
|
+
VDP_setTileMapXY(BG_B, TILE_ATTR_FULL(PAL2, 0, 0, 0, T_RAIL), c, 27);
|
|
70
274
|
}
|
|
275
|
+
for (r = 3; r < 27; r++)
|
|
276
|
+
for (c = 0; c < 40; c++)
|
|
277
|
+
VDP_setTileMapXY(BG_B, TILE_ATTR_FULL(PAL2, 0, 0, 0,
|
|
278
|
+
(c == NET_COL) ? T_NET : T_FLOOR), c, r);
|
|
71
279
|
}
|
|
72
280
|
|
|
73
|
-
|
|
74
|
-
static
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
281
|
+
/* ── GAME LOGIC (clay) — the title screen (text on plane A over the court) ── */
|
|
282
|
+
static void paint_title(void) {
|
|
283
|
+
VDP_clearPlane(BG_A, TRUE);
|
|
284
|
+
VDP_drawTextBG(BG_A, GAME_TITLE, (40 - (sizeof(GAME_TITLE) - 1)) / 2, 8);
|
|
285
|
+
VDP_drawTextBG(BG_A, "1P VS CPU - A", 13, 14);
|
|
286
|
+
VDP_drawTextBG(BG_A, "2P VERSUS - B", 13, 16);
|
|
287
|
+
VDP_drawTextBG(BG_A, "FIRST TO 5", 15, 19);
|
|
288
|
+
VDP_drawTextBG(BG_A, "UP DOWN MOVES YOUR PADDLE", 7, 22);
|
|
289
|
+
draw_hud_title();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/* ── GAME LOGIC (clay) — the result screen ── */
|
|
293
|
+
static void paint_over(void) {
|
|
294
|
+
char line[8];
|
|
295
|
+
VDP_clearPlane(BG_A, TRUE);
|
|
296
|
+
if (score_p1 >= WIN_SCORE)
|
|
297
|
+
VDP_drawTextBG(BG_A, "P1 WINS", 16, 8);
|
|
298
|
+
else
|
|
299
|
+
VDP_drawTextBG(BG_A, two_player ? "P2 WINS" : "CPU WINS", 16, 8);
|
|
300
|
+
line[0] = '0' + score_p1;
|
|
301
|
+
line[1] = ' '; line[2] = '-'; line[3] = ' ';
|
|
302
|
+
line[4] = '0' + score_p2;
|
|
303
|
+
line[5] = 0;
|
|
304
|
+
VDP_drawTextBG(BG_A, line, 17, 11);
|
|
305
|
+
if (new_record) VDP_drawTextBG(BG_A, "NEW RECORD", 15, 14);
|
|
306
|
+
VDP_drawTextBG(BG_A, "START - TITLE", 13, 21);
|
|
307
|
+
}
|
|
78
308
|
|
|
79
|
-
|
|
80
|
-
|
|
309
|
+
/* ── GAME LOGIC (clay) — serve: ball to centre, toward the chosen side.
|
|
310
|
+
* The serve angle takes a PRNG bit (not a fixed alternation) — one more
|
|
311
|
+
* place determinism is broken so idle matches can't settle into a cycle. */
|
|
312
|
+
static void serve_ball(u8 to_left) {
|
|
313
|
+
bx = SCREEN_W / 2 - BALL_W / 2;
|
|
81
314
|
by = (COURT_TOP + COURT_BOT) / 2;
|
|
82
315
|
bdx = to_left ? -2 : 2;
|
|
83
|
-
bdy = ((
|
|
84
|
-
serve_timer = 30;
|
|
316
|
+
bdy = (random8() & 1) ? -1 : 1;
|
|
317
|
+
serve_timer = 30; /* half-second breather */
|
|
85
318
|
}
|
|
86
319
|
|
|
87
|
-
|
|
320
|
+
/* ── GAME LOGIC (clay) — start a match ── */
|
|
321
|
+
static void start_match(u8 players) {
|
|
322
|
+
two_player = players;
|
|
88
323
|
p1y = (COURT_TOP + COURT_BOT) / 2 - PADDLE_H / 2;
|
|
89
324
|
p2y = p1y;
|
|
90
325
|
score_p1 = 0;
|
|
91
326
|
score_p2 = 0;
|
|
92
|
-
|
|
327
|
+
new_record = 0;
|
|
328
|
+
serve_ball(0);
|
|
329
|
+
VDP_clearPlane(BG_A, TRUE); /* drop the title text — court shows */
|
|
330
|
+
draw_hud_play();
|
|
331
|
+
sfx_tone(0, 523, 10); /* start jingle (C5) */
|
|
332
|
+
state = ST_PLAY;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/* ── GAME LOGIC (clay) — match over: result + record bookkeeping ── */
|
|
336
|
+
static void end_match(void) {
|
|
337
|
+
if (score_p1 >= WIN_SCORE && !two_player) {
|
|
338
|
+
++streak;
|
|
339
|
+
if (streak > best_streak) {
|
|
340
|
+
best_streak = streak;
|
|
341
|
+
new_record = 1;
|
|
342
|
+
record_save(best_streak); /* battery SRAM — see the SRAM idiom */
|
|
343
|
+
}
|
|
344
|
+
} else if (!two_player) {
|
|
345
|
+
streak = 0; /* the streak dies with the loss */
|
|
346
|
+
}
|
|
347
|
+
/* End-of-match whistle: two quick descending tones. */
|
|
348
|
+
sfx_tone(0, 380, 8);
|
|
349
|
+
sfx_tone(1, 570, 12);
|
|
350
|
+
paint_over();
|
|
351
|
+
prev_pad = 0xFFFF; /* swallow buttons held at match end */
|
|
352
|
+
state = ST_OVER;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/* ── GAME LOGIC (clay) — one point scored ── */
|
|
356
|
+
static void score_point(u8 for_p1) {
|
|
357
|
+
if (for_p1) ++score_p1; else ++score_p2;
|
|
358
|
+
sfx_noise(10);
|
|
359
|
+
draw_scores();
|
|
360
|
+
if (score_p1 >= WIN_SCORE || score_p2 >= WIN_SCORE) end_match();
|
|
361
|
+
else serve_ball(for_p1); /* winner of the point receives */
|
|
93
362
|
}
|
|
94
363
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
364
|
+
/* ── GAME LOGIC (clay) — paddle hit: deflect by where the ball struck.
|
|
365
|
+
* Centre = flat-ish, edges = steep. Max |bdy| is 2 — the CPU moves at 1,
|
|
366
|
+
* so an edge hit is exactly how a human beats it. The ±1 random "spin" on
|
|
367
|
+
* every return keeps rallies from repeating (see the PRNG note above). */
|
|
368
|
+
static void deflect(s16 paddle_y) {
|
|
369
|
+
s16 rel = (by + BALL_H / 2) - (paddle_y + PADDLE_H / 2);
|
|
370
|
+
bdy = rel >> 3;
|
|
371
|
+
bdy += (s16)(random8() & 2) - 1; /* spin: -1 or +1 */
|
|
372
|
+
if (bdy > 2) bdy = 2;
|
|
373
|
+
if (bdy < -2) bdy = -2;
|
|
374
|
+
if (bdy == 0) bdy = (rel < 0) ? -1 : 1; /* never return a flat ball */
|
|
375
|
+
sfx_tone(0, 280, 4);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/* ── GAME LOGIC (clay) — stage this frame's sprites ─────────────────────────
|
|
379
|
+
* Fixed SAT slots: 0-2 = P1 paddle, 3-5 = P2 paddle, 6 = ball. Hidden
|
|
380
|
+
* sprites park at y = -16 (above the screen). NEVER hide with x = -128..0 —
|
|
381
|
+
* a SAT x of 0 is the VDP's sprite-masking trigger and silently blanks
|
|
382
|
+
* every lower-priority sprite on those scanlines. */
|
|
383
|
+
#define HIDE_Y (-16)
|
|
384
|
+
static void stage_sprites(void) {
|
|
385
|
+
u16 i;
|
|
386
|
+
u8 actors = (state != ST_TITLE); /* paddles freeze on the result */
|
|
387
|
+
u8 ball_on = (state == ST_PLAY); /* the match ball went off-side */
|
|
388
|
+
for (i = 0; i < PADDLE_H / 8; i++) {
|
|
389
|
+
VDP_setSprite(0 + i, PADDLE_X1, actors ? p1y + (s16)(i * 8) : (s16)HIDE_Y,
|
|
390
|
+
SPRITE_SIZE(1, 1), TILE_ATTR_FULL(PAL0, 1, 0, 0, T_PADDLE));
|
|
391
|
+
VDP_setSprite(3 + i, PADDLE_X2, actors ? p2y + (s16)(i * 8) : (s16)HIDE_Y,
|
|
392
|
+
SPRITE_SIZE(1, 1), TILE_ATTR_FULL(PAL1, 1, 0, 0, T_PADDLE));
|
|
393
|
+
}
|
|
394
|
+
VDP_setSprite(6, bx, ball_on ? by : (s16)HIDE_Y,
|
|
395
|
+
SPRITE_SIZE(1, 1), TILE_ATTR_FULL(PAL0, 1, 0, 0, T_BALL));
|
|
396
|
+
/* ── HARDWARE IDIOM (load-bearing) — CHAIN the sprite list before
|
|
397
|
+
* uploading. VDP_setSprite does NOT set the SAT link byte, and link 0
|
|
398
|
+
* means "end of list": skip this and the VDP draws sprite 0 only.
|
|
399
|
+
* VDP_linkSprites(0, 7) links slots 0..6; the queued DMA flushes the
|
|
400
|
+
* 7 SAT entries during vblank. ── */
|
|
401
|
+
VDP_linkSprites(0, 7);
|
|
402
|
+
VDP_updateSprites(7, DMA_QUEUE);
|
|
101
403
|
}
|
|
102
404
|
|
|
103
405
|
int main(bool hard) {
|
|
406
|
+
u16 pad, pad2, fresh;
|
|
104
407
|
(void)hard;
|
|
105
408
|
|
|
106
|
-
/*
|
|
107
|
-
|
|
409
|
+
/* SRAM first — before any VDP work. The save file then exists within
|
|
410
|
+
* the game's first frames of life, which is what lets a frontend (or
|
|
411
|
+
* a headless host) see a non-empty save_ram region as early as
|
|
412
|
+
* possible (see the SRAM idiom note on gpgx's size scan). */
|
|
413
|
+
record_init();
|
|
414
|
+
streak = 0;
|
|
415
|
+
|
|
416
|
+
/* ── HARDWARE IDIOM (load-bearing — see TROUBLESHOOTING) ──
|
|
417
|
+
* Init order: tiles + palettes before the tilemaps that reference them,
|
|
418
|
+
* window size before window text. SGDK's boot already did the dangerous
|
|
419
|
+
* part (VDP regs, Z80, vblank int); this game never scrolls, so the
|
|
420
|
+
* default scroll mode + zero scroll values are exactly right. */
|
|
421
|
+
hud_init();
|
|
108
422
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
423
|
+
/* Palettes: PAL0 sprites + font, PAL1 the P2 paddle, PAL2 the court.
|
|
424
|
+
* Colours are BGR, 3 bits per channel: 0x0BGR with E = full.
|
|
425
|
+
* PAL0 colour 0 is also the BACKDROP — the court floor colour. */
|
|
426
|
+
PAL_setColor( 0, 0x0420); /* backdrop: dark navy court */
|
|
427
|
+
PAL_setColor( 1, 0x0EE2); /* P1 paddle volt cyan */
|
|
428
|
+
PAL_setColor( 4, 0x00EE); /* ball volt yellow */
|
|
429
|
+
PAL_setColor( 5, 0x08FF); /* ball highlight */
|
|
430
|
+
PAL_setColor(15, 0x0EEE); /* font white (index 15 = SGDK font colour) */
|
|
431
|
+
PAL_setColor(16 + 1, 0x022E); /* P2 paddle red */
|
|
432
|
+
PAL_setColor(32 + 1, 0x0CC4); /* rail cyan */
|
|
433
|
+
PAL_setColor(32 + 2, 0x0875); /* net — DIM (text overlaps it) */
|
|
434
|
+
PAL_setColor(32 + 3, 0x0641); /* floor speckle */
|
|
435
|
+
PAL_setColor(32 + 5, 0x0201); /* HUD band near-black */
|
|
113
436
|
|
|
114
|
-
|
|
115
|
-
|
|
437
|
+
VDP_loadTileData(tile_paddle, T_PADDLE, 1, DMA);
|
|
438
|
+
VDP_loadTileData(tile_ball, T_BALL, 1, DMA);
|
|
439
|
+
VDP_loadTileData(tile_rail, T_RAIL, 1, DMA);
|
|
440
|
+
VDP_loadTileData(tile_net, T_NET, 1, DMA);
|
|
441
|
+
VDP_loadTileData(tile_floor, T_FLOOR, 1, DMA);
|
|
442
|
+
VDP_loadTileData(tile_band, T_BAND, 1, DMA);
|
|
116
443
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
VDP_drawText("UP/DOWN MOVES YOUR PADDLE", 7, 27);
|
|
444
|
+
paint_court(); /* plane B: painted once, never again */
|
|
445
|
+
sfx_init(); /* PSG: sfx channels + background melody */
|
|
120
446
|
|
|
121
|
-
|
|
122
|
-
|
|
447
|
+
state = ST_TITLE;
|
|
448
|
+
prev_pad = 0xFFFF; /* swallow buttons held across power-on */
|
|
449
|
+
paint_title();
|
|
123
450
|
|
|
124
451
|
while (TRUE) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if ((p2 & BUTTON_DOWN) && p2y < COURT_BOT - PADDLE_H) p2y += 3;
|
|
138
|
-
} else {
|
|
139
|
-
s16 target = by - PADDLE_H / 2;
|
|
140
|
-
if (p2y < target && p2y < COURT_BOT - PADDLE_H) p2y += 2;
|
|
141
|
-
else if (p2y > target && p2y > COURT_TOP) p2y -= 2;
|
|
452
|
+
stage_sprites();
|
|
453
|
+
|
|
454
|
+
if (state == ST_TITLE) {
|
|
455
|
+
/* ── GAME LOGIC (clay) — title: A/START = 1P vs CPU, B = 2P ── */
|
|
456
|
+
pad = JOY_readJoypad(JOY_1);
|
|
457
|
+
fresh = pad & ~prev_pad;
|
|
458
|
+
prev_pad = pad;
|
|
459
|
+
if (fresh & BTN_1P) start_match(0);
|
|
460
|
+
else if (fresh & BTN_2P) start_match(1);
|
|
461
|
+
sfx_update();
|
|
462
|
+
SYS_doVBlankProcess();
|
|
463
|
+
continue;
|
|
142
464
|
}
|
|
143
465
|
|
|
144
|
-
if (
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
/* Paddle 1 (left) collision */
|
|
154
|
-
if (bdx < 0
|
|
155
|
-
&& bx <= PADDLE_X1 + PADDLE_W
|
|
156
|
-
&& bx + BALL_SIZE >= PADDLE_X1
|
|
157
|
-
&& by + BALL_SIZE > p1y
|
|
158
|
-
&& by < p1y + PADDLE_H) {
|
|
159
|
-
bdx = -bdx;
|
|
160
|
-
bx = PADDLE_X1 + PADDLE_W;
|
|
161
|
-
sfx_tone(1, 280, 3); /* paddle hit */
|
|
162
|
-
}
|
|
163
|
-
/* Paddle 2 (right) collision */
|
|
164
|
-
if (bdx > 0
|
|
165
|
-
&& bx + BALL_SIZE >= PADDLE_X2
|
|
166
|
-
&& bx <= PADDLE_X2 + PADDLE_W
|
|
167
|
-
&& by + BALL_SIZE > p2y
|
|
168
|
-
&& by < p2y + PADDLE_H) {
|
|
169
|
-
bdx = -bdx;
|
|
170
|
-
bx = PADDLE_X2 - BALL_SIZE;
|
|
171
|
-
sfx_tone(1, 280, 3);
|
|
466
|
+
if (state == ST_OVER) {
|
|
467
|
+
/* Result screen freezes the final scene; START or A → title. */
|
|
468
|
+
pad = JOY_readJoypad(JOY_1);
|
|
469
|
+
fresh = pad & ~prev_pad;
|
|
470
|
+
prev_pad = pad;
|
|
471
|
+
if (fresh & (BUTTON_START | BUTTON_A | BUTTON_C)) {
|
|
472
|
+
state = ST_TITLE;
|
|
473
|
+
prev_pad = 0xFFFF; /* swallow the held START */
|
|
474
|
+
paint_title();
|
|
172
475
|
}
|
|
476
|
+
sfx_update();
|
|
477
|
+
SYS_doVBlankProcess();
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
173
480
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
481
|
+
/* ── ST_PLAY ──────────────────────────────────────────────────── */
|
|
482
|
+
|
|
483
|
+
/* ── GAME LOGIC (clay) from here down ── */
|
|
484
|
+
random8(); /* tick the noise source every play frame */
|
|
485
|
+
|
|
486
|
+
/* P1 — controller 1, UP/DOWN. (prev_pad tracks through play so the
|
|
487
|
+
* result screen's edge-detect doesn't eat a held button.) */
|
|
488
|
+
pad = JOY_readJoypad(JOY_1);
|
|
489
|
+
prev_pad = pad;
|
|
490
|
+
if ((pad & BUTTON_UP) && p1y > COURT_TOP) p1y -= P1_SPEED;
|
|
491
|
+
if ((pad & BUTTON_DOWN) && p1y < COURT_BOT - PADDLE_H) p1y += P1_SPEED;
|
|
492
|
+
|
|
493
|
+
if (two_player) {
|
|
494
|
+
/* P2 — CONTROLLER 2, same speed: a fair simultaneous-versus
|
|
495
|
+
* match. (JOY_readJoypad(JOY_2) returns 0 with no pad in port
|
|
496
|
+
* 2 — the paddle just sits still; this mode is for two humans,
|
|
497
|
+
* the CPU lives in 1P mode.) */
|
|
498
|
+
pad2 = JOY_readJoypad(JOY_2);
|
|
499
|
+
if ((pad2 & BUTTON_UP) && p2y > COURT_TOP) p2y -= P1_SPEED;
|
|
500
|
+
if ((pad2 & BUTTON_DOWN) && p2y < COURT_BOT - PADDLE_H) p2y += P1_SPEED;
|
|
501
|
+
} else {
|
|
502
|
+
/* CPU — chases the ball centre at half player speed with a
|
|
503
|
+
* small dead zone. Beatable by design: steep edge deflections
|
|
504
|
+
* outrun it. */
|
|
505
|
+
s16 target = by + BALL_H / 2 - PADDLE_H / 2;
|
|
506
|
+
if (p2y + 2 < target && p2y < COURT_BOT - PADDLE_H) p2y += CPU_SPEED;
|
|
507
|
+
else if (p2y > target + 2 && p2y > COURT_TOP) p2y -= CPU_SPEED;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/* Ball update (frozen during the post-point serve pause). */
|
|
511
|
+
if (serve_timer > 0) {
|
|
512
|
+
--serve_timer;
|
|
513
|
+
sfx_update();
|
|
514
|
+
SYS_doVBlankProcess();
|
|
515
|
+
continue;
|
|
184
516
|
}
|
|
517
|
+
bx += bdx;
|
|
518
|
+
by += bdy;
|
|
185
519
|
|
|
186
|
-
/*
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
520
|
+
/* Rail bounce. */
|
|
521
|
+
if (by < COURT_TOP) { by = COURT_TOP; bdy = -bdy; sfx_tone(1, 350, 3); }
|
|
522
|
+
if (by + BALL_H > COURT_BOT) { by = COURT_BOT - BALL_H; bdy = -bdy; sfx_tone(1, 350, 3); }
|
|
523
|
+
|
|
524
|
+
/* Paddle collisions (direction-gated so the ball can't double-hit). */
|
|
525
|
+
if (bdx < 0
|
|
526
|
+
&& bx <= PADDLE_X1 + PADDLE_W && bx + BALL_W >= PADDLE_X1
|
|
527
|
+
&& by + BALL_H > p1y && by < p1y + PADDLE_H) {
|
|
528
|
+
bdx = -bdx;
|
|
529
|
+
bx = PADDLE_X1 + PADDLE_W;
|
|
530
|
+
deflect(p1y);
|
|
193
531
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
532
|
+
if (bdx > 0
|
|
533
|
+
&& bx + BALL_W >= PADDLE_X2 && bx <= PADDLE_X2 + PADDLE_W
|
|
534
|
+
&& by + BALL_H > p2y && by < p2y + PADDLE_H) {
|
|
535
|
+
bdx = -bdx;
|
|
536
|
+
bx = PADDLE_X2 - BALL_W;
|
|
537
|
+
deflect(p2y);
|
|
197
538
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
VDP_linkSprites(0, slot);
|
|
203
|
-
VDP_updateSprites(slot, DMA);
|
|
204
|
-
|
|
205
|
-
render_scores();
|
|
539
|
+
|
|
540
|
+
/* Off either side → point. */
|
|
541
|
+
if (bx < 4) score_point(0); /* past P1 → right side scores */
|
|
542
|
+
if (bx > SCREEN_W - 4) score_point(1); /* past P2 → P1 scores */
|
|
206
543
|
|
|
207
544
|
sfx_update();
|
|
208
545
|
SYS_doVBlankProcess();
|