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,215 +1,702 @@
|
|
|
1
|
-
/* ── platformer/main.c — MSX
|
|
1
|
+
/* ── platformer/main.c — MSX side-scrolling platformer (complete example game) ─
|
|
2
2
|
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* MESA HOPPER — a COMPLETE, working game: title screen, 1P mode and 2P
|
|
4
|
+
* ALTERNATING-TURNS mode (arcade-classic: players swap on death; each player
|
|
5
|
+
* has its OWN score and OWN 3 lives; player 2 plays on JOYSTICK PORT 2),
|
|
6
|
+
* coins + traversal scoring, session hi-score, music + SFX on the AY-3-8910
|
|
7
|
+
* PSG, gravity/jump/one-way-platform physics — and the MSX's signature
|
|
8
|
+
* SCREEN-2 PER-ROW COLOR: the level's depth bands (far sky, mid air, near
|
|
9
|
+
* ground) and the title recolor come ENTIRELY from the three independent
|
|
10
|
+
* color thirds + a one-tile vertical gradient, costing zero extra tiles.
|
|
5
11
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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 MSX footgun; reshape
|
|
15
|
+
* your gameplay around it (see TROUBLESHOOTING before changing).
|
|
16
|
+
* GAME LOGIC (clay) — level layout, physics tuning, scoring, art: reshape
|
|
17
|
+
* freely.
|
|
12
18
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
19
|
+
* What depends on what:
|
|
20
|
+
* msx_hw.h / msx_vdp.c — VDP + PSG + joystick helpers (direct Z80 ports;
|
|
21
|
+
* the PSG functions carry a DI/EI guard against the BIOS KEYINT race —
|
|
22
|
+
* read msx_vdp.c before adding your own PSG pokes).
|
|
23
|
+
* msx_crt0.s — the $4000 "AB" cart header + static-init copy. Load-bearing;
|
|
24
|
+
* INIT must NEVER return, so main() ends in for(;;).
|
|
15
25
|
*
|
|
16
|
-
*
|
|
26
|
+
* The level: a FIXED 32-cell-wide screen-2 arena (NOT a scroller — see the
|
|
27
|
+
* "no hardware scroll" idiom below). The column map gives every screen column
|
|
28
|
+
* a ground height + an optional one-way platform; pits (gaps in the ground)
|
|
29
|
+
* are instant death, spikes patrol the ground, a coin floats over a platform.
|
|
30
|
+
* The player walks the full width of the screen, hops platforms, and banks
|
|
31
|
+
* coins + a traversal "distance" tick. A run ends when all of the current
|
|
32
|
+
* player's lives are gone.
|
|
17
33
|
*
|
|
18
|
-
*
|
|
34
|
+
* Controls: JOYSTICK PORT 1 (or keyboard cursors) LEFT/RIGHT walks, trigger A
|
|
35
|
+
* jumps (only when grounded). In 2P alternating-turns mode, player 2 plays
|
|
36
|
+
* on JOYSTICK PORT 2 when it is their turn. On the title screen trigger A
|
|
37
|
+
* starts 1P; trigger B (port-1 button 2) starts 2P turns.
|
|
38
|
+
*
|
|
39
|
+
* Hi-score honesty: the bundled bluemsx core build exposes NO battery save
|
|
40
|
+
* path (retro_get_memory(SAVE_RAM) is unimplemented for MSX carts), so the
|
|
41
|
+
* hi-score lives in plain RAM: it survives title↔game cycles but NOT a
|
|
42
|
+
* power cycle / hardReset. Never fake persistence — if you need real saves,
|
|
43
|
+
* that's a future core round (ASCII8-SRAM mapper carts exist; the core just
|
|
44
|
+
* doesn't surface their RAM yet).
|
|
19
45
|
*/
|
|
20
46
|
#include "msx_hw.h"
|
|
21
47
|
|
|
22
|
-
/*
|
|
48
|
+
/* The title screen renders this — examples({op:'fork'}) stamps your game's
|
|
49
|
+
* name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
|
|
50
|
+
#define GAME_TITLE "MESA HOPPER"
|
|
51
|
+
|
|
52
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
53
|
+
* Interrupt-free vblank sync: poll VDP status S#0 bit 7 (port 0x99). Reading
|
|
54
|
+
* the port ALSO clears the flag, so one read per frame = one game step per
|
|
55
|
+
* frame. We deliberately do NOT use the BIOS JIFFY counter here: this poll
|
|
56
|
+
* works even with interrupts masked, and never depends on the BIOS ISR
|
|
57
|
+
* keeping pace. (The BIOS KEYINT also reads S#0 — on rare frames it eats the
|
|
58
|
+
* flag first and this loop just waits for the next one; a one-frame hiccup,
|
|
59
|
+
* never a hang.) */
|
|
23
60
|
__sfr __at 0x99 VDPSTATUS;
|
|
24
61
|
static void vsync(void) {
|
|
25
|
-
(void)VDPSTATUS;
|
|
62
|
+
(void)VDPSTATUS; /* throw away a possibly-stale flag */
|
|
26
63
|
while (!(VDPSTATUS & 0x80)) {
|
|
27
64
|
}
|
|
28
65
|
}
|
|
29
|
-
/* jump uses the BIOS GTTRIG wrapper (gttrig) provided by msx_hw.h. */
|
|
30
66
|
|
|
31
|
-
|
|
32
|
-
|
|
67
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
68
|
+
* NO HARDWARE SCROLL ON SCREEN 2. The TMS9918 GRAPHIC-II mode has no smooth
|
|
69
|
+
* pixel-scroll register at all (the V9938's R23 is a whole-screen vertical
|
|
70
|
+
* line shift, not a per-layer camera, and MSX1 lacks even that). The ONLY way
|
|
71
|
+
* to "scroll" is to rewrite the name table column-by-column every 8-px step —
|
|
72
|
+
* a heavy per-frame VRAM burst that ALSO fights the per-row color idiom below
|
|
73
|
+
* (each scrolled column needs its color third re-evaluated).
|
|
74
|
+
*
|
|
75
|
+
* So this platformer uses a FIXED single-screen arena: the camera never moves,
|
|
76
|
+
* the whole level is one 32-cell painting, and the player traverses it left to
|
|
77
|
+
* right. That keeps the screen-2 per-row color signature CHEAP (the color
|
|
78
|
+
* tables upload ONCE) and the frame budget roomy. If you want a true scroller,
|
|
79
|
+
* budget a column-streaming routine and re-upload the affected third's color
|
|
80
|
+
* slice as columns enter — see TROUBLESHOOTING; it is the single biggest MSX
|
|
81
|
+
* platformer footgun. */
|
|
82
|
+
|
|
83
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
84
|
+
* Tile font: index 0 = space, 1-26 = A-Z, 27-36 = 0-9, 37 = dash, then the
|
|
85
|
+
* level tiles. One 8x8 pattern = 8 bytes, one bit per pixel; set bits draw in
|
|
86
|
+
* the tile's FOREGROUND color, clear bits in its BACKGROUND color (both come
|
|
87
|
+
* from the screen-2 color table — see the per-row-color idiom below). */
|
|
88
|
+
#define T_SPACE 0
|
|
89
|
+
#define T_A 1 /* 'A'..'Z' = T_A + (c - 'A') */
|
|
90
|
+
#define T_0 27 /* '0'..'9' = T_0 + (c - '0') */
|
|
91
|
+
#define T_DASH 37
|
|
92
|
+
#define T_SKY 38 /* empty air cell (pattern all 0 = all bg) */
|
|
93
|
+
#define T_CLOUD 39 /* faint cloud puff in the sky band */
|
|
94
|
+
#define T_GRASS 40 /* platform / ground surface (grassy top) */
|
|
95
|
+
#define T_DIRT 41 /* ground body below the surface */
|
|
96
|
+
#define T_HORIZON 42 /* the per-8x1-row gradient strip (see below) */
|
|
97
|
+
#define NUM_TILES 43
|
|
98
|
+
|
|
99
|
+
static const uint8_t font[NUM_TILES][8] = {
|
|
100
|
+
/* SPACE */ {0,0,0,0,0,0,0,0},
|
|
101
|
+
/* 1 A */ {0x38,0x6C,0xC6,0xC6,0xFE,0xC6,0xC6,0x00},
|
|
102
|
+
/* 2 B */ {0xFC,0xC6,0xC6,0xFC,0xC6,0xC6,0xFC,0x00},
|
|
103
|
+
/* 3 C */ {0x7C,0xC6,0xC0,0xC0,0xC0,0xC6,0x7C,0x00},
|
|
104
|
+
/* 4 D */ {0xF8,0xCC,0xC6,0xC6,0xC6,0xCC,0xF8,0x00},
|
|
105
|
+
/* 5 E */ {0xFE,0xC0,0xC0,0xF8,0xC0,0xC0,0xFE,0x00},
|
|
106
|
+
/* 6 F */ {0xFE,0xC0,0xC0,0xF8,0xC0,0xC0,0xC0,0x00},
|
|
107
|
+
/* 7 G */ {0x7C,0xC6,0xC0,0xCE,0xC6,0xC6,0x7C,0x00},
|
|
108
|
+
/* 8 H */ {0xC6,0xC6,0xC6,0xFE,0xC6,0xC6,0xC6,0x00},
|
|
109
|
+
/* 9 I */ {0x7E,0x18,0x18,0x18,0x18,0x18,0x7E,0x00},
|
|
110
|
+
/* 10 J */ {0x1E,0x06,0x06,0x06,0xC6,0xC6,0x7C,0x00},
|
|
111
|
+
/* 11 K */ {0xC6,0xCC,0xD8,0xF0,0xD8,0xCC,0xC6,0x00},
|
|
112
|
+
/* 12 L */ {0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xFE,0x00},
|
|
113
|
+
/* 13 M */ {0xC6,0xEE,0xFE,0xD6,0xC6,0xC6,0xC6,0x00},
|
|
114
|
+
/* 14 N */ {0xC6,0xE6,0xF6,0xDE,0xCE,0xC6,0xC6,0x00},
|
|
115
|
+
/* 15 O */ {0x7C,0xC6,0xC6,0xC6,0xC6,0xC6,0x7C,0x00},
|
|
116
|
+
/* 16 P */ {0xFC,0xC6,0xC6,0xFC,0xC0,0xC0,0xC0,0x00},
|
|
117
|
+
/* 17 Q */ {0x7C,0xC6,0xC6,0xC6,0xD6,0xCC,0x76,0x00},
|
|
118
|
+
/* 18 R */ {0xFC,0xC6,0xC6,0xFC,0xD8,0xCC,0xC6,0x00},
|
|
119
|
+
/* 19 S */ {0x7C,0xC0,0xC0,0x78,0x0C,0x0C,0xF8,0x00},
|
|
120
|
+
/* 20 T */ {0x7E,0x18,0x18,0x18,0x18,0x18,0x18,0x00},
|
|
121
|
+
/* 21 U */ {0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0x7C,0x00},
|
|
122
|
+
/* 22 V */ {0xC6,0xC6,0xC6,0xC6,0x6C,0x38,0x10,0x00},
|
|
123
|
+
/* 23 W */ {0xC6,0xC6,0xC6,0xD6,0xFE,0xEE,0xC6,0x00},
|
|
124
|
+
/* 24 X */ {0xC6,0x6C,0x38,0x10,0x38,0x6C,0xC6,0x00},
|
|
125
|
+
/* 25 Y */ {0x66,0x66,0x66,0x3C,0x18,0x18,0x18,0x00},
|
|
126
|
+
/* 26 Z */ {0xFE,0x0C,0x18,0x30,0x60,0xC0,0xFE,0x00},
|
|
127
|
+
/* 27 0 */ {0x7C,0xCE,0xDE,0xF6,0xE6,0xC6,0x7C,0x00},
|
|
128
|
+
/* 28 1 */ {0x18,0x38,0x18,0x18,0x18,0x18,0x7E,0x00},
|
|
129
|
+
/* 29 2 */ {0x7C,0xC6,0x06,0x1C,0x70,0xC0,0xFE,0x00},
|
|
130
|
+
/* 30 3 */ {0x7C,0xC6,0x06,0x3C,0x06,0xC6,0x7C,0x00},
|
|
131
|
+
/* 31 4 */ {0x1C,0x3C,0x6C,0xCC,0xFE,0x0C,0x0C,0x00},
|
|
132
|
+
/* 32 5 */ {0xFE,0xC0,0xFC,0x06,0x06,0xC6,0x7C,0x00},
|
|
133
|
+
/* 33 6 */ {0x3C,0x60,0xC0,0xFC,0xC6,0xC6,0x7C,0x00},
|
|
134
|
+
/* 34 7 */ {0xFE,0x06,0x0C,0x18,0x30,0x30,0x30,0x00},
|
|
135
|
+
/* 35 8 */ {0x7C,0xC6,0xC6,0x7C,0xC6,0xC6,0x7C,0x00},
|
|
136
|
+
/* 36 9 */ {0x7C,0xC6,0xC6,0x7E,0x06,0x0C,0x78,0x00},
|
|
137
|
+
/* 37 - */ {0x00,0x00,0x00,0x7E,0x00,0x00,0x00,0x00},
|
|
138
|
+
/* 38 SKY (all bg) */ {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
|
|
139
|
+
/* 39 CLOUD (soft puff) */ {0x00,0x00,0x18,0x3C,0x7E,0x00,0x00,0x00},
|
|
140
|
+
/* 40 GRASS (grassy top) */ {0xFF,0xFF,0x00,0x00,0x00,0x00,0x00,0x00},
|
|
141
|
+
/* 41 DIRT (solid body) */ {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF},
|
|
142
|
+
/* 42 HORIZON(solid fg — its COLOR bytes paint the gradient) */
|
|
143
|
+
{0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF},
|
|
144
|
+
};
|
|
33
145
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
146
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
147
|
+
* SCREEN-2 PER-ROW COLOR — the MSX's signature background trick.
|
|
148
|
+
*
|
|
149
|
+
* Screen 2 (GRAPHIC II) is NOT "one color byte per tile" like most consoles:
|
|
150
|
+
*
|
|
151
|
+
* 1. The 256x192 screen is THREE INDEPENDENT THIRDS of 8 rows each
|
|
152
|
+
* (name-table rows 0-7, 8-15, 16-23). Each third has its OWN 2KB
|
|
153
|
+
* pattern table slice and its OWN 2KB color table slice:
|
|
154
|
+
* patterns: VRAM_PATTERN + third*0x800, colors: VRAM_COLOR + third*0x800
|
|
155
|
+
* The SAME tile index can look completely different in each third —
|
|
156
|
+
* we exploit exactly that for the depth-banded level below.
|
|
157
|
+
*
|
|
158
|
+
* 2. Within a tile, the color table holds EIGHT bytes — one per 8x1 pixel
|
|
159
|
+
* row — each packing (foreground<<4)|background from the fixed TMS9918
|
|
160
|
+
* palette. So one tile can carry an 8-color vertical gradient
|
|
161
|
+
* (T_HORIZON's whole "glow horizon" line is a single tile, colors only).
|
|
162
|
+
*
|
|
163
|
+
* Requires: the screen-2 table layout set by msx_set_screen2() (R3=0xFF,
|
|
164
|
+
* R4=0x03 — the "thirds" configuration), and pattern + color uploads to
|
|
165
|
+
* EVERY third a tile is used in. Tile N's slot is pattern[N*8] / color[N*8].
|
|
166
|
+
*
|
|
167
|
+
* Depth scheme taught here (TMS9918 fixed palette: 1 black, 4 dark blue,
|
|
168
|
+
* 5 light blue, 6 dark red, 8 cyan/medium-red, 12 dark green, 14 gray,
|
|
169
|
+
* 15 white — the high nibble is fg, low nibble is bg of each row byte):
|
|
170
|
+
* third 0 (top) = far sky: light-blue field, white clouds — the HUD
|
|
171
|
+
* text band (row 0) also lives here in its own colors.
|
|
172
|
+
* third 1 (middle) = mid air: black field, gray clouds — the play space.
|
|
173
|
+
* third 2 (bottom) = near ground: green grass on brown dirt, the platforms
|
|
174
|
+
* and the one-tile horizon gradient seam.
|
|
175
|
+
* The HUD text band (row 0, third 0) gets white-on-blue, distinct from the
|
|
176
|
+
* sky below it, WITHOUT costing any extra tiles. */
|
|
177
|
+
static const uint8_t col_text[3] = { 0xF4, 0xF1, 0xF1 }; /* HUD/title white-on-blue; mid+near white-on-black */
|
|
178
|
+
static const uint8_t col_sky[3] = { 0x55, 0x11, 0x18 }; /* the 3 depth bands (bg shows: pattern is all 0) */
|
|
179
|
+
static const uint8_t col_cloud[3] = { 0xF5, 0xE1, 0x55 }; /* cloud puff per band: white/gray over its band bg */
|
|
180
|
+
static const uint8_t col_grass[3] = { 0xC5, 0xC1, 0xC6 }; /* grassy top: dark-green fg over band bg / over brown */
|
|
181
|
+
static const uint8_t col_dirt[3] = { 0x66, 0x66, 0x66 }; /* solid dirt body: dark-red/brown everywhere */
|
|
182
|
+
/* T_HORIZON: 8 DIFFERENT color bytes inside ONE tile = an 8-pixel-row glow
|
|
183
|
+
* gradient (dark blue → light blue → cyan → white and back down). The pattern
|
|
184
|
+
* is solid 0xFF so only the fg nibbles show. Drawn as the seam row between the
|
|
185
|
+
* air thirds and the ground. */
|
|
186
|
+
static const uint8_t col_horizon[8] = { 0x44,0x55,0x88,0xF8,0x85,0x54,0x41,0x11 };
|
|
38
187
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
188
|
+
static void load_tiles(void) {
|
|
189
|
+
uint8_t third, i;
|
|
190
|
+
uint16_t patbase, colbase;
|
|
191
|
+
for (third = 0; third < 3; third++) {
|
|
192
|
+
patbase = (uint16_t)(VRAM_PATTERN + ((uint16_t)third << 11));
|
|
193
|
+
colbase = (uint16_t)(VRAM_COLOR + ((uint16_t)third << 11));
|
|
194
|
+
for (i = 0; i < NUM_TILES; i++) {
|
|
195
|
+
uint8_t col;
|
|
196
|
+
/* pattern bits are the same in every third — only COLOR varies */
|
|
197
|
+
msx_vram_write((uint16_t)(patbase + ((uint16_t)i << 3)), font[i], 8);
|
|
198
|
+
if (i == T_HORIZON) { /* the one per-pixel-row gradient */
|
|
199
|
+
msx_vram_write((uint16_t)(colbase + ((uint16_t)i << 3)), col_horizon, 8);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (i == T_SKY) col = col_sky[third];
|
|
203
|
+
else if (i == T_CLOUD) col = col_cloud[third];
|
|
204
|
+
else if (i == T_GRASS) col = col_grass[third];
|
|
205
|
+
else if (i == T_DIRT) col = col_dirt[third];
|
|
206
|
+
else col = col_text[third];
|
|
207
|
+
msx_fill_vram((uint16_t)(colbase + ((uint16_t)i << 3)), 8, col);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
42
211
|
|
|
43
|
-
|
|
44
|
-
|
|
212
|
+
/* ── GAME LOGIC (clay — reshape freely) — name-table drawing helpers ────────
|
|
213
|
+
* Screen 2 VRAM writes are safe at any point in the frame at C speed: the
|
|
214
|
+
* TMS9918 needs ~29 Z80 cycles between VRAM accesses during active display,
|
|
215
|
+
* and SDCC-compiled loops are slower than that. (Hand-tuned asm OTIR bursts
|
|
216
|
+
* are the thing that outruns the VDP — see TROUBLESHOOTING.) */
|
|
217
|
+
static void put_tile(uint8_t col, uint8_t row, uint8_t tile) {
|
|
218
|
+
msx_vram_write((uint16_t)(VRAM_NAME + (uint16_t)row * 32 + col), &tile, 1);
|
|
219
|
+
}
|
|
45
220
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
221
|
+
static void draw_text(uint8_t col, uint8_t row, const char *s) {
|
|
222
|
+
uint8_t buf[32];
|
|
223
|
+
uint8_t n = 0;
|
|
224
|
+
while (*s && n < 32) {
|
|
225
|
+
char c = *s++;
|
|
226
|
+
if (c >= 'A' && c <= 'Z') buf[n] = (uint8_t)(T_A + c - 'A');
|
|
227
|
+
else if (c >= '0' && c <= '9') buf[n] = (uint8_t)(T_0 + c - '0');
|
|
228
|
+
else if (c == '-') buf[n] = T_DASH;
|
|
229
|
+
else buf[n] = T_SPACE;
|
|
230
|
+
n++;
|
|
231
|
+
}
|
|
232
|
+
msx_vram_write((uint16_t)(VRAM_NAME + (uint16_t)row * 32 + col), buf, n);
|
|
233
|
+
}
|
|
49
234
|
|
|
50
|
-
|
|
235
|
+
static void draw_num4(uint8_t col, uint8_t row, uint16_t v) {
|
|
236
|
+
uint8_t buf[4];
|
|
237
|
+
buf[0] = (uint8_t)(T_0 + (v / 1000) % 10);
|
|
238
|
+
buf[1] = (uint8_t)(T_0 + (v / 100) % 10);
|
|
239
|
+
buf[2] = (uint8_t)(T_0 + (v / 10) % 10);
|
|
240
|
+
buf[3] = (uint8_t)(T_0 + v % 10);
|
|
241
|
+
msx_vram_write((uint16_t)(VRAM_NAME + (uint16_t)row * 32 + col), buf, 4);
|
|
242
|
+
}
|
|
51
243
|
|
|
52
|
-
/*
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
244
|
+
/* ── GAME LOGIC (clay — reshape freely) — the level (a fixed 32-cell map) ───
|
|
245
|
+
* One entry per screen column:
|
|
246
|
+
* ground_row[c] — name-table row of the ground's grassy top; NO_GROUND = a
|
|
247
|
+
* bottomless pit (fall in = lose the turn).
|
|
248
|
+
* plat_row[c] — row of a one-way floating platform, 0 = none.
|
|
249
|
+
* Name-table rows: 0 = HUD band, 1..GROUND_ROW-1 = air, then ground.
|
|
250
|
+
* The level is one fixed screen — reshape these two tables freely. */
|
|
251
|
+
#define NO_GROUND 0xFF
|
|
252
|
+
#define HORIZON_ROW 17 /* the one-tile gradient seam row */
|
|
253
|
+
#define GROUND_ROW 20 /* default grassy ground top */
|
|
254
|
+
static const uint8_t ground_row[32] = {
|
|
255
|
+
20, 20, 20, 20, 20, 20, /* start ledge */
|
|
256
|
+
NO_GROUND, NO_GROUND, NO_GROUND, /* pit 1 (24 px) */
|
|
257
|
+
20, 20, 20, 20, 20, /* mid ledge */
|
|
258
|
+
NO_GROUND, NO_GROUND, /* pit 2 (16 px) */
|
|
259
|
+
20, 20, 20, 20, 20, 20, /* long ledge */
|
|
260
|
+
NO_GROUND, NO_GROUND, NO_GROUND, /* pit 3 (24 px) */
|
|
261
|
+
20, 20, 20, 20, 20, 20, /* finish ledge */
|
|
262
|
+
};
|
|
263
|
+
static const uint8_t plat_row[32] = {
|
|
264
|
+
0, 0, 0, 0, 0, 0,
|
|
265
|
+
0, 14, 14, /* slab spanning pit 1 */
|
|
266
|
+
0, 0, 0, 0, 0,
|
|
267
|
+
13, 13, /* slab over pit 2 */
|
|
268
|
+
0, 0, 0, 0, 0, 0,
|
|
269
|
+
0, 14, 14, /* slab over pit 3 */
|
|
270
|
+
0, 0, 0, 0, 0, 0,
|
|
63
271
|
};
|
|
64
|
-
#define N_PLATFORMS (sizeof(platforms) / sizeof(platforms[0]))
|
|
65
272
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
static
|
|
273
|
+
/* ── GAME LOGIC (clay — reshape freely) — sprites ────────────────────────────
|
|
274
|
+
* 8x8 one-color hardware sprites. Plane layout (lower plane = on top):
|
|
275
|
+
* 0 player, 1 coin, 2-3 spikes. */
|
|
276
|
+
static const uint8_t spr_player_idle[8] = {0x18,0x3C,0x7E,0x7E,0xFF,0xFF,0x66,0x66};
|
|
277
|
+
static const uint8_t spr_player_jump[8] = {0x18,0x7E,0xFF,0xE7,0xC3,0x81,0x42,0x24};
|
|
278
|
+
static const uint8_t spr_coin[8] = {0x3C,0x7E,0xDB,0xFF,0xFF,0xDB,0x7E,0x3C};
|
|
279
|
+
static const uint8_t spr_spike[8] = {0x10,0x10,0x38,0x38,0x7C,0x7C,0xFE,0xFE};
|
|
280
|
+
#define PAT_IDLE 0
|
|
281
|
+
#define PAT_JUMP 1
|
|
282
|
+
#define PAT_COIN 2
|
|
283
|
+
#define PAT_SPIKE 3
|
|
284
|
+
#define COL_PLAYER1 15 /* white */
|
|
285
|
+
#define COL_PLAYER2 10 /* dark yellow */
|
|
286
|
+
#define COL_COIN 10 /* dark yellow (gold-ish) */
|
|
287
|
+
#define COL_SPIKE 9 /* light red */
|
|
288
|
+
|
|
289
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
290
|
+
* Sprite limits + the Y=208 terminator:
|
|
291
|
+
* - A sprite Y of 0xD0 (208) tells the TMS9918 to STOP SCANNING the
|
|
292
|
+
* attribute table — every higher-numbered plane vanishes, not just that
|
|
293
|
+
* one. (msx_clear_sprites parks ALL planes at 0xD0, which is fine at the
|
|
294
|
+
* END of the list.) To hide ONE sprite mid-list, park it OFFSCREEN at
|
|
295
|
+
* PARK_Y (192 = first line below the display) — never at 0xD0.
|
|
296
|
+
* (On MSX2's V9938 sprite mode 2 the terminator moves to 0xD8 and 0xD0
|
|
297
|
+
* is "just offscreen" — code that leans on that breaks on MSX1.)
|
|
298
|
+
* - Per scanline the TMS9918 draws only 4 sprites (V9938: 8); the rest drop
|
|
299
|
+
* out for that line. This game peaks at 4 planes (player + coin + 2
|
|
300
|
+
* spikes) so a row pileup is rare. */
|
|
301
|
+
#define PARK_Y 192
|
|
302
|
+
|
|
303
|
+
#define NUM_SPIKES 2
|
|
304
|
+
|
|
305
|
+
/* ── GAME LOGIC (clay — reshape freely) — physics + tuning ──────────────────
|
|
306
|
+
* Player position is screen-pixel X; Y is Q4.4 fixed point so gravity can add
|
|
307
|
+
* <1 px/frame near the jump apex. */
|
|
308
|
+
#define GRAVITY_Q44 6 /* +6/16 px per frame per frame */
|
|
309
|
+
#define JUMP_VEL_Q44 (-56) /* launch vy (Q4.4) → ~4-tile apex */
|
|
310
|
+
#define MAX_VY_Q44 64 /* terminal velocity, 4 px/frame — MUST stay *
|
|
311
|
+
* under 6: the landing probe's window can't *
|
|
312
|
+
* catch a faster fall (tunnelling) */
|
|
313
|
+
#define MOVE_SPEED 2 /* px/frame walk */
|
|
314
|
+
#define GROUND_TOP (GROUND_ROW * 8) /* 160: grassy top pixel row */
|
|
315
|
+
#define PLAYER_LEFT 8 /* walk bounds */
|
|
316
|
+
#define PLAYER_RIGHT 240
|
|
317
|
+
|
|
318
|
+
/* ── GAME LOGIC (clay — reshape freely) — game state ─────────────────────── */
|
|
319
|
+
static uint8_t px; /* player screen x (pixels) */
|
|
320
|
+
static uint16_t py_q44; /* player y, Q4.4 fixed point */
|
|
321
|
+
static int8_t vy_q44;
|
|
322
|
+
static uint8_t on_ground;
|
|
323
|
+
static uint8_t coin_x, coin_y, coin_live;
|
|
324
|
+
static uint8_t spike_x[NUM_SPIKES];
|
|
325
|
+
static int8_t spike_vx[NUM_SPIKES];
|
|
326
|
+
|
|
327
|
+
/* Players: index 0 = P1 (joystick port 1), 1 = P2 (joystick port 2 —
|
|
328
|
+
* alternating turns, arcade-classic style). Each has its own score + own
|
|
329
|
+
* lives; the HUD shows the CURRENT player's numbers. */
|
|
330
|
+
static uint8_t two_player; /* mode chosen on the title screen */
|
|
331
|
+
static uint8_t cur_player;
|
|
332
|
+
static uint8_t p_lives[2];
|
|
333
|
+
static uint16_t p_score[2];
|
|
334
|
+
static uint16_t hiscore; /* SESSION-ONLY: plain RAM. The bundled
|
|
335
|
+
* bluemsx build exposes no SAVE_RAM region,
|
|
336
|
+
* so there is nothing battery-backed to
|
|
337
|
+
* write — survives title↔game cycles, not a
|
|
338
|
+
* power cycle (honest, not faked). */
|
|
339
|
+
static uint8_t turn_pause; /* freeze frames after a turn change */
|
|
340
|
+
static uint8_t dist_sub; /* sub-counter: traversal pays a point */
|
|
341
|
+
static uint16_t rng;
|
|
342
|
+
|
|
343
|
+
#define ST_TITLE 0
|
|
344
|
+
#define ST_PLAY 1
|
|
345
|
+
#define ST_OVER 2
|
|
346
|
+
static uint8_t state;
|
|
347
|
+
static uint8_t prev_t1, prev_t2; /* trigger edge detection across states */
|
|
348
|
+
static uint8_t prev_jump; /* per-turn jump edge (cur_player's port) */
|
|
349
|
+
|
|
350
|
+
/* xorshift16 PRNG — a few dozen cycles, no tables. */
|
|
351
|
+
static uint8_t next_rand(void) {
|
|
352
|
+
rng ^= (uint16_t)(rng << 7);
|
|
353
|
+
rng ^= (uint16_t)(rng >> 9);
|
|
354
|
+
rng ^= (uint16_t)(rng << 8);
|
|
355
|
+
return (uint8_t)(rng & 0xFF);
|
|
356
|
+
}
|
|
70
357
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
358
|
+
/* ── GAME LOGIC (clay — reshape freely) — music + SFX on the AY-3-8910 ──────
|
|
359
|
+
* Channel plan: A = jump/coin/land blips, B = death noise, C = music. The PSG
|
|
360
|
+
* has 3 tone channels + ONE shared noise generator, mixed per-channel in
|
|
361
|
+
* reg 7. All register traffic goes through msx_psg_tone/noise/off — they wrap
|
|
362
|
+
* the PSGADDR/PSGWRITE pair in DI/EI because the BIOS KEYINT ISR clobbers the
|
|
363
|
+
* PSG address latch every frame (the bug that once silenced every MSX scaffold
|
|
364
|
+
* — see msx_vdp.c).
|
|
365
|
+
*
|
|
366
|
+
* The tune: one period entry per half-beat, 0 = rest. AY period =
|
|
367
|
+
* 1789773 / (16 * freq) — e.g. A4 (440Hz) -> 254. Ticked once per frame; a
|
|
368
|
+
* note advances every 8 frames. The lib's built-in demo loop (msx_music_tick)
|
|
369
|
+
* also uses channel C, so we switch it OFF in main() and run THIS table
|
|
370
|
+
* instead — edit this table to rescore. */
|
|
371
|
+
static const uint16_t tune[32] = {
|
|
372
|
+
427, 0, 339, 0, 285, 0, 339, 0, /* C4 E4 G4 E4 (bright major bounce) */
|
|
373
|
+
254, 0, 285, 339, 285, 0, 0, 0, /* A4 G4 E4 G4 rest */
|
|
374
|
+
320, 0, 285, 0, 254, 0, 285, 0, /* F4 G4 A4 G4 */
|
|
375
|
+
339, 0, 285, 0, 427, 0, 0, 0, /* E4 G4 C4 rest */
|
|
376
|
+
};
|
|
377
|
+
static uint8_t music_step, music_timer;
|
|
378
|
+
static uint8_t sfx_a_t, sfx_b_t; /* frames left on the A/B SFX channels */
|
|
379
|
+
|
|
380
|
+
static void music_tick(void) {
|
|
381
|
+
if (music_timer == 0) {
|
|
382
|
+
uint16_t p = tune[music_step & 31];
|
|
383
|
+
if (p) msx_psg_tone(2, p, 9);
|
|
384
|
+
else msx_psg_off(2);
|
|
385
|
+
music_step++;
|
|
80
386
|
}
|
|
387
|
+
music_timer++;
|
|
388
|
+
if (music_timer >= 8) music_timer = 0;
|
|
81
389
|
}
|
|
82
390
|
|
|
83
|
-
static
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
391
|
+
static void sfx_tick(void) {
|
|
392
|
+
if (sfx_a_t) { sfx_a_t--; if (!sfx_a_t) msx_psg_off(0); }
|
|
393
|
+
if (sfx_b_t) { sfx_b_t--; if (!sfx_b_t) msx_psg_noise(1, 0, 0); }
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
static void sfx_jump(void) { msx_psg_tone(0, 0x110, 11); sfx_a_t = 4; }
|
|
397
|
+
static void sfx_coin(void) { msx_psg_tone(0, 0x090, 12); sfx_a_t = 6; }
|
|
398
|
+
static void sfx_land(void) { msx_psg_tone(0, 0x250, 8); sfx_a_t = 3; }
|
|
399
|
+
static void sfx_die(void) { msx_psg_noise(1, 24, 14); sfx_b_t = 18; }
|
|
400
|
+
|
|
401
|
+
/* ── GAME LOGIC (clay — reshape freely) — HUD ──────────────────────────────
|
|
402
|
+
* Row 0 = the HUD band (third 0's text colors make it a distinct strip).
|
|
403
|
+
* P#=current player, SC=score, HI=hi-score, LV=lives. */
|
|
404
|
+
static void draw_hud_labels(void) {
|
|
405
|
+
draw_text(1, 0, "P");
|
|
406
|
+
draw_text(5, 0, "SC");
|
|
407
|
+
draw_text(15, 0, "HI");
|
|
408
|
+
draw_text(25, 0, "LV");
|
|
409
|
+
}
|
|
410
|
+
static void draw_player(void) { put_tile(2, 0, (uint8_t)(T_0 + 1 + cur_player)); }
|
|
411
|
+
static void draw_score(void) { draw_num4(8, 0, p_score[cur_player]); }
|
|
412
|
+
static void draw_hi(void) { draw_num4(18, 0, hiscore); }
|
|
413
|
+
static void draw_lives(void) { put_tile(28, 0, (uint8_t)(T_0 + p_lives[cur_player])); }
|
|
414
|
+
static void draw_hud(void) { draw_player(); draw_score(); draw_hi(); draw_lives(); }
|
|
415
|
+
|
|
416
|
+
/* ── GAME LOGIC (clay — reshape freely) — paint the fixed level ─────────────
|
|
417
|
+
* The whole arena is one 32x24 painting. Row 0 is the HUD band; the sky thirds
|
|
418
|
+
* get scattered clouds; HORIZON_ROW is the one-tile gradient seam; the
|
|
419
|
+
* ground/platforms come from the column map. The per-third color tables (set
|
|
420
|
+
* once in load_tiles) give the depth bands for free. */
|
|
421
|
+
static void paint_field(void) {
|
|
422
|
+
uint8_t row, col, t;
|
|
423
|
+
uint8_t buf[32];
|
|
424
|
+
msx_fill_vram(VRAM_NAME, 32, T_SPACE); /* row 0: HUD band */
|
|
425
|
+
for (row = 1; row < 24; row++) {
|
|
426
|
+
for (col = 0; col < 32; col++) {
|
|
427
|
+
uint8_t g = ground_row[col];
|
|
428
|
+
t = T_SKY;
|
|
429
|
+
if (row == HORIZON_ROW) t = T_HORIZON;
|
|
430
|
+
else if (row == plat_row[col]) t = T_GRASS; /* slab */
|
|
431
|
+
else if (g != NO_GROUND) {
|
|
432
|
+
if (row == g) t = T_GRASS; /* surface */
|
|
433
|
+
else if (row > g) t = T_DIRT; /* body */
|
|
434
|
+
}
|
|
435
|
+
if (t == T_SKY && row < HORIZON_ROW) { /* clouds */
|
|
436
|
+
if (((row * 7 + col * 5) & 15) == 0) t = T_CLOUD;
|
|
437
|
+
}
|
|
438
|
+
buf[col] = t;
|
|
439
|
+
}
|
|
440
|
+
msx_vram_write((uint16_t)(VRAM_NAME + (uint16_t)row * 32), buf, 32);
|
|
92
441
|
}
|
|
93
|
-
return 0;
|
|
94
442
|
}
|
|
95
443
|
|
|
96
|
-
|
|
444
|
+
/* ── GAME LOGIC (clay — reshape freely) — screens ──────────────────────────
|
|
445
|
+
* Title rows land in third 1 (white-on-black) and third 2 — the same glyph
|
|
446
|
+
* tiles as the HUD, recolored for free by the thirds idiom. */
|
|
447
|
+
static void paint_title(void) {
|
|
448
|
+
uint8_t len = 0, col;
|
|
449
|
+
const char *p = GAME_TITLE;
|
|
450
|
+
while (*p++) len++;
|
|
451
|
+
col = (uint8_t)((32 - len) / 2);
|
|
452
|
+
paint_field();
|
|
453
|
+
draw_text(col, 6, GAME_TITLE);
|
|
454
|
+
draw_text(7, 10, "1P START - FIRE A");
|
|
455
|
+
draw_text(7, 12, "2P TURNS - FIRE B");
|
|
456
|
+
draw_text(12, 14, "HI 0000"); /* the space blanks the cell between */
|
|
457
|
+
draw_num4(15, 14, hiscore);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/* ── GAME LOGIC (clay — reshape freely) — start a turn / a run ── */
|
|
461
|
+
static void begin_turn(void) {
|
|
97
462
|
uint8_t i;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
463
|
+
px = 24;
|
|
464
|
+
py_q44 = (uint16_t)((uint16_t)(GROUND_TOP - 8) << 4);
|
|
465
|
+
vy_q44 = 0;
|
|
466
|
+
on_ground = 1;
|
|
467
|
+
coin_x = 120; coin_y = (uint8_t)(13 * 8); coin_live = 1;
|
|
468
|
+
for (i = 0; i < NUM_SPIKES; i++) {
|
|
469
|
+
spike_x[i] = (uint8_t)(64 + i * 96);
|
|
470
|
+
spike_vx[i] = (i & 1) ? -1 : 1;
|
|
102
471
|
}
|
|
103
|
-
|
|
472
|
+
turn_pause = 24; /* "P# ready" breather (blinks ~0.6-0.8s) */
|
|
473
|
+
prev_jump = 1; /* swallow a held jump across the turn */
|
|
474
|
+
paint_field();
|
|
475
|
+
draw_hud_labels();
|
|
476
|
+
draw_hud();
|
|
104
477
|
}
|
|
105
478
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
479
|
+
static void start_game(uint8_t players) {
|
|
480
|
+
two_player = players;
|
|
481
|
+
cur_player = 0;
|
|
482
|
+
p_score[0] = p_score[1] = 0;
|
|
483
|
+
p_lives[0] = 3;
|
|
484
|
+
p_lives[1] = players ? 3 : 0;
|
|
485
|
+
dist_sub = 0;
|
|
486
|
+
begin_turn();
|
|
487
|
+
sfx_coin(); /* start chirp */
|
|
488
|
+
state = ST_PLAY;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
static void game_over(void) {
|
|
492
|
+
uint16_t best = p_score[0];
|
|
493
|
+
if (two_player && p_score[1] > best) best = p_score[1];
|
|
494
|
+
if (best > hiscore) { hiscore = best; }
|
|
495
|
+
draw_text(11, 9, "GAME OVER");
|
|
496
|
+
draw_text(9, 11, "P1"); draw_num4(13, 11, p_score[0]);
|
|
497
|
+
if (two_player) { draw_text(9, 13, "P2"); draw_num4(13, 13, p_score[1]); }
|
|
498
|
+
draw_text(8, 15, "FIRE FOR TITLE");
|
|
499
|
+
prev_t1 = prev_t2 = 1; /* swallow a fire still held from play */
|
|
500
|
+
state = ST_OVER;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/* ── GAME LOGIC (clay — reshape freely) — death + alternating-turn handoff ── */
|
|
504
|
+
static void kill_player(void) {
|
|
505
|
+
uint8_t other;
|
|
506
|
+
sfx_die();
|
|
507
|
+
if (p_lives[cur_player] > 0) --p_lives[cur_player];
|
|
508
|
+
if (two_player) {
|
|
509
|
+
other = (uint8_t)(cur_player ^ 1);
|
|
510
|
+
if (p_lives[other] > 0) cur_player = other; /* swap turn */
|
|
511
|
+
else if (p_lives[cur_player] == 0) { game_over(); return; }
|
|
512
|
+
} else if (p_lives[0] == 0) {
|
|
513
|
+
game_over();
|
|
514
|
+
return;
|
|
116
515
|
}
|
|
516
|
+
begin_turn();
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/* ── GAME LOGIC (clay — reshape freely) — landing probe against the map ─────
|
|
520
|
+
* One-way platforms: only catch the player's feet while FALLING through a
|
|
521
|
+
* narrow window at a surface top. Window is top-1..top+4 (the -1 slack keeps
|
|
522
|
+
* on_ground stable while the sub-pixel gravity trickle hasn't moved integer Y
|
|
523
|
+
* yet; the +4 stops a 4 px/frame fall stepping over a 1-row surface). */
|
|
524
|
+
static uint8_t land_top(uint8_t c, uint8_t feet) {
|
|
525
|
+
uint8_t r, top;
|
|
526
|
+
r = plat_row[c];
|
|
527
|
+
if (r) {
|
|
528
|
+
top = (uint8_t)(r << 3);
|
|
529
|
+
if ((uint8_t)(feet + 1) >= top && feet <= (uint8_t)(top + 4)) return top;
|
|
530
|
+
}
|
|
531
|
+
r = ground_row[c];
|
|
532
|
+
if (r != NO_GROUND) {
|
|
533
|
+
top = (uint8_t)(r << 3);
|
|
534
|
+
if ((uint8_t)(feet + 1) >= top && feet <= (uint8_t)(top + 4)) return top;
|
|
535
|
+
}
|
|
536
|
+
return 0;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/* ── GAME LOGIC (clay — reshape freely) — AABB ── */
|
|
540
|
+
static uint8_t aabb(uint8_t ax, uint8_t ay, uint8_t bx, uint8_t by) {
|
|
541
|
+
return ax < (uint8_t)(bx + 8) && (uint8_t)(ax + 8) > bx
|
|
542
|
+
&& ay < (uint8_t)(by + 8) && (uint8_t)(ay + 8) > by;
|
|
117
543
|
}
|
|
118
544
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
545
|
+
/* Push every object to its sprite plane. A dead object parks at PARK_Y
|
|
546
|
+
* (offscreen), NEVER 0xD0 — see the sprite idiom block above. */
|
|
547
|
+
static void push_sprites(uint8_t player_y) {
|
|
548
|
+
uint8_t i;
|
|
549
|
+
msx_set_sprite(0, px, player_y, on_ground ? PAT_IDLE : PAT_JUMP,
|
|
550
|
+
cur_player ? COL_PLAYER2 : COL_PLAYER1);
|
|
551
|
+
msx_set_sprite(1, coin_x, coin_live ? coin_y : PARK_Y, PAT_COIN, COL_COIN);
|
|
552
|
+
for (i = 0; i < NUM_SPIKES; i++)
|
|
553
|
+
msx_set_sprite((uint8_t)(2 + i), spike_x[i],
|
|
554
|
+
(uint8_t)(GROUND_TOP - 8), PAT_SPIKE, COL_SPIKE);
|
|
122
555
|
}
|
|
123
556
|
|
|
124
557
|
void main(void) {
|
|
125
|
-
|
|
126
|
-
const int16_t MOVE = 24;
|
|
127
|
-
const int16_t JUMP = -200;
|
|
128
|
-
const int16_t MAXFALL = 280;
|
|
129
|
-
uint8_t dir, trig, prev_trig, grounded, blip;
|
|
558
|
+
uint8_t dir, jump, y8, feet, c0, c1, top, i;
|
|
130
559
|
|
|
560
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
561
|
+
* Init order: set the video mode FIRST (INIGRP also clears VRAM — any
|
|
562
|
+
* upload done before it is wiped), then tiles, then sprites. The crt0's
|
|
563
|
+
* INIT contract means main() must NEVER return — the BIOS has nothing
|
|
564
|
+
* sane to fall back to — hence the for(;;) below. */
|
|
131
565
|
msx_set_screen2();
|
|
132
566
|
msx_clear_sprites();
|
|
133
567
|
load_tiles();
|
|
134
|
-
|
|
135
|
-
msx_vram_write((uint16_t)(VRAM_SPRPAT +
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
568
|
+
msx_vram_write((uint16_t)(VRAM_SPRPAT + PAT_IDLE * 8), spr_player_idle, 8);
|
|
569
|
+
msx_vram_write((uint16_t)(VRAM_SPRPAT + PAT_JUMP * 8), spr_player_jump, 8);
|
|
570
|
+
msx_vram_write((uint16_t)(VRAM_SPRPAT + PAT_COIN * 8), spr_coin, 8);
|
|
571
|
+
msx_vram_write((uint16_t)(VRAM_SPRPAT + PAT_SPIKE * 8), spr_spike, 8);
|
|
572
|
+
|
|
573
|
+
msx_music(0); /* the lib's demo loop also owns channel C —
|
|
574
|
+
* hand the channel to OUR tune table instead */
|
|
575
|
+
hiscore = 0; /* session hi-score (no SAVE_RAM on this core) */
|
|
576
|
+
rng = 0xACE1;
|
|
577
|
+
(void)next_rand; /* PRNG kept for forks that add random spawns */
|
|
578
|
+
music_step = music_timer = 0;
|
|
579
|
+
sfx_a_t = sfx_b_t = 0;
|
|
580
|
+
prev_t1 = prev_t2 = 1; /* swallow a held trigger across state changes */
|
|
581
|
+
state = ST_TITLE;
|
|
582
|
+
paint_title();
|
|
144
583
|
|
|
145
584
|
for (;;) {
|
|
146
|
-
int16_t ipx, ipy, npy, sx, camCol;
|
|
147
|
-
int32_t np;
|
|
148
|
-
uint8_t i, landed;
|
|
149
|
-
const Rect *p;
|
|
150
|
-
|
|
151
585
|
vsync();
|
|
586
|
+
music_tick();
|
|
587
|
+
sfx_tick();
|
|
588
|
+
|
|
589
|
+
if (state == ST_TITLE) {
|
|
590
|
+
/* ── GAME LOGIC (clay) — title: trig A = 1P; trig B (port-1
|
|
591
|
+
* button 2, gttrig 3) = 2P alternating turns. */
|
|
592
|
+
uint8_t t1 = (uint8_t)(gttrig(1) || gttrig(0));
|
|
593
|
+
uint8_t t2 = gttrig(3);
|
|
594
|
+
if (t2 && !prev_t2) start_game(1);
|
|
595
|
+
else if (t1 && !prev_t1) start_game(0);
|
|
596
|
+
prev_t1 = t1; prev_t2 = t2;
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
152
599
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (camX < 0) camX = 0;
|
|
161
|
-
if (camX > WORLD_W - SCREEN_W) camX = (int16_t)(WORLD_W - SCREEN_W);
|
|
162
|
-
camX = (int16_t)(camX & ~7);
|
|
163
|
-
|
|
164
|
-
camCol = (int16_t)(camX >> 3);
|
|
165
|
-
while (camCol > lastCamCol) { lastCamCol++; paint_column((int16_t)(lastCamCol + 31)); }
|
|
166
|
-
while (camCol < lastCamCol) { lastCamCol--; paint_column(lastCamCol); }
|
|
167
|
-
|
|
168
|
-
sx = (int16_t)(ipx - camX);
|
|
169
|
-
if (sx < 0) sx = 0;
|
|
170
|
-
if (sx > 248) sx = 248;
|
|
171
|
-
msx_set_sprite(0, (uint8_t)sx, (uint8_t)ipy, 0, COL_PLAYER);
|
|
172
|
-
|
|
173
|
-
dir = msx_read_joystick(1);
|
|
174
|
-
if (dir == STICK_CENTER) dir = msx_read_joystick(0);
|
|
175
|
-
trig = (uint8_t)(gttrig(1) || gttrig(2));
|
|
176
|
-
|
|
177
|
-
vx = 0;
|
|
178
|
-
if (dir == STICK_LEFT || dir == STICK_UL || dir == STICK_DL) vx = (int16_t)(-MOVE);
|
|
179
|
-
if (dir == STICK_RIGHT || dir == STICK_UR || dir == STICK_DR) vx = MOVE;
|
|
180
|
-
|
|
181
|
-
grounded = on_platform(ipx, ipy);
|
|
182
|
-
if (trig && !prev_trig && grounded) { vy = JUMP; msx_psg_tone(0, 0x180, 12); blip = 6; }
|
|
183
|
-
prev_trig = trig;
|
|
184
|
-
|
|
185
|
-
vy = (int16_t)(vy + GRAVITY);
|
|
186
|
-
if (vy > MAXFALL) vy = MAXFALL;
|
|
187
|
-
if (grounded && vy > 0) vy = 0;
|
|
188
|
-
|
|
189
|
-
px = (int16_t)(px + vx);
|
|
190
|
-
if (px < 0) px = 0;
|
|
191
|
-
if (px > (int16_t)((WORLD_W - 8) << 4)) px = (int16_t)((WORLD_W - 8) << 4);
|
|
192
|
-
|
|
193
|
-
np = (int32_t)py + (int32_t)vy;
|
|
194
|
-
npy = (int16_t)(np >> 4);
|
|
195
|
-
if (vy > 0) {
|
|
196
|
-
landed = 0;
|
|
197
|
-
for (i = 0; i < N_PLATFORMS; i++) {
|
|
198
|
-
p = &platforms[i];
|
|
199
|
-
if (ipy + 8 <= p->y && npy + 8 >= p->y
|
|
200
|
-
&& ipx + 8 > p->x && ipx < p->x + p->w) {
|
|
201
|
-
py = (int16_t)((p->y - 8) << 4);
|
|
202
|
-
vy = 0;
|
|
203
|
-
landed = 1;
|
|
204
|
-
break;
|
|
205
|
-
}
|
|
600
|
+
if (state == ST_OVER) {
|
|
601
|
+
/* Freeze the final frame; any fire button returns to the title. */
|
|
602
|
+
uint8_t t1 = (uint8_t)(gttrig(1) || gttrig(0) || gttrig(2));
|
|
603
|
+
if (t1 && !prev_t1) {
|
|
604
|
+
state = ST_TITLE;
|
|
605
|
+
msx_clear_sprites();
|
|
606
|
+
paint_title();
|
|
206
607
|
}
|
|
207
|
-
|
|
608
|
+
prev_t1 = t1; prev_t2 = t1;
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/* ── ST_PLAY — GAME LOGIC (clay) from here down ─────────────────── */
|
|
613
|
+
y8 = (uint8_t)(py_q44 >> 4);
|
|
614
|
+
|
|
615
|
+
if (turn_pause) { /* "P# ready" breather: blink + freeze */
|
|
616
|
+
push_sprites((turn_pause & 4) ? y8 : PARK_Y);
|
|
617
|
+
turn_pause--;
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/* Input — the CURRENT player's joystick (P1 port 1 + keyboard;
|
|
622
|
+
* P2 port 2). GTSTCK returns 0=center then 1-8 clockwise from up. */
|
|
623
|
+
if (cur_player == 0) {
|
|
624
|
+
dir = msx_read_joystick(1);
|
|
625
|
+
if (dir == STICK_CENTER) dir = msx_read_joystick(0);
|
|
626
|
+
jump = (uint8_t)(gttrig(1) || gttrig(0));
|
|
208
627
|
} else {
|
|
209
|
-
|
|
628
|
+
dir = msx_read_joystick(2);
|
|
629
|
+
jump = gttrig(2);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if ((dir == STICK_LEFT || dir == STICK_UL || dir == STICK_DL)
|
|
633
|
+
&& px > PLAYER_LEFT) px = (uint8_t)(px - MOVE_SPEED);
|
|
634
|
+
if ((dir == STICK_RIGHT || dir == STICK_UR || dir == STICK_DR)
|
|
635
|
+
&& px < PLAYER_RIGHT) px = (uint8_t)(px + MOVE_SPEED);
|
|
636
|
+
if (jump && !prev_jump && on_ground) {
|
|
637
|
+
vy_q44 = JUMP_VEL_Q44;
|
|
638
|
+
on_ground = 0;
|
|
639
|
+
sfx_jump();
|
|
640
|
+
}
|
|
641
|
+
prev_jump = jump;
|
|
642
|
+
|
|
643
|
+
/* Traversal pays a point every few right-steps (cheap distance). */
|
|
644
|
+
if (dir == STICK_RIGHT || dir == STICK_UR || dir == STICK_DR) {
|
|
645
|
+
if (++dist_sub >= 24) {
|
|
646
|
+
dist_sub = 0;
|
|
647
|
+
if (p_score[cur_player] < 9999) p_score[cur_player]++;
|
|
648
|
+
draw_score();
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/* Physics: gravity + sub-pixel Y. */
|
|
653
|
+
if (vy_q44 < MAX_VY_Q44) vy_q44 = (int8_t)(vy_q44 + GRAVITY_Q44);
|
|
654
|
+
py_q44 = (uint16_t)(py_q44 + (uint16_t)(int16_t)vy_q44);
|
|
655
|
+
y8 = (uint8_t)(py_q44 >> 4);
|
|
656
|
+
|
|
657
|
+
/* Fell into a pit (below the floor line) → lose the turn. */
|
|
658
|
+
if (y8 >= GROUND_TOP + 16) {
|
|
659
|
+
kill_player();
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/* Landing — probe the two columns under the player's feet. */
|
|
664
|
+
if (vy_q44 >= 0) {
|
|
665
|
+
feet = (uint8_t)(y8 + 8);
|
|
666
|
+
c0 = (uint8_t)(px >> 3);
|
|
667
|
+
c1 = (uint8_t)((px + 7) >> 3);
|
|
668
|
+
top = land_top(c0, feet);
|
|
669
|
+
if (top == 0) top = land_top(c1, feet);
|
|
670
|
+
if (top) {
|
|
671
|
+
py_q44 = (uint16_t)((uint16_t)(top - 8) << 4);
|
|
672
|
+
vy_q44 = 0;
|
|
673
|
+
if (!on_ground) sfx_land();
|
|
674
|
+
on_ground = 1;
|
|
675
|
+
} else {
|
|
676
|
+
on_ground = 0; /* walked off an edge */
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/* Coin (collect). */
|
|
681
|
+
if (coin_live && aabb(px, y8, coin_x, coin_y)) {
|
|
682
|
+
coin_live = 0;
|
|
683
|
+
p_score[cur_player] += 10;
|
|
684
|
+
sfx_coin();
|
|
685
|
+
draw_score();
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/* Spikes patrol the ground and kill on touch. */
|
|
689
|
+
for (i = 0; i < NUM_SPIKES; i++) {
|
|
690
|
+
spike_x[i] = (uint8_t)(spike_x[i] + spike_vx[i]);
|
|
691
|
+
if (spike_x[i] <= PLAYER_LEFT) spike_vx[i] = 1;
|
|
692
|
+
if (spike_x[i] >= PLAYER_RIGHT) spike_vx[i] = -1;
|
|
693
|
+
if (aabb(px, y8, spike_x[i], (uint8_t)(GROUND_TOP - 8))) {
|
|
694
|
+
kill_player();
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
210
697
|
}
|
|
211
|
-
if (
|
|
698
|
+
if (state != ST_PLAY) continue; /* kill_player may have ended it */
|
|
212
699
|
|
|
213
|
-
|
|
700
|
+
push_sprites(y8);
|
|
214
701
|
}
|
|
215
702
|
}
|