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,238 +1,656 @@
|
|
|
1
|
-
/* ── platformer.c — NES
|
|
1
|
+
/* ── platformer.c — NES side-scrolling platformer (complete example game) ────
|
|
2
2
|
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
3
|
+
* LEDGE LEAPER — a COMPLETE, working game: title screen, 1P mode and 2P
|
|
4
|
+
* ALTERNATING-TURNS mode (arcade-classic: players swap on death; each player
|
|
5
|
+
* has their own score and own 3 lives; player 2 plays on CONTROLLER 2),
|
|
6
|
+
* coins + distance scoring, persistent hi-score (battery SRAM), music +
|
|
7
|
+
* SFX, and the NES's signature sprite-0-hit split: a fixed HUD strip over
|
|
8
|
+
* a horizontally scrolling level.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
|
|
11
|
+
* very different one. The markers tell you what's what:
|
|
12
|
+
* HARDWARE IDIOM (load-bearing) — dodges a documented NES footgun; reshape
|
|
13
|
+
* your gameplay around it (see TROUBLESHOOTING before changing).
|
|
14
|
+
* GAME LOGIC (clay) — level layout, physics tuning, scoring, art: reshape
|
|
15
|
+
* freely.
|
|
13
16
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
+
* What depends on what:
|
|
18
|
+
* nes_runtime.{h,c} — rendering/input/sound/text/hi-score library.
|
|
19
|
+
* chr-ram-runtime.crt0.s — boot + NMI + iNES header (BATTERY bit feeds
|
|
20
|
+
* hiscore_load/save). Load-bearing; edit with TROUBLESHOOTING open.
|
|
17
21
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
22
|
+
* The level: a 256-px-wide COLUMN MAP (ground height + one-way platforms +
|
|
23
|
+
* pits) painted IDENTICALLY into both nametables, so the 8-bit X scroll
|
|
24
|
+
* wraps seamlessly — an endless looping run of pits, platforms, coins and
|
|
25
|
+
* spikes. Coins/spikes are sprites that drift with the scroll (world-
|
|
26
|
+
* anchored while on screen, respawning at the right edge).
|
|
27
|
+
*
|
|
28
|
+
* Frame budget (NTSC, 60fps): player physics + a two-column tile probe +
|
|
29
|
+
* (3 coins + 2 spikes) of AABB + the sprite-0 spin (a few scanlines) fits
|
|
30
|
+
* comfortably in one frame; a HUD redraw is ≤12 queued VRAM writes (the
|
|
31
|
+
* queue drains 16 per vblank).
|
|
27
32
|
*/
|
|
28
33
|
|
|
29
34
|
#include "nes_runtime.h"
|
|
30
35
|
|
|
31
|
-
/*
|
|
36
|
+
/* The title screen renders this — examples({op:'fork'}) stamps your game's
|
|
37
|
+
* name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
|
|
38
|
+
#define GAME_TITLE "LEDGE LEAPER"
|
|
39
|
+
|
|
40
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
41
|
+
* Tile art. Each 8x8 tile = 16 bytes: 8 plane-0 rows then 8 plane-1 rows
|
|
42
|
+
* (2bpp — plane0-only pixels use colour 1, both planes = colour 3). */
|
|
32
43
|
static const uint8_t tile_blank[16] = { 0 };
|
|
33
44
|
static const uint8_t tile_player_idle[16] = {
|
|
34
|
-
0x3C, 0x7E, 0xFF, 0xFF, 0xFF,
|
|
35
|
-
|
|
45
|
+
0x3C, 0x7E, 0xFF, 0xFF, 0xFF, 0x7E, 0x66, 0x66, /* round body + legs */
|
|
46
|
+
0x00, 0x24, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, /* eyes (colour 3) */
|
|
36
47
|
};
|
|
37
48
|
static const uint8_t tile_player_jump[16] = {
|
|
38
|
-
0x18, 0x7E, 0xFF, 0xFF, 0xE7, 0xC3, 0x81, 0x00, /*
|
|
49
|
+
0x18, 0x7E, 0xFF, 0xFF, 0xE7, 0xC3, 0x81, 0x00, /* arms up */
|
|
50
|
+
0x00, 0x24, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
51
|
+
};
|
|
52
|
+
static const uint8_t tile_coin[16] = {
|
|
53
|
+
0x3C, 0x7E, 0xFF, 0xFF, 0xFF, 0xFF, 0x7E, 0x3C, /* coin disc */
|
|
54
|
+
0x00, 0x3C, 0x66, 0x5A, 0x5A, 0x66, 0x3C, 0x00, /* embossed ring */
|
|
55
|
+
};
|
|
56
|
+
static const uint8_t tile_spike[16] = {
|
|
57
|
+
0x00, 0x18, 0x18, 0x3C, 0x3C, 0x7E, 0x7E, 0xFF, /* ground spike */
|
|
39
58
|
0, 0, 0, 0, 0, 0, 0, 0,
|
|
40
59
|
};
|
|
41
|
-
|
|
42
|
-
|
|
60
|
+
/* Sprite 0's marker block — fully OPAQUE (the sprite-0 hit fires on
|
|
61
|
+
* opaque-sprite-over-opaque-BG, colour is irrelevant). Its palette below
|
|
62
|
+
* makes it the same brown as the HUD bar, so it's invisible in the bar. */
|
|
63
|
+
static const uint8_t tile_mark[16] = {
|
|
64
|
+
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
|
43
65
|
0, 0, 0, 0, 0, 0, 0, 0,
|
|
44
66
|
};
|
|
45
67
|
|
|
46
|
-
/*
|
|
47
|
-
*
|
|
48
|
-
* boot (sky + clouds + dirt) instead of sprites floating on flat black. The
|
|
49
|
-
* BG backdrop colour (palette[0]) is sky blue, so the colours used here are
|
|
50
|
-
* cloud white (idx1), dirt brown (idx2) and grass green (idx3).
|
|
51
|
-
*
|
|
52
|
-
* BG_CLOUD — a puffy cloud (idx1) dotted across the upper sky.
|
|
53
|
-
* BG_DIRT — solid dirt fill (idx2) for the ground band.
|
|
54
|
-
* BG_GRASS — a grass-topped dirt tile (idx3 cap over idx2) for the
|
|
55
|
-
* surface row of the ground. */
|
|
56
|
-
#define BG_CLOUD 1 /* BG tile index 1 → uploaded to $1010 */
|
|
57
|
-
#define BG_DIRT 2 /* BG tile index 2 → uploaded to $1020 */
|
|
58
|
-
#define BG_GRASS 3 /* BG tile index 3 → uploaded to $1030 */
|
|
68
|
+
/* BG tiles (BACKGROUND pattern table $1000 — separate from the sprite
|
|
69
|
+
* table at $0000; the runtime's PPUCTRL setup makes that split). */
|
|
59
70
|
static const uint8_t bg_tile_cloud[16] = {
|
|
60
|
-
0x00, 0x18, 0x3C, 0x7E, 0x7E, 0x00, 0x00, 0x00, /*
|
|
71
|
+
0x00, 0x18, 0x3C, 0x7E, 0x7E, 0x00, 0x00, 0x00, /* puffy cloud (idx1) */
|
|
61
72
|
0, 0, 0, 0, 0, 0, 0, 0,
|
|
62
73
|
};
|
|
63
74
|
static const uint8_t bg_tile_dirt[16] = {
|
|
64
|
-
0, 0, 0, 0, 0, 0, 0, 0,
|
|
65
|
-
0xFF, 0xFF, 0xEF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF,
|
|
75
|
+
0, 0, 0, 0, 0, 0, 0, 0,
|
|
76
|
+
0xFF, 0xFF, 0xEF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, /* dirt fill (idx2) */
|
|
66
77
|
};
|
|
67
78
|
static const uint8_t bg_tile_grass[16] = {
|
|
68
|
-
0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /*
|
|
69
|
-
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, /*
|
|
70
|
-
};
|
|
79
|
+
0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* top 2 rows idx3 */
|
|
80
|
+
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, /* rest idx2 (dirt) */
|
|
81
|
+
};
|
|
82
|
+
/* A solid tile for the HUD bar — sprite 0 must overlap an OPAQUE BG pixel
|
|
83
|
+
* for the sprite-0 hit to fire (see the split idiom below). */
|
|
84
|
+
static const uint8_t bg_tile_hudbar[16] = {
|
|
85
|
+
0, 0, 0, 0, 0, 0, 0, 0,
|
|
86
|
+
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, /* solid idx2 brown */
|
|
87
|
+
};
|
|
88
|
+
#define BG_CLOUD 1
|
|
89
|
+
#define BG_DIRT 2
|
|
90
|
+
#define BG_GRASS 3 /* also used for floating platforms (grass slabs) */
|
|
91
|
+
#define BG_HUDBAR 4
|
|
71
92
|
|
|
72
93
|
static const uint8_t palette[32] = {
|
|
73
|
-
/*
|
|
74
|
-
* grass green
|
|
94
|
+
/* BG: ALL FOUR sub-palettes identical (sky, cloud white, dirt brown,
|
|
95
|
+
* grass green). That makes stale attribute-table bits harmless — power-on
|
|
96
|
+
* CIRAM is garbage, and identical sub-palettes mean any attribute value
|
|
97
|
+
* picks the same colours. We clear the attribute tables anyway (belt and
|
|
98
|
+
* braces, see paint_field). */
|
|
75
99
|
0x21, 0x30, 0x17, 0x2A,
|
|
76
100
|
0x21, 0x30, 0x17, 0x2A,
|
|
77
101
|
0x21, 0x30, 0x17, 0x2A,
|
|
78
102
|
0x21, 0x30, 0x17, 0x2A,
|
|
79
|
-
/* The universal backdrop ($3F00) is MIRRORED at $3F10 —
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
0x0F, 0x18, 0x28, 0x38, /* sp1: platforms — green */
|
|
89
|
-
0x0F, 0x16, 0x06, 0x36,
|
|
90
|
-
0x0F, 0x2A, 0x1A, 0x0A,
|
|
103
|
+
/* The universal backdrop ($3F00) is MIRRORED at $3F10 — sprite palette 0
|
|
104
|
+
* colour 0. palette_load writes all 32 bytes in order, so this byte is
|
|
105
|
+
* the LAST write to the mirror and wins: keep it equal to the BG backdrop
|
|
106
|
+
* (sky blue) or the whole sky changes colour. (Sprite colour 0 is
|
|
107
|
+
* transparent regardless — this never affects how sprites draw.) */
|
|
108
|
+
0x21, 0x16, 0x30, 0x27, /* sp0: player — red body, white/orange trim */
|
|
109
|
+
0x0F, 0x17, 0x17, 0x17, /* sp1: sprite-0 marker — HUD-bar brown camo */
|
|
110
|
+
0x0F, 0x16, 0x06, 0x30, /* sp2: spikes — danger red */
|
|
111
|
+
0x0F, 0x28, 0x27, 0x30, /* sp3: coins — gold */
|
|
91
112
|
};
|
|
92
113
|
|
|
93
|
-
/* ──
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
114
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
115
|
+
* The level — a 32-column map; world x = (screen x + scroll) mod 256.
|
|
116
|
+
* ground_row[c] — nametable row of the ground's grass top, 0xFF = pit.
|
|
117
|
+
* plat_row[c] — row of a one-way floating platform, 0 = none.
|
|
118
|
+
* Rows are nametable rows (y = row*8). Playfield rows are 3..29. */
|
|
119
|
+
#define NO_GROUND 0xFF
|
|
120
|
+
static const uint8_t ground_row[32] = {
|
|
121
|
+
26, 26, 26, 26, 26, 26, 26, 26, /* start runway */
|
|
122
|
+
26, NO_GROUND, NO_GROUND, 26, 26, 26, 26, 26, /* pit 1 (16 px) */
|
|
123
|
+
26, 26, 26, 26, NO_GROUND, NO_GROUND, NO_GROUND, /* pit 2 (24 px) */
|
|
124
|
+
26, 26, 26, 26, 26, 26, 26, 26, 26,
|
|
125
|
+
};
|
|
126
|
+
static const uint8_t plat_row[32] = {
|
|
127
|
+
0, 0, 0, 0, 21, 21, 21, 0, /* slab before pit 1 */
|
|
128
|
+
0, 0, 0, 0, 0, 0, 20, 20, /* slab mid-level */
|
|
129
|
+
20, 0, 0, 0, 0, 0, 0, 0,
|
|
130
|
+
0, 21, 21, 21, 0, 0, 0, 0, /* slab near the loop */
|
|
102
131
|
};
|
|
103
132
|
|
|
104
|
-
#define TILE_PLAYER_IDLE
|
|
105
|
-
#define TILE_PLAYER_JUMP
|
|
106
|
-
#define
|
|
107
|
-
#define
|
|
108
|
-
#define
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
+
#define TILE_PLAYER_IDLE 1
|
|
134
|
+
#define TILE_PLAYER_JUMP 2
|
|
135
|
+
#define TILE_COIN 3
|
|
136
|
+
#define TILE_SPIKE 4
|
|
137
|
+
#define TILE_MARK 5
|
|
138
|
+
#define PLAYER_PAL 0
|
|
139
|
+
#define MARK_PAL 1
|
|
140
|
+
#define SPIKE_PAL 2
|
|
141
|
+
#define COIN_PAL 3
|
|
142
|
+
|
|
143
|
+
/* HUD layout (mind the OVERSCAN: most NTSC displays/cores crop the top 8
|
|
144
|
+
* scanlines, so nametable row 0 is invisible — never put text there):
|
|
145
|
+
* row 0 — blank (cropped by overscan)
|
|
146
|
+
* row 1 — HUD text (P# / lives / SC / HI)
|
|
147
|
+
* row 2 — solid bar: the visual divider AND sprite 0's opaque anchor
|
|
148
|
+
* row 3+ — the scrolling playfield
|
|
149
|
+
* The HUD strip always renders with scroll (0,0) from nametable 0, so HUD
|
|
150
|
+
* text lives ONLY in nametable 0 — it can never scroll into view twice. */
|
|
151
|
+
#define HUD_ROWS 3
|
|
152
|
+
#define START_LIVES 3
|
|
153
|
+
|
|
154
|
+
/* ── GAME LOGIC (clay) — physics + tuning ── */
|
|
155
|
+
#define GRAVITY_Q44 1 /* +1/16 px per frame per frame */
|
|
156
|
+
#define JUMP_VEL_Q44 (-40) /* launch vy (Q4.4) → ~50 px / ~6 tile apex */
|
|
157
|
+
#define MAX_VY_Q44 80 /* terminal velocity, 5 px/frame — MUST stay *
|
|
158
|
+
* under 6: the landing probe's 6-px window *
|
|
159
|
+
* can't catch a faster fall (tunnelling) */
|
|
160
|
+
#define MOVE_SPEED 2 /* px/frame walk + scroll speed */
|
|
161
|
+
#define SCROLL_WALL 112 /* px: past this the world scrolls, not you */
|
|
162
|
+
#define GROUND_TOP 208 /* ground_row 26 * 8 */
|
|
163
|
+
#define SPIKE_Y 200 /* spikes stand on the ground */
|
|
164
|
+
#define NUM_COINS 3
|
|
165
|
+
#define NUM_SPIKES 2
|
|
166
|
+
|
|
167
|
+
static uint8_t px; /* player screen x */
|
|
168
|
+
static uint16_t py_q44; /* player y, Q4.4 fixed point — gravity
|
|
169
|
+
* adds <1 px/frame near the jump apex,
|
|
170
|
+
* so we need sub-pixel precision */
|
|
171
|
+
static int8_t vy_q44;
|
|
172
|
+
static uint8_t on_ground;
|
|
173
|
+
static uint8_t scroll_x; /* level scroll — uint8 wraps at 256 = *
|
|
174
|
+
* exactly one level loop (seamless) */
|
|
175
|
+
static uint8_t dist_sub; /* sub-counter: 64 px scrolled = +1 pt */
|
|
176
|
+
static uint8_t coin_x[NUM_COINS], coin_y[NUM_COINS];
|
|
177
|
+
static uint8_t spike_x[NUM_SPIKES], spike_active[NUM_SPIKES];
|
|
178
|
+
|
|
179
|
+
/* Players: index 0 = P1 (controller 1), 1 = P2 (controller 2 — alternating
|
|
180
|
+
* turns, arcade-classic style). Each has own score + own lives; the HUD shows the
|
|
181
|
+
* CURRENT player's numbers. */
|
|
182
|
+
static uint8_t two_player;
|
|
183
|
+
static uint8_t cur_player;
|
|
184
|
+
static uint8_t p_lives[2];
|
|
185
|
+
static uint16_t p_score[2];
|
|
186
|
+
static uint16_t hiscore;
|
|
187
|
+
static uint8_t turn_pause; /* freeze frames after a turn change */
|
|
188
|
+
static uint16_t rng = 0xC0DE;
|
|
189
|
+
|
|
190
|
+
/* Game states — the shell every example shares: title → play → game over. */
|
|
191
|
+
#define ST_TITLE 0
|
|
192
|
+
#define ST_PLAY 1
|
|
193
|
+
#define ST_OVER 2
|
|
194
|
+
static uint8_t state;
|
|
195
|
+
static uint8_t prev_pad;
|
|
196
|
+
|
|
197
|
+
/* ── GAME LOGIC (clay) — xorshift16 PRNG (~tens of cycles per call) ── */
|
|
198
|
+
static uint8_t random8(void) {
|
|
199
|
+
uint16_t r = rng;
|
|
200
|
+
r ^= r << 7;
|
|
201
|
+
r ^= r >> 9;
|
|
202
|
+
r ^= r << 8;
|
|
203
|
+
rng = r;
|
|
204
|
+
return (uint8_t)r;
|
|
133
205
|
}
|
|
134
206
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
207
|
+
static uint8_t dist8(uint8_t a, uint8_t b) {
|
|
208
|
+
return (a > b) ? (a - b) : (b - a);
|
|
209
|
+
}
|
|
138
210
|
|
|
139
|
-
|
|
211
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
212
|
+
* Sprite-0-hit split scroll — THE classic NES technique (the fixed
|
|
213
|
+
* status bar over a scrolling field in countless NES classics). The PPU has ONE scroll for the whole
|
|
214
|
+
* frame; to keep the HUD fixed while the playfield scrolls, you change the
|
|
215
|
+
* scroll MID-FRAME, and sprite 0 is your timing signal:
|
|
216
|
+
*
|
|
217
|
+
* 1. Sprite 0 (the FIRST sprite staged each frame) sits inside the HUD,
|
|
218
|
+
* overlapping an OPAQUE background pixel (our solid HUD bar tile).
|
|
219
|
+
* 2. The NMI commits scroll (0,0) at vblank — the HUD renders unscrolled.
|
|
220
|
+
* 3. After ppu_wait_nmi(), poll PPUSTATUS bit 6 in TWO phases: first wait
|
|
221
|
+
* for it to CLEAR (the stale flag from the previous frame survives all
|
|
222
|
+
* of vblank and only clears at the pre-render line), then wait for it
|
|
223
|
+
* to SET — the exact pixel where sprite 0's opaque pixel overlaps
|
|
224
|
+
* opaque background.
|
|
225
|
+
* 4. THEN write the playfield scroll to PPUSCROLL — everything below the
|
|
226
|
+
* HUD renders with the new scroll.
|
|
227
|
+
*
|
|
228
|
+
* Requires: sprite 0 staged FIRST (oam_spr call order = OAM order), an
|
|
229
|
+
* opaque BG pixel under it, ppu_scroll(0,0) left as the frame scroll, and
|
|
230
|
+
* this poll running EVERY frame (miss a frame and the field jumps).
|
|
231
|
+
* Mid-frame X-scroll needs only the two PPUSCROLL writes below. (Mid-frame
|
|
232
|
+
* Y needs the 4-write $2006/$2005 dance — see TROUBLESHOOTING before
|
|
233
|
+
* attempting; X covers the HUD-over-scrolling-field pattern.)
|
|
234
|
+
* The two-phase spin burns from vblank start to the hit scanline — about
|
|
235
|
+
* 35 scanlines of CPU every frame. Budget for it: your game logic gets the
|
|
236
|
+
* rest of the visible frame, which is plenty for a game this size. */
|
|
237
|
+
#define PPUSTATUS_REG (*(volatile uint8_t *)0x2002)
|
|
238
|
+
#define PPUSCROLL_REG (*(volatile uint8_t *)0x2005)
|
|
239
|
+
static void split_after_hud(void) {
|
|
240
|
+
uint8_t timeout = 240;
|
|
241
|
+
/* FOOTGUN: the hit flag from the frame JUST RENDERED stays set all the
|
|
242
|
+
* way through vblank — it only clears at the next pre-render line. We're
|
|
243
|
+
* called right after ppu_wait_nmi() (i.e. inside vblank), so polling for
|
|
244
|
+
* "set" alone exits INSTANTLY on the stale flag and the PPUSCROLL write
|
|
245
|
+
* lands during vblank — scrolling the WHOLE next frame, HUD included
|
|
246
|
+
* (the shear is subtle: it looks like the HUD "drifting"). The classic
|
|
247
|
+
* fix is the two-phase poll: wait for the stale flag to CLEAR (the
|
|
248
|
+
* pre-render line), then wait for THIS frame's hit to SET. */
|
|
249
|
+
while (PPUSTATUS_REG & 0x40) {
|
|
250
|
+
if (--timeout == 0) return; /* flag stuck: bail, keep scroll (0,0) */
|
|
251
|
+
}
|
|
252
|
+
timeout = 240;
|
|
253
|
+
while (!(PPUSTATUS_REG & 0x40)) {
|
|
254
|
+
if (--timeout == 0) return; /* rendering off / sprite-0 missing: bail */
|
|
255
|
+
}
|
|
256
|
+
PPUSCROLL_REG = scroll_x; /* playfield X scroll (below the HUD) */
|
|
257
|
+
PPUSCROLL_REG = 0;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/* Stage sprite 0 = an 8x8 opaque block over the HUD BAR row (OAM y is
|
|
261
|
+
* scanline-1, so y=16 renders scanlines 17-24 = nametable row 2 = the bar —
|
|
262
|
+
* opaque-on-opaque, so the hit fires INSIDE the bar and the scroll change
|
|
263
|
+
* lands below it, never shearing the text row). Must be the FIRST oam_spr
|
|
264
|
+
* call of the frame (OAM order = call order; the split needs index 0). */
|
|
265
|
+
static void stage_sprite0(void) {
|
|
266
|
+
oam_spr(4, (HUD_ROWS - 1) * 8, TILE_MARK, MARK_PAL);
|
|
267
|
+
}
|
|
140
268
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
269
|
+
/* ── GAME LOGIC (clay) — HUD text (queued writes; NMI commits next vblank) ── */
|
|
270
|
+
static void draw_hud(void) {
|
|
271
|
+
tile_set(0, 1, 1, (uint8_t)(0x41 + cur_player)); /* '1' or '2' */
|
|
272
|
+
tile_set(0, 3, 1, 0x40 + p_lives[cur_player]); /* lives as a digit */
|
|
273
|
+
text_draw_u16(0, 9, 1, p_score[cur_player]);
|
|
274
|
+
text_draw_u16(0, 19, 1, hiscore);
|
|
275
|
+
}
|
|
145
276
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
277
|
+
static void draw_hud_labels(void) {
|
|
278
|
+
text_draw(0, 0, 1, "P");
|
|
279
|
+
text_draw(0, 6, 1, "SC");
|
|
280
|
+
text_draw(0, 16, 1, "HI");
|
|
281
|
+
}
|
|
151
282
|
|
|
152
|
-
|
|
283
|
+
/* PPU-off digit painter (the queued text_draw_u16 needs rendering ON). */
|
|
284
|
+
static void digits_unsafe(uint16_t ppu_addr, uint16_t v) {
|
|
285
|
+
uint8_t d[5], i;
|
|
286
|
+
for (i = 0; i < 5; i++) { d[i] = v % 10; v /= 10; }
|
|
287
|
+
for (i = 0; i < 5; i++) vram_unsafe_set(ppu_addr + i, (uint8_t)(0x40 + d[4 - i]));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/* ── GAME LOGIC (clay) — the title screen ──────────────────────────────────
|
|
291
|
+
* Painted with the PPU OFF (text_draw_unsafe = raw VRAM writes; the queued
|
|
292
|
+
* variant would deadlock with rendering disabled — see TROUBLESHOOTING). */
|
|
293
|
+
static void paint_title(void) {
|
|
294
|
+
uint16_t a = 0x2000;
|
|
295
|
+
uint8_t r, c, t;
|
|
296
|
+
ppu_off();
|
|
297
|
+
for (r = 0; r < 30; r++) {
|
|
298
|
+
for (c = 0; c < 32; c++) {
|
|
299
|
+
t = 0; /* sky backdrop */
|
|
300
|
+
if (r == 26) t = BG_GRASS;
|
|
301
|
+
else if (r > 26) t = BG_DIRT;
|
|
302
|
+
vram_unsafe_set(a, t);
|
|
303
|
+
++a;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
text_draw_unsafe(0x2000 + 8 * 32 + ((32 - sizeof(GAME_TITLE) + 1) / 2), GAME_TITLE);
|
|
307
|
+
text_draw_unsafe(0x2000 + 13 * 32 + 10, "1P START - A");
|
|
308
|
+
text_draw_unsafe(0x2000 + 15 * 32 + 10, "2P TURNS - B");
|
|
309
|
+
text_draw_unsafe(0x2000 + 20 * 32 + 10, "HI");
|
|
310
|
+
digits_unsafe(0x2000 + 20 * 32 + 13, hiscore);
|
|
311
|
+
ppu_scroll(0, 0);
|
|
312
|
+
oam_clear();
|
|
313
|
+
ppu_on_all();
|
|
314
|
+
}
|
|
153
315
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
for (c = 0; c < 32; c++)
|
|
167
|
-
|
|
168
|
-
for (
|
|
169
|
-
|
|
170
|
-
|
|
316
|
+
/* ── GAME LOGIC (clay) — paint the level from the column map ───────────────
|
|
317
|
+
* Painted into BOTH nametables (vertical mirroring puts NT0 + NT1 side by
|
|
318
|
+
* side = a 512-px canvas). Identical copies + a 256-px-periodic level make
|
|
319
|
+
* the uint8 scroll wrap PERFECTLY seamless: the visible window always shows
|
|
320
|
+
* the same content at world x mod 256, so the run loops forever. */
|
|
321
|
+
static void paint_field(void) {
|
|
322
|
+
uint16_t base, a;
|
|
323
|
+
uint8_t nt, r, c, t, g;
|
|
324
|
+
ppu_off();
|
|
325
|
+
for (nt = 0; nt < 2; nt++) {
|
|
326
|
+
base = nt ? 0x2400 : 0x2000;
|
|
327
|
+
a = base;
|
|
328
|
+
for (c = 0; c < 32; c++) { vram_unsafe_set(a, 0); ++a; } /* row 0: overscan */
|
|
329
|
+
for (c = 0; c < 32; c++) { vram_unsafe_set(a, 0); ++a; } /* row 1: HUD text */
|
|
330
|
+
for (c = 0; c < 32; c++) { vram_unsafe_set(a, BG_HUDBAR); ++a; } /* row 2: bar */
|
|
331
|
+
for (r = HUD_ROWS; r < 30; r++) {
|
|
332
|
+
for (c = 0; c < 32; c++) {
|
|
333
|
+
g = ground_row[c];
|
|
334
|
+
t = 0;
|
|
335
|
+
if (r == plat_row[c]) t = BG_GRASS; /* floating slab */
|
|
336
|
+
else if (g != NO_GROUND) {
|
|
337
|
+
if (r == g) t = BG_GRASS; /* ground surface */
|
|
338
|
+
else if (r > g) t = BG_DIRT; /* ground body */
|
|
339
|
+
}
|
|
340
|
+
if (t == 0 && r >= 4 && r <= 9) {
|
|
341
|
+
if (((r * 7 + c * 5) & 15) == 0) t = BG_CLOUD;
|
|
342
|
+
}
|
|
343
|
+
vram_unsafe_set(a, t);
|
|
344
|
+
++a;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
/* Attribute table → palette 0 everywhere. CIRAM powers on as garbage;
|
|
348
|
+
* with our identical BG sub-palettes it wouldn't show, but clear it so
|
|
349
|
+
* forks that diverge the palettes don't inherit a latent bug. */
|
|
350
|
+
a = base + 0x3C0;
|
|
351
|
+
for (c = 0; c < 64; c++) { vram_unsafe_set(a, 0); ++a; }
|
|
171
352
|
}
|
|
353
|
+
ppu_scroll(0, 0);
|
|
354
|
+
oam_clear();
|
|
355
|
+
ppu_on_all();
|
|
356
|
+
/* Labels go through the queued path once rendering is on. */
|
|
357
|
+
draw_hud_labels();
|
|
358
|
+
}
|
|
172
359
|
|
|
360
|
+
/* ── GAME LOGIC (clay) — the game-over results screen ── */
|
|
361
|
+
static void paint_over(void) {
|
|
362
|
+
uint16_t a = 0x2000;
|
|
363
|
+
uint16_t i;
|
|
364
|
+
ppu_off();
|
|
365
|
+
for (i = 0; i < 960; i++) { vram_unsafe_set(a, 0); ++a; }
|
|
366
|
+
text_draw_unsafe(0x2000 + 8 * 32 + 11, "GAME OVER");
|
|
367
|
+
text_draw_unsafe(0x2000 + 12 * 32 + 9, "P1");
|
|
368
|
+
digits_unsafe(0x2000 + 12 * 32 + 13, p_score[0]);
|
|
369
|
+
if (two_player) {
|
|
370
|
+
text_draw_unsafe(0x2000 + 14 * 32 + 9, "P2");
|
|
371
|
+
digits_unsafe(0x2000 + 14 * 32 + 13, p_score[1]);
|
|
372
|
+
}
|
|
373
|
+
text_draw_unsafe(0x2000 + 17 * 32 + 9, "HI");
|
|
374
|
+
digits_unsafe(0x2000 + 17 * 32 + 13, hiscore);
|
|
375
|
+
text_draw_unsafe(0x2000 + 21 * 32 + 9, "START - TITLE");
|
|
376
|
+
ppu_scroll(0, 0);
|
|
173
377
|
oam_clear();
|
|
174
378
|
ppu_on_all();
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/* ── GAME LOGIC (clay) — coins + spikes (sprite objects in the world) ── */
|
|
382
|
+
static const uint8_t coin_heights[4] = { 184, 160, 128, 152 };
|
|
383
|
+
static void respawn_coin(uint8_t i) {
|
|
384
|
+
coin_x[i] = (uint8_t)(232 + (random8() & 15)); /* enter at the right */
|
|
385
|
+
coin_y[i] = coin_heights[random8() & 3];
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
static void try_spawn_spike(uint8_t i) {
|
|
389
|
+
/* Anchor only over ground: an inactive spike rolls a low per-frame
|
|
390
|
+
* chance, and only spawns if the level column entering at the right
|
|
391
|
+
* edge has ground under it (never floats over a pit). */
|
|
392
|
+
uint8_t c = (uint8_t)(248 + scroll_x) >> 3;
|
|
393
|
+
if (ground_row[c] == NO_GROUND) return;
|
|
394
|
+
if (random8() > 4) return;
|
|
395
|
+
spike_x[i] = 248;
|
|
396
|
+
spike_active[i] = 1;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/* ── GAME LOGIC (clay) — start a turn / a run ── */
|
|
400
|
+
static void begin_turn(void) {
|
|
401
|
+
px = 24;
|
|
402
|
+
py_q44 = (uint16_t)(GROUND_TOP - 8) << 4;
|
|
403
|
+
vy_q44 = 0;
|
|
404
|
+
on_ground = 1;
|
|
405
|
+
scroll_x = 0;
|
|
406
|
+
dist_sub = 0;
|
|
407
|
+
coin_x[0] = 88; coin_y[0] = 184;
|
|
408
|
+
coin_x[1] = 152; coin_y[1] = 160;
|
|
409
|
+
coin_x[2] = 216; coin_y[2] = 128;
|
|
410
|
+
spike_x[0] = 136; spike_active[0] = 1; /* both anchored on ground at */
|
|
411
|
+
spike_x[1] = 224; spike_active[1] = 1; /* scroll 0 — see ground_row */
|
|
412
|
+
turn_pause = 48; /* "P1/P2 ready" breather */
|
|
413
|
+
prev_pad = 0xFF; /* swallow held buttons across *
|
|
414
|
+
* the turn change */
|
|
415
|
+
ppu_scroll(0, 0);
|
|
416
|
+
draw_hud();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
static void start_game(uint8_t players) {
|
|
420
|
+
two_player = players;
|
|
421
|
+
cur_player = 0;
|
|
422
|
+
p_score[0] = p_score[1] = 0;
|
|
423
|
+
p_lives[0] = START_LIVES;
|
|
424
|
+
p_lives[1] = players ? START_LIVES : 0;
|
|
425
|
+
paint_field();
|
|
426
|
+
begin_turn();
|
|
427
|
+
sound_play_tone(0, 0x0FD, 8, 8); /* start jingle (A4) */
|
|
428
|
+
state = ST_PLAY;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
static void game_over(void) {
|
|
432
|
+
uint16_t best = p_score[0];
|
|
433
|
+
if (two_player && p_score[1] > best) best = p_score[1];
|
|
434
|
+
if (best > hiscore) {
|
|
435
|
+
hiscore = best;
|
|
436
|
+
/* ── HARDWARE IDIOM (load-bearing) — persists via battery PRG-RAM at
|
|
437
|
+
* $6000; works because the crt0's iNES header sets the BATTERY bit.
|
|
438
|
+
* See nes_runtime.c for the magic+checksum layout. ── */
|
|
439
|
+
hiscore_save(hiscore);
|
|
440
|
+
}
|
|
441
|
+
state = ST_OVER;
|
|
442
|
+
paint_over();
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/* ── GAME LOGIC (clay) — death + alternating-turn handoff ── */
|
|
446
|
+
static void kill_player(void) {
|
|
447
|
+
uint8_t other;
|
|
448
|
+
sound_play_noise(12, 12, 14);
|
|
449
|
+
if (p_lives[cur_player] > 0) --p_lives[cur_player];
|
|
450
|
+
if (two_player) {
|
|
451
|
+
other = cur_player ^ 1;
|
|
452
|
+
if (p_lives[other] > 0) cur_player = other; /* swap turns */
|
|
453
|
+
else if (p_lives[cur_player] == 0) { game_over(); return; }
|
|
454
|
+
} else if (p_lives[0] == 0) {
|
|
455
|
+
game_over();
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
begin_turn();
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/* ── GAME LOGIC (clay) — landing probe against the column map ──────────────
|
|
462
|
+
* One-way platforms, classic NES style: only catch the player while FALLING
|
|
463
|
+
* through a narrow window at the surface. The window is 6 px tall —
|
|
464
|
+
* top-1 (the standing snap parks feet at top, and gravity's sub-pixel
|
|
465
|
+
* trickle doesn't move the integer Y every frame; without the -1 slack the
|
|
466
|
+
* player "stands" with on_ground=0 most frames, so jumps only register on
|
|
467
|
+
* lucky frames and the idle/jump sprite flickers) through top+4 (so a
|
|
468
|
+
* 5 px/frame terminal-velocity fall can't step over it). */
|
|
469
|
+
static uint8_t land_top(uint8_t c, uint8_t feet) {
|
|
470
|
+
uint8_t r, top;
|
|
471
|
+
r = plat_row[c];
|
|
472
|
+
if (r) {
|
|
473
|
+
top = r << 3;
|
|
474
|
+
if ((uint8_t)(feet + 1) >= top && feet <= (uint8_t)(top + 4)) return top;
|
|
475
|
+
}
|
|
476
|
+
r = ground_row[c];
|
|
477
|
+
if (r != NO_GROUND) {
|
|
478
|
+
top = r << 3;
|
|
479
|
+
if ((uint8_t)(feet + 1) >= top && feet <= (uint8_t)(top + 4)) return top;
|
|
480
|
+
}
|
|
481
|
+
return 0;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
void main(void) {
|
|
485
|
+
uint8_t i, pad, delta, y8, feet, c0, c1, top, killed;
|
|
486
|
+
uint8_t player_y;
|
|
487
|
+
|
|
488
|
+
/* ── HARDWARE IDIOM (load-bearing — see TROUBLESHOOTING) ──
|
|
489
|
+
* Init order: PPU off → CHR upload → palette → nametable (raw writes) →
|
|
490
|
+
* OAM clear → rendering on. CHR/palette/nametable writes REQUIRE the PPU
|
|
491
|
+
* off (raw $2007 traffic during rendering corrupts the address latch
|
|
492
|
+
* mid-frame). The runtime's ppu_off/ppu_on_all pair owns the PPUCTRL/
|
|
493
|
+
* PPUMASK bits — don't poke those registers directly alongside it. */
|
|
494
|
+
ppu_off();
|
|
495
|
+
chr_ram_upload(0x0000, tile_blank, 16);
|
|
496
|
+
chr_ram_upload(0x0010, tile_player_idle, 16);
|
|
497
|
+
chr_ram_upload(0x0020, tile_player_jump, 16);
|
|
498
|
+
chr_ram_upload(0x0030, tile_coin, 16);
|
|
499
|
+
chr_ram_upload(0x0040, tile_spike, 16);
|
|
500
|
+
chr_ram_upload(0x0050, tile_mark, 16);
|
|
501
|
+
chr_ram_upload(0x1010, bg_tile_cloud, 16);
|
|
502
|
+
chr_ram_upload(0x1020, bg_tile_dirt, 16);
|
|
503
|
+
chr_ram_upload(0x1030, bg_tile_grass, 16);
|
|
504
|
+
chr_ram_upload(0x1040, bg_tile_hudbar, 16);
|
|
505
|
+
font_upload();
|
|
506
|
+
palette_load(palette);
|
|
175
507
|
sound_init();
|
|
176
508
|
|
|
509
|
+
hiscore = hiscore_load(); /* battery SRAM — 0 on first boot */
|
|
510
|
+
state = ST_TITLE;
|
|
511
|
+
paint_title();
|
|
512
|
+
|
|
177
513
|
for (;;) {
|
|
178
|
-
|
|
514
|
+
if (state == ST_TITLE) {
|
|
515
|
+
/* ── GAME LOGIC (clay) — title: A = 1P, B = 2P alternating turns ── */
|
|
516
|
+
oam_clear();
|
|
517
|
+
ppu_wait_nmi();
|
|
518
|
+
sound_music_tick();
|
|
519
|
+
pad = pad_poll(0);
|
|
520
|
+
if ((pad & PAD_A) && !(prev_pad & PAD_A)) start_game(0);
|
|
521
|
+
else if ((pad & PAD_B) && !(prev_pad & PAD_B)) start_game(1);
|
|
522
|
+
else if ((pad & PAD_START) && !(prev_pad & PAD_START)) start_game(0);
|
|
523
|
+
prev_pad = pad;
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
179
526
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
/* draw each platform tile (one OAM entry per 8 px column) */
|
|
190
|
-
for (i = 0; i < NUM_PLATFORMS; i++) {
|
|
191
|
-
uint8_t k;
|
|
192
|
-
for (k = 0; k < platforms[i].w; k++) {
|
|
193
|
-
oam_spr(platforms[i].x + (k << 3), platforms[i].y, TILE_PLATFORM, PLATFORM_PAL);
|
|
527
|
+
if (state == ST_OVER) {
|
|
528
|
+
/* Results screen (scroll 0, no split needed); START or A → title. */
|
|
529
|
+
oam_clear();
|
|
530
|
+
ppu_wait_nmi();
|
|
531
|
+
sound_music_tick();
|
|
532
|
+
pad = pad_poll(0);
|
|
533
|
+
if ((pad & (PAD_START | PAD_A)) && !(prev_pad & (PAD_START | PAD_A))) {
|
|
534
|
+
state = ST_TITLE;
|
|
535
|
+
paint_title();
|
|
194
536
|
}
|
|
537
|
+
prev_pad = pad;
|
|
538
|
+
continue;
|
|
195
539
|
}
|
|
196
540
|
|
|
197
|
-
|
|
541
|
+
/* ── ST_PLAY ─────────────────────────────────────────────────────── */
|
|
542
|
+
|
|
543
|
+
/* ── HARDWARE IDIOM (load-bearing — see TROUBLESHOOTING) ──
|
|
544
|
+
* Stage ALL sprites BEFORE ppu_wait_nmi(). The NMI DMAs shadow OAM →
|
|
545
|
+
* real OAM at the START of vblank, copying whatever shadow OAM holds AT
|
|
546
|
+
* THAT MOMENT. Stage-then-wait; flipping it shows stale/empty sprites.
|
|
547
|
+
* Sprite 0 (the split marker) must be staged FIRST — OAM order is
|
|
548
|
+
* oam_spr call order, and the split idiom needs it at index 0. */
|
|
549
|
+
player_y = (uint8_t)(py_q44 >> 4);
|
|
550
|
+
oam_clear();
|
|
551
|
+
stage_sprite0();
|
|
552
|
+
/* Blink the player during the turn-change breather. */
|
|
553
|
+
if (turn_pause == 0 || (turn_pause & 4))
|
|
554
|
+
oam_spr(px, player_y,
|
|
555
|
+
on_ground ? TILE_PLAYER_IDLE : TILE_PLAYER_JUMP, PLAYER_PAL);
|
|
556
|
+
for (i = 0; i < NUM_COINS; i++)
|
|
557
|
+
oam_spr(coin_x[i], coin_y[i], TILE_COIN, COIN_PAL);
|
|
558
|
+
for (i = 0; i < NUM_SPIKES; i++)
|
|
559
|
+
if (spike_active[i]) oam_spr(spike_x[i], SPIKE_Y, TILE_SPIKE, SPIKE_PAL);
|
|
198
560
|
|
|
199
|
-
|
|
561
|
+
ppu_wait_nmi();
|
|
562
|
+
split_after_hud(); /* the sprite-0 split — every frame */
|
|
200
563
|
sound_music_tick();
|
|
201
|
-
|
|
202
|
-
if (
|
|
203
|
-
|
|
204
|
-
|
|
564
|
+
|
|
565
|
+
if (turn_pause) { /* freeze gameplay, keep the frame honest */
|
|
566
|
+
--turn_pause;
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/* ── GAME LOGIC (clay) from here down ──────────────────────────────
|
|
571
|
+
* Input — the CURRENT player's controller (alternating turns: P2 is
|
|
572
|
+
* on controller 2). Past SCROLL_WALL the world scrolls instead of the
|
|
573
|
+
* player (the camera never scrolls back — the classic one-way camera). */
|
|
574
|
+
pad = pad_poll(cur_player);
|
|
575
|
+
delta = 0;
|
|
576
|
+
if (pad & PAD_RIGHT) {
|
|
577
|
+
if (px < SCROLL_WALL) px += MOVE_SPEED;
|
|
578
|
+
else { scroll_x += MOVE_SPEED; delta = MOVE_SPEED; }
|
|
579
|
+
}
|
|
580
|
+
if ((pad & PAD_LEFT) && px > 8) px -= MOVE_SPEED;
|
|
205
581
|
if ((pad & PAD_A) && !(prev_pad & PAD_A) && on_ground) {
|
|
206
582
|
vy_q44 = JUMP_VEL_Q44;
|
|
207
583
|
on_ground = 0;
|
|
208
|
-
sound_play_tone(0, 0x150, 6, 6);
|
|
584
|
+
sound_play_tone(0, 0x150, 6, 6); /* jump whoop */
|
|
209
585
|
}
|
|
210
586
|
prev_pad = pad;
|
|
211
587
|
|
|
212
|
-
/*
|
|
213
|
-
|
|
214
|
-
|
|
588
|
+
/* World objects drift left as the level scrolls (world-anchored). */
|
|
589
|
+
if (delta) {
|
|
590
|
+
dist_sub += delta;
|
|
591
|
+
if (dist_sub >= 64) { /* distance pay */
|
|
592
|
+
dist_sub -= 64;
|
|
593
|
+
++p_score[cur_player];
|
|
594
|
+
draw_hud();
|
|
595
|
+
}
|
|
596
|
+
for (i = 0; i < NUM_COINS; i++) {
|
|
597
|
+
if (coin_x[i] < 16 + delta) respawn_coin(i);
|
|
598
|
+
else coin_x[i] -= delta;
|
|
599
|
+
}
|
|
600
|
+
for (i = 0; i < NUM_SPIKES; i++) {
|
|
601
|
+
if (!spike_active[i]) continue;
|
|
602
|
+
if (spike_x[i] < 16 + delta) spike_active[i] = 0;
|
|
603
|
+
else spike_x[i] -= delta;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
for (i = 0; i < NUM_SPIKES; i++)
|
|
607
|
+
if (!spike_active[i]) try_spawn_spike(i);
|
|
608
|
+
|
|
609
|
+
/* Physics: gravity + sub-pixel Y. */
|
|
610
|
+
if (vy_q44 < MAX_VY_Q44) vy_q44 += GRAVITY_Q44;
|
|
215
611
|
py_q44 += vy_q44;
|
|
612
|
+
y8 = (uint8_t)(py_q44 >> 4);
|
|
216
613
|
|
|
217
|
-
/*
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
614
|
+
/* Fell into a pit (below the screen) → lose the turn. */
|
|
615
|
+
if (y8 >= 232) {
|
|
616
|
+
kill_player();
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/* Landing — probe the two level columns under the player's feet. */
|
|
621
|
+
if (vy_q44 >= 0) {
|
|
622
|
+
feet = y8 + 8;
|
|
623
|
+
c0 = (uint8_t)(px + scroll_x) >> 3;
|
|
624
|
+
c1 = (uint8_t)(px + scroll_x + 7) >> 3;
|
|
625
|
+
top = land_top(c0, feet);
|
|
626
|
+
if (top == 0) top = land_top(c1, feet);
|
|
627
|
+
if (top) {
|
|
628
|
+
py_q44 = (uint16_t)(top - 8) << 4;
|
|
629
|
+
vy_q44 = 0;
|
|
630
|
+
if (!on_ground) sound_play_tone(1, 0x2A0, 3, 2); /* landing thud */
|
|
631
|
+
on_ground = 1;
|
|
632
|
+
} else {
|
|
633
|
+
on_ground = 0; /* walked off */
|
|
227
634
|
}
|
|
228
635
|
}
|
|
229
636
|
|
|
230
|
-
/*
|
|
231
|
-
|
|
232
|
-
px
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
637
|
+
/* Coins (collect) + spikes (death). */
|
|
638
|
+
for (i = 0; i < NUM_COINS; i++) {
|
|
639
|
+
if (dist8(coin_x[i], px) < 8 && dist8(coin_y[i], y8) < 8) {
|
|
640
|
+
p_score[cur_player] += 10;
|
|
641
|
+
sound_play_tone(0, 0x0D6, 8, 5); /* coin ping */
|
|
642
|
+
draw_hud();
|
|
643
|
+
respawn_coin(i);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
killed = 0;
|
|
647
|
+
for (i = 0; i < NUM_SPIKES; i++) {
|
|
648
|
+
if (!spike_active[i]) continue;
|
|
649
|
+
if (dist8(spike_x[i], px) < 7 && dist8(SPIKE_Y, y8) < 7) {
|
|
650
|
+
killed = 1;
|
|
651
|
+
break;
|
|
652
|
+
}
|
|
236
653
|
}
|
|
654
|
+
if (killed) kill_player();
|
|
237
655
|
}
|
|
238
656
|
}
|