romdevtools 0.27.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 +56 -44
- package/CHANGELOG.md +355 -0
- package/README.md +4 -4
- package/examples/README.md +7 -7
- package/examples/atari2600/templates/platformer.asm +1227 -325
- package/examples/atari2600/templates/puzzle.asm +1056 -0
- package/examples/atari2600/templates/racing.asm +909 -257
- package/examples/atari2600/templates/shmup.asm +1035 -218
- package/examples/atari2600/templates/sports.asm +1143 -229
- package/examples/atari7800/templates/hello_sprite.c +8 -4
- package/examples/atari7800/templates/platformer.c +991 -152
- package/examples/atari7800/templates/puzzle.c +1091 -145
- package/examples/atari7800/templates/racing.c +949 -118
- package/examples/atari7800/templates/shmup.c +812 -130
- package/examples/atari7800/templates/sports.c +820 -181
- package/examples/c64/templates/platformer.c +876 -157
- package/examples/c64/templates/puzzle.c +881 -143
- package/examples/c64/templates/racing.c +873 -88
- package/examples/c64/templates/shmup.c +762 -154
- package/examples/c64/templates/sports.c +755 -95
- package/examples/gb/templates/platformer.c +841 -175
- package/examples/gb/templates/puzzle.c +1094 -176
- package/examples/gb/templates/racing.c +761 -169
- package/examples/gb/templates/shmup.c +679 -169
- package/examples/gb/templates/sports.c +790 -153
- package/examples/gba/templates/platformer.c +624 -169
- package/examples/gba/templates/puzzle.c +535 -207
- package/examples/gba/templates/racing.c +513 -196
- package/examples/gba/templates/shmup.c +565 -168
- package/examples/gba/templates/sports.c +454 -162
- package/examples/gbc/templates/platformer.c +944 -176
- package/examples/gbc/templates/puzzle.c +1131 -177
- package/examples/gbc/templates/racing.c +891 -175
- package/examples/gbc/templates/shmup.c +827 -179
- package/examples/gbc/templates/sports.c +870 -156
- package/examples/genesis/templates/platformer.c +747 -129
- package/examples/genesis/templates/puzzle.c +702 -208
- package/examples/genesis/templates/racing.c +728 -193
- package/examples/genesis/templates/shmup.c +535 -142
- package/examples/genesis/templates/shmup_2p.c +13 -1
- package/examples/genesis/templates/sports.c +495 -158
- package/examples/gg/templates/platformer.c +883 -214
- package/examples/gg/templates/puzzle.c +906 -181
- package/examples/gg/templates/racing.c +919 -160
- package/examples/gg/templates/shmup.c +716 -177
- package/examples/gg/templates/sports.c +735 -128
- package/examples/lynx/templates/platformer.c +604 -50
- package/examples/lynx/templates/puzzle.c +533 -130
- package/examples/lynx/templates/racing.c +538 -102
- package/examples/lynx/templates/shmup.c +461 -122
- package/examples/lynx/templates/sports.c +496 -69
- package/examples/msx/platformer/main.c +648 -159
- package/examples/msx/puzzle/main.c +750 -185
- package/examples/msx/racing/main.c +669 -177
- package/examples/msx/shmup/main.c +460 -177
- package/examples/msx/sports/main.c +591 -124
- package/examples/nes/templates/platformer.c +586 -160
- package/examples/nes/templates/puzzle.c +603 -222
- package/examples/nes/templates/racing.c +505 -197
- package/examples/nes/templates/shmup.c +339 -144
- package/examples/nes/templates/sports.c +341 -182
- package/examples/pce/platformer/main.c +875 -204
- package/examples/pce/puzzle/main.c +797 -216
- package/examples/pce/racing/main.c +782 -206
- package/examples/pce/shmup/main.c +638 -211
- package/examples/pce/sports/main.c +585 -167
- package/examples/porting-across-platforms/README.md +1 -1
- package/examples/sms/templates/platformer.c +765 -176
- package/examples/sms/templates/puzzle.c +783 -177
- package/examples/sms/templates/racing.c +812 -133
- package/examples/sms/templates/shmup.c +601 -148
- package/examples/sms/templates/shmup_2p.c +17 -1
- package/examples/sms/templates/sports.c +633 -121
- 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 +587 -149
- package/examples/snes/templates/puzzle-data.asm +116 -21
- package/examples/snes/templates/puzzle-hdr.asm +57 -0
- package/examples/snes/templates/puzzle.c +632 -185
- 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 -177
- 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 -180
- 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 -156
- 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 +304 -11
- package/src/http/tool-registry.js +11 -11
- package/src/mcp/server.js +6 -0
- package/src/mcp/tools/cheats.js +2 -1
- package/src/mcp/tools/disasm-rebuild.js +315 -65
- package/src/mcp/tools/disasm.js +149 -28
- package/src/mcp/tools/find-references.js +216 -51
- package/src/mcp/tools/frame.js +14 -6
- package/src/mcp/tools/index.js +18 -4
- package/src/mcp/tools/input.js +31 -7
- package/src/mcp/tools/lifecycle.js +6 -4
- package/src/mcp/tools/memory.js +208 -39
- package/src/mcp/tools/platform-docs.js +1 -1
- package/src/mcp/tools/playtest.js +56 -4
- package/src/mcp/tools/preview-tile.js +6 -2
- package/src/mcp/tools/project.js +1114 -120
- 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 +55 -11
- package/src/mcp/tools/watch-memory.js +145 -27
- 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 +64 -17
- package/src/platforms/atari2600/MENTAL_MODEL.md +5 -1
- package/src/platforms/atari2600/TROUBLESHOOTING.md +40 -0
- package/src/platforms/atari7800/MENTAL_MODEL.md +32 -11
- 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 +19 -4
- package/src/platforms/gb/TROUBLESHOOTING.md +101 -6
- 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 +19 -6
- 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 +16 -4
- package/src/platforms/gbc/TROUBLESHOOTING.md +24 -3
- 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/font.h +43 -0
- package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
- package/src/platforms/gbc/lib/c/patch-header.js +19 -6
- package/src/platforms/genesis/MENTAL_MODEL.md +43 -9
- package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
- package/src/platforms/genesis/lib/c/genesis_sfx.c +37 -0
- package/src/platforms/genesis/lib/c/genesis_sfx.h +1 -0
- package/src/platforms/gg/MENTAL_MODEL.md +4 -4
- package/src/platforms/gg/TROUBLESHOOTING.md +14 -18
- package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gg/lib/c/gg_crt0.s +14 -2
- 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/lynx/lib/c/lynx_sfx.c +38 -2
- package/src/platforms/lynx/lib/c/lynx_sfx.h +1 -0
- package/src/platforms/msx/MENTAL_MODEL.md +11 -5
- package/src/platforms/msx/TROUBLESHOOTING.md +21 -0
- package/src/platforms/msx/lib/c/msx_crt0.s +27 -0
- package/src/platforms/msx/lib/c/msx_hw.h +3 -0
- package/src/platforms/msx/lib/c/msx_vdp.c +70 -0
- package/src/platforms/nes/MENTAL_MODEL.md +12 -5
- package/src/platforms/nes/lib/c/nes_runtime.c +190 -34
- package/src/platforms/nes/lib/c/nes_runtime.h +35 -0
- package/src/platforms/pce/MENTAL_MODEL.md +14 -5
- package/src/platforms/pce/TROUBLESHOOTING.md +9 -0
- package/src/platforms/pce/lib/c/pce_hw.h +13 -1
- package/src/platforms/pce/lib/c/pce_sound.c +22 -0
- package/src/platforms/pce/lib/c/pce_video.c +32 -0
- package/src/platforms/sms/MENTAL_MODEL.md +11 -6
- package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
- package/src/platforms/sms/lib/c/sms_crt0.s +14 -2
- package/src/platforms/snes/MENTAL_MODEL.md +7 -2
- package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
- package/src/playtest/playtest.js +73 -3
- 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 +64 -19
|
@@ -1,169 +1,607 @@
|
|
|
1
|
-
/* ── platformer.c — SNES
|
|
1
|
+
/* ── platformer.c — SNES side-scrolling platformer (complete example game) ───
|
|
2
2
|
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
3
|
+
* CRAG CAPER — 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), SPC music +
|
|
7
|
+
* SFX, and the SNES's answer to the fixed-HUD-over-scrolling-field problem:
|
|
8
|
+
* the HUD is simply ANOTHER BACKGROUND LAYER with its own scroll register.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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 SNES footgun; reshape
|
|
13
|
+
* your gameplay around it (see TROUBLESHOOTING before changing).
|
|
14
|
+
* GAME LOGIC (clay) — level layout, physics tuning, scoring, art: reshape
|
|
15
|
+
* freely.
|
|
11
16
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
17
|
+
* What depends on what:
|
|
18
|
+
* data.asm — font + sprite/level tiles, sram_read16/write16 (battery SRAM
|
|
19
|
+
* needs 24-bit addressing tcc can't emit), and the bank-$7E telem block.
|
|
20
|
+
* Load-bearing.
|
|
21
|
+
* hdr.asm — THIS PROJECT OVERRIDES the stock header to declare battery
|
|
22
|
+
* SRAM (CARTRIDGETYPE $02 + SRAMSIZE $01). Delete that file and saves
|
|
23
|
+
* silently stop existing — the build still succeeds.
|
|
24
|
+
* snes_sfx.{h,c} + snes_sfx_data.asm + apu_blob.bin — the SPC700 sound
|
|
25
|
+
* driver (music + 2 one-shot samples). #include'd, not separately built.
|
|
26
|
+
*
|
|
27
|
+
* ── THE TWO-LAYER SPLIT (the SNES bonus this example teaches) ───────────────
|
|
28
|
+
* Mode 1 gives three independent background layers, EACH with its own
|
|
29
|
+
* H/V scroll registers. So a fixed HUD over a scrolling playfield is just:
|
|
30
|
+
* BG0 (text console) — HUD + all menu text. Its scroll stays (0,0). Ever.
|
|
31
|
+
* BG1 — the level. One register write per frame (bgSetScroll) moves it.
|
|
32
|
+
* Zero raster tricks, zero CPU. Contrast the NES platformer example (this
|
|
33
|
+
* game's direct ancestor): the NES has ONE scroll for the WHOLE frame, so
|
|
34
|
+
* its fixed HUD costs a sprite-0-hit polling spin — ~35 scanlines of CPU
|
|
35
|
+
* burned EVERY frame waiting for the beam to clear the HUD before rewriting
|
|
36
|
+
* PPUSCROLL mid-frame. On SNES you only reach for that kind of mid-frame
|
|
37
|
+
* machinery (HDMA) when one layer must be two things at once — see the
|
|
38
|
+
* racing example's Mode 1/Mode 7 split.
|
|
39
|
+
*
|
|
40
|
+
* The level itself: a 256-px-wide COLUMN MAP (ground height + one-way
|
|
41
|
+
* platforms + pits) painted once into a 32x32 tilemap. 256 px is exactly
|
|
42
|
+
* the map's width, so a uint8 scroll wraps seamlessly — an endless looping
|
|
43
|
+
* run of pits, platforms, coins and spikes. Coins/spikes are sprites that
|
|
44
|
+
* drift with the scroll (world-anchored while on screen, respawning at the
|
|
45
|
+
* right edge).
|
|
19
46
|
*/
|
|
20
47
|
|
|
21
48
|
#include <snes.h>
|
|
22
49
|
#include "snes_sfx.c"
|
|
23
50
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
51
|
+
/* The title screen renders this — examples({op:'fork'}) stamps your game's
|
|
52
|
+
* name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
|
|
53
|
+
#define GAME_TITLE "CRAG CAPER"
|
|
54
|
+
|
|
55
|
+
extern char tilfont, palfont; /* HUD font + text palette (data.asm) */
|
|
56
|
+
extern char tilsprite, palsprite; /* player/coin/spike tiles + palette */
|
|
57
|
+
extern char tilbg, palbg; /* level tiles + sky/dirt/grass colours*/
|
|
27
58
|
|
|
28
59
|
/* consoleVblank() copies the dirty text tilemap to VRAM during VBlank.
|
|
29
60
|
* No public prototype in console.h, so declare it; call once per frame. */
|
|
30
61
|
extern void consoleVblank(void);
|
|
31
62
|
|
|
32
|
-
/*
|
|
33
|
-
*
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
63
|
+
/* data.asm exports — battery SRAM accessors ($70:0000 long addressing) and
|
|
64
|
+
* the bank-$7E telemetry block a headless test can find by scanning. */
|
|
65
|
+
extern u16 sram_read16(u16 offset);
|
|
66
|
+
extern void sram_write16(u16 offset, u16 value);
|
|
67
|
+
extern u8 telem[];
|
|
68
|
+
|
|
69
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
70
|
+
* VRAM budget (word addresses):
|
|
71
|
+
* $0000 OBJ tiles, $2000 level tiles, $3000 HUD font,
|
|
72
|
+
* $4000 level map (BG1), $6800 HUD/console text map (BG0).
|
|
73
|
+
* Sprite tile numbers + the level tile numbers the map painter uses. */
|
|
74
|
+
#define TILE_IDLE 0
|
|
75
|
+
#define TILE_JUMP 1
|
|
76
|
+
#define TILE_COIN 2
|
|
77
|
+
#define TILE_SPIKE 3
|
|
78
|
+
#define BG_CLOUD 1
|
|
79
|
+
#define BG_DIRT 2
|
|
80
|
+
#define BG_GRASS 3 /* also used for floating platforms (grass slabs) */
|
|
81
|
+
|
|
82
|
+
/* ── GAME LOGIC (clay) — the level ───────────────────────────────────────────
|
|
83
|
+
* A 32-column map; world x = (screen x + scroll) mod 256.
|
|
84
|
+
* ground_row[c] — tilemap row of the ground's grass top, 0xFF = pit.
|
|
85
|
+
* plat_row[c] — row of a one-way floating platform, 0 = none.
|
|
86
|
+
* Rows are tilemap rows (y = row*8). The SNES screen shows rows 0-27. */
|
|
87
|
+
#define NO_GROUND 0xFF
|
|
88
|
+
static const u8 ground_row[32] = {
|
|
89
|
+
26, 26, 26, 26, 26, 26, 26, 26, /* start runway */
|
|
90
|
+
26, NO_GROUND, NO_GROUND, 26, 26, 26, 26, 26, /* pit 1 (16 px) */
|
|
91
|
+
26, 26, 26, 26, NO_GROUND, NO_GROUND, NO_GROUND, /* pit 2 (24 px) */
|
|
92
|
+
26, 26, 26, 26, 26, 26, 26, 26, 26,
|
|
52
93
|
};
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
94
|
+
static const u8 plat_row[32] = {
|
|
95
|
+
0, 0, 0, 0, 21, 21, 21, 0, /* slab before pit 1 */
|
|
96
|
+
0, 0, 0, 0, 0, 0, 20, 20, /* slab mid-level */
|
|
97
|
+
20, 0, 0, 0, 0, 0, 0, 0,
|
|
98
|
+
0, 21, 21, 21, 0, 0, 0, 0, /* slab near the loop */
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/* ── GAME LOGIC (clay) — physics + tuning ── */
|
|
102
|
+
#define GRAVITY_Q44 1 /* +1/16 px per frame per frame */
|
|
103
|
+
#define JUMP_VEL_Q44 (-40) /* launch vy (Q4.4) → ~50 px / ~6 tile apex */
|
|
104
|
+
#define MAX_VY_Q44 80 /* terminal velocity, 5 px/frame — MUST stay *
|
|
105
|
+
* under 6: the landing probe's 6-px window *
|
|
106
|
+
* can't catch a faster fall (tunnelling) */
|
|
107
|
+
#define MOVE_SPEED 2 /* px/frame walk + scroll speed */
|
|
108
|
+
#define SCROLL_WALL 112 /* px: past this the world scrolls, not you */
|
|
109
|
+
#define GROUND_TOP 208 /* ground_row 26 * 8 */
|
|
110
|
+
#define SPIKE_Y 200 /* spikes stand on the ground */
|
|
111
|
+
#define NUM_COINS 3
|
|
112
|
+
#define NUM_SPIKES 2
|
|
113
|
+
#define START_LIVES 3
|
|
114
|
+
|
|
115
|
+
/* SRAM layout: [0]=magic "CG", [2]=hi-score, [4]=hi ^ 0x5AC3.
|
|
116
|
+
* Magic is written LAST in hi_save so a torn write never validates. */
|
|
117
|
+
#define SRAM_MAGIC 0x4743u
|
|
118
|
+
|
|
119
|
+
/* Game states — the shell every example shares: title → play → game over. */
|
|
120
|
+
#define ST_TITLE 0
|
|
121
|
+
#define ST_PLAY 1
|
|
122
|
+
#define ST_OVER 2
|
|
123
|
+
|
|
124
|
+
static u8 state;
|
|
125
|
+
static u8 px; /* player screen x */
|
|
126
|
+
static u16 py_q44; /* player y, Q4.4 fixed point — gravity adds
|
|
127
|
+
* <1 px/frame near the jump apex, so we
|
|
128
|
+
* need sub-pixel precision */
|
|
129
|
+
static s8 vy_q44;
|
|
130
|
+
static u8 on_ground;
|
|
131
|
+
static u8 scroll_x; /* level scroll — u8 wraps at 256 = exactly *
|
|
132
|
+
* one level loop (seamless) */
|
|
133
|
+
static u8 dist_sub; /* sub-counter: 64 px scrolled = +1 pt */
|
|
134
|
+
static u8 coin_x[NUM_COINS], coin_y[NUM_COINS];
|
|
135
|
+
static u8 spike_x[NUM_SPIKES], spike_active[NUM_SPIKES];
|
|
136
|
+
|
|
137
|
+
/* Players: index 0 = P1 (controller 1), 1 = P2 (controller 2 — alternating
|
|
138
|
+
* turns, arcade-classic style). Each has own score + own lives; the HUD
|
|
139
|
+
* shows the CURRENT player's numbers. */
|
|
140
|
+
static u8 two_player;
|
|
141
|
+
static u8 cur_player;
|
|
142
|
+
static u8 p_lives[2];
|
|
143
|
+
static u16 p_score[2];
|
|
144
|
+
static u16 hiscore;
|
|
145
|
+
static u8 turn_pause; /* freeze frames after a turn change */
|
|
146
|
+
static u8 sound_ok;
|
|
147
|
+
static u16 rng = 0xC0DE;
|
|
148
|
+
static u16 prev_pad0, prev_padP;
|
|
149
|
+
static u8 attract_sub; /* title attract: scroll every 2nd frame */
|
|
150
|
+
static char tbuf[8]; /* 5-digit score formatter output */
|
|
151
|
+
|
|
152
|
+
static u16 bg_map[32 * 32]; /* level tilemap staging (DMA'd at boot) */
|
|
153
|
+
|
|
154
|
+
/* ── GAME LOGIC (clay) — xorshift16 PRNG (~tens of cycles per call) ── */
|
|
155
|
+
static u8 random8(void) {
|
|
156
|
+
u16 r = rng;
|
|
157
|
+
r ^= r << 7;
|
|
158
|
+
r ^= r >> 9;
|
|
159
|
+
r ^= r << 8;
|
|
160
|
+
rng = r;
|
|
161
|
+
return (u8)r;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
static u8 dist8(u8 a, u8 b) {
|
|
165
|
+
return (a > b) ? (u8)(a - b) : (u8)(b - a);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* ── GAME LOGIC (clay) — battery SRAM hi-score (see sram_* in data.asm) ───── */
|
|
169
|
+
static u16 hi_load(void) {
|
|
170
|
+
u16 v;
|
|
171
|
+
if (sram_read16(0) != SRAM_MAGIC) return 0;
|
|
172
|
+
v = sram_read16(2);
|
|
173
|
+
if (sram_read16(4) != (u16)(v ^ 0x5AC3u)) return 0;
|
|
174
|
+
return v;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
static void hi_save(u16 v) {
|
|
178
|
+
sram_write16(2, v);
|
|
179
|
+
sram_write16(4, (u16)(v ^ 0x5AC3u));
|
|
180
|
+
sram_write16(0, SRAM_MAGIC); /* magic LAST — torn write = no record */
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/* ── GAME LOGIC (clay) — text helpers ──────────────────────────────────────── */
|
|
184
|
+
static void fmt_u16(u16 v) { /* 5 right-aligned digits into tbuf */
|
|
185
|
+
u8 i;
|
|
186
|
+
for (i = 0; i < 5; i++) { tbuf[4 - i] = (char)('0' + v % 10); v /= 10; }
|
|
187
|
+
tbuf[5] = 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
static void clear_row(u16 y) {
|
|
191
|
+
consoleDrawText(0, y, " ");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
static void clear_rows(u16 a, u16 b) {
|
|
195
|
+
u16 y;
|
|
196
|
+
for (y = a; y <= b; y++) clear_row(y);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/* HUD row 1, on BG0 — fixed because BG0's scroll never moves (see the
|
|
200
|
+
* two-layer split note up top). Layout: "P1 L3 SC 00000 HI 00000". */
|
|
201
|
+
static void draw_hud(void) {
|
|
202
|
+
consoleDrawText(1, 1, cur_player ? "P2" : "P1");
|
|
203
|
+
tbuf[0] = 'L'; tbuf[1] = (char)('0' + p_lives[cur_player]); tbuf[2] = 0;
|
|
204
|
+
consoleDrawText(4, 1, tbuf);
|
|
205
|
+
fmt_u16(p_score[cur_player]);
|
|
206
|
+
consoleDrawText(10, 1, tbuf);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
static void draw_hud_labels(void) {
|
|
210
|
+
consoleDrawText(7, 1, "SC");
|
|
211
|
+
consoleDrawText(17, 1, "HI");
|
|
212
|
+
fmt_u16(hiscore);
|
|
213
|
+
consoleDrawText(20, 1, tbuf);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* ── GAME LOGIC (clay) — paint the level from the column map ─────────────────
|
|
217
|
+
* Composed once in WRAM and DMA'd to VRAM at boot (bgInitMapSet). The level
|
|
218
|
+
* is static; only the scroll register moves it. Rows 0-2 stay sky so the
|
|
219
|
+
* HUD text floats over clean backdrop. Map entries are plain tile numbers
|
|
220
|
+
* (palette block 0, no flips, no priority). */
|
|
221
|
+
static void paint_level(void) {
|
|
222
|
+
u8 r, c, g;
|
|
223
|
+
u16 t;
|
|
224
|
+
for (r = 0; r < 32; r++) {
|
|
225
|
+
for (c = 0; c < 32; c++) {
|
|
226
|
+
g = ground_row[c];
|
|
227
|
+
t = 0; /* sky backdrop */
|
|
228
|
+
if (plat_row[c] && r == plat_row[c]) t = BG_GRASS; /* floating slab */
|
|
229
|
+
else if (g != NO_GROUND) {
|
|
230
|
+
if (r == g) t = BG_GRASS; /* ground surface */
|
|
231
|
+
else if (r > g) t = BG_DIRT; /* ground body */
|
|
232
|
+
}
|
|
233
|
+
if (t == 0 && r >= 14 && r <= 18) { /* sparse cloud band */
|
|
234
|
+
if (((r * 7 + c * 5) & 15) == 0) t = BG_CLOUD;
|
|
235
|
+
}
|
|
236
|
+
bg_map[(u16)(r << 5) + c] = t;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/* ── GAME LOGIC (clay) — coins + spikes (sprite objects in the world) ── */
|
|
242
|
+
static const u8 coin_heights[4] = { 184, 160, 128, 152 };
|
|
243
|
+
static void respawn_coin(u8 i) {
|
|
244
|
+
coin_x[i] = (u8)(232 + (random8() & 15)); /* enter at the right */
|
|
245
|
+
coin_y[i] = coin_heights[random8() & 3];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
static void try_spawn_spike(u8 i) {
|
|
249
|
+
/* Anchor only over ground: an inactive spike rolls a low per-frame
|
|
250
|
+
* chance, and only spawns if the level column entering at the right
|
|
251
|
+
* edge has ground under it (never floats over a pit). */
|
|
252
|
+
u8 c = (u8)(248 + scroll_x) >> 3;
|
|
253
|
+
if (ground_row[c] == NO_GROUND) return;
|
|
254
|
+
if (random8() > 4) return;
|
|
255
|
+
spike_x[i] = 248;
|
|
256
|
+
spike_active[i] = 1;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/* Hide every gameplay sprite (OAM ids 0,4,..,20 = player, 3 coins, 2 spikes). */
|
|
260
|
+
static void hide_actors(void) {
|
|
261
|
+
u8 i;
|
|
262
|
+
for (i = 0; i < 24; i += 4) oamSetVisible(i, OBJ_HIDE);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
266
|
+
* Stage ALL sprites BEFORE WaitForVBlank. PVSnesLib's NMI handler DMAs the
|
|
267
|
+
* shadow OAM to the real OAM every vblank (on channel 7 — never park HDMA
|
|
268
|
+
* there), copying whatever the shadow holds AT THAT MOMENT. Stage-then-wait;
|
|
269
|
+
* flipping it shows stale/empty sprites. oamSet rewrites x/y, which is also
|
|
270
|
+
* what un-hides a sprite after OBJ_HIDE (hide just parks it off-screen). */
|
|
271
|
+
static void stage_actors(void) {
|
|
272
|
+
u8 i, y8;
|
|
273
|
+
y8 = (u8)(py_q44 >> 4);
|
|
274
|
+
/* Blink the player during the turn-change breather. */
|
|
275
|
+
if (turn_pause == 0 || (turn_pause & 4))
|
|
276
|
+
oamSet(0, px, y8, 3, 0, 0, on_ground ? TILE_IDLE : TILE_JUMP, 0);
|
|
277
|
+
else
|
|
278
|
+
oamSetVisible(0, OBJ_HIDE);
|
|
279
|
+
for (i = 0; i < NUM_COINS; i++)
|
|
280
|
+
oamSet((u16)(4 + (i << 2)), coin_x[i], coin_y[i], 3, 0, 0, TILE_COIN, 0);
|
|
281
|
+
for (i = 0; i < NUM_SPIKES; i++) {
|
|
282
|
+
if (spike_active[i])
|
|
283
|
+
oamSet((u16)(16 + (i << 2)), spike_x[i], SPIKE_Y, 3, 0, 0, TILE_SPIKE, 0);
|
|
284
|
+
else
|
|
285
|
+
oamSetVisible((u16)(16 + (i << 2)), OBJ_HIDE);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/* ── GAME LOGIC (clay) — state entries ─────────────────────────────────────── */
|
|
290
|
+
static void title_enter(void) {
|
|
291
|
+
bgSetEnable(1); /* the level scrolls behind the title */
|
|
292
|
+
hide_actors();
|
|
293
|
+
clear_rows(0, 27);
|
|
294
|
+
consoleDrawText(11, 3, GAME_TITLE);
|
|
295
|
+
consoleDrawText(10, 6, "A - 1P GAME");
|
|
296
|
+
consoleDrawText(10, 7, "B - 2P TURNS");
|
|
297
|
+
consoleDrawText(11, 9, "HI");
|
|
298
|
+
fmt_u16(hiscore);
|
|
299
|
+
consoleDrawText(14, 9, tbuf);
|
|
300
|
+
state = ST_TITLE;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/* ── GAME LOGIC (clay) — start a turn / a run ── */
|
|
304
|
+
static void begin_turn(void) {
|
|
305
|
+
px = 24;
|
|
306
|
+
py_q44 = (u16)(GROUND_TOP - 8) << 4;
|
|
307
|
+
vy_q44 = 0;
|
|
308
|
+
on_ground = 1;
|
|
309
|
+
scroll_x = 0;
|
|
310
|
+
dist_sub = 0;
|
|
311
|
+
coin_x[0] = 88; coin_y[0] = 184;
|
|
312
|
+
coin_x[1] = 152; coin_y[1] = 160;
|
|
313
|
+
coin_x[2] = 216; coin_y[2] = 128;
|
|
314
|
+
spike_x[0] = 136; spike_active[0] = 1; /* both anchored on ground at */
|
|
315
|
+
spike_x[1] = 224; spike_active[1] = 1; /* scroll 0 — see ground_row */
|
|
316
|
+
turn_pause = 48; /* "P1/P2 GO" breather */
|
|
317
|
+
prev_padP = 0xFFFF; /* swallow held buttons across the turn change —
|
|
318
|
+
* without this the A that picked 1P on the title
|
|
319
|
+
* instantly jumps (classic edge-detect reuse bug) */
|
|
320
|
+
draw_hud();
|
|
321
|
+
if (two_player)
|
|
322
|
+
consoleDrawText(11, 4, cur_player ? "PLAYER 2 GO" : "PLAYER 1 GO");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
static void start_game(u8 players) {
|
|
326
|
+
u8 i;
|
|
327
|
+
two_player = players;
|
|
328
|
+
cur_player = 0;
|
|
329
|
+
p_score[0] = p_score[1] = 0;
|
|
330
|
+
p_lives[0] = START_LIVES;
|
|
331
|
+
p_lives[1] = players ? START_LIVES : 0;
|
|
332
|
+
clear_rows(0, 27);
|
|
333
|
+
draw_hud_labels();
|
|
334
|
+
for (i = 0; i < 24; i += 4) oamSetEx(i, OBJ_SMALL, OBJ_SHOW);
|
|
335
|
+
begin_turn();
|
|
336
|
+
if (sound_ok) sfx_play(1); /* start blip */
|
|
337
|
+
state = ST_PLAY;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
static void game_over(void) {
|
|
341
|
+
u16 best = p_score[0];
|
|
342
|
+
if (two_player && p_score[1] > best) best = p_score[1];
|
|
343
|
+
if (best > hiscore) { hiscore = best; hi_save(hiscore); }
|
|
344
|
+
bgSetDisable(1); /* clean card: sky backdrop + text only */
|
|
345
|
+
hide_actors();
|
|
346
|
+
clear_rows(0, 27);
|
|
347
|
+
consoleDrawText(11, 6, "GAME OVER");
|
|
348
|
+
consoleDrawText(9, 10, "P1");
|
|
349
|
+
fmt_u16(p_score[0]); consoleDrawText(15, 10, tbuf);
|
|
350
|
+
if (two_player) {
|
|
351
|
+
consoleDrawText(9, 12, "P2");
|
|
352
|
+
fmt_u16(p_score[1]); consoleDrawText(15, 12, tbuf);
|
|
353
|
+
}
|
|
354
|
+
consoleDrawText(9, 15, "HI");
|
|
355
|
+
fmt_u16(hiscore); consoleDrawText(15, 15, tbuf);
|
|
356
|
+
consoleDrawText(9, 20, "START - TITLE");
|
|
357
|
+
if (sound_ok) sfx_play(2); /* game-over thud */
|
|
358
|
+
state = ST_OVER;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/* ── GAME LOGIC (clay) — death + alternating-turn handoff ── */
|
|
362
|
+
static void kill_player(void) {
|
|
363
|
+
u8 other;
|
|
364
|
+
if (sound_ok) sfx_play(2);
|
|
365
|
+
if (p_lives[cur_player] > 0) --p_lives[cur_player];
|
|
366
|
+
if (two_player) {
|
|
367
|
+
other = cur_player ^ 1;
|
|
368
|
+
if (p_lives[other] > 0) cur_player = other; /* swap turns */
|
|
369
|
+
else if (p_lives[cur_player] == 0) { game_over(); return; }
|
|
370
|
+
} else if (p_lives[0] == 0) {
|
|
371
|
+
game_over();
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
begin_turn();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/* ── GAME LOGIC (clay) — landing probe against the column map ──────────────
|
|
378
|
+
* One-way platforms, classic style: only catch the player while FALLING
|
|
379
|
+
* through a narrow window at the surface. The window is 6 px tall —
|
|
380
|
+
* top-1 (the standing snap parks feet at top, and gravity's sub-pixel
|
|
381
|
+
* trickle doesn't move the integer Y every frame; without the -1 slack the
|
|
382
|
+
* player "stands" with on_ground=0 most frames, so jumps only register on
|
|
383
|
+
* lucky frames and the idle/jump sprite flickers) through top+4 (so a
|
|
384
|
+
* 5 px/frame terminal-velocity fall can't step over it). */
|
|
385
|
+
static u8 land_top(u8 c, u8 feet) {
|
|
386
|
+
u8 r, top;
|
|
387
|
+
r = plat_row[c];
|
|
388
|
+
if (r) {
|
|
389
|
+
top = (u8)(r << 3);
|
|
390
|
+
if ((u8)(feet + 1) >= top && feet <= (u8)(top + 4)) return top;
|
|
391
|
+
}
|
|
392
|
+
r = ground_row[c];
|
|
393
|
+
if (r != NO_GROUND) {
|
|
394
|
+
top = (u8)(r << 3);
|
|
395
|
+
if ((u8)(feet + 1) >= top && feet <= (u8)(top + 4)) return top;
|
|
396
|
+
}
|
|
397
|
+
return 0;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/* ── GAME LOGIC (clay) — one frame of gameplay ─────────────────────────────── */
|
|
401
|
+
static void play_update(void) {
|
|
402
|
+
u16 pad;
|
|
403
|
+
u8 i, delta, y8, feet, c0, c1, top, killed;
|
|
404
|
+
|
|
405
|
+
if (turn_pause) { /* freeze gameplay, keep the frame honest */
|
|
406
|
+
--turn_pause;
|
|
407
|
+
if (turn_pause == 0) clear_row(4); /* drop the "Pn GO" banner */
|
|
408
|
+
stage_actors();
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/* Input — the CURRENT player's controller (alternating turns: P2 is on
|
|
413
|
+
* controller 2 — padsCurrent(1); that one index IS the 2P wiring). Past
|
|
414
|
+
* SCROLL_WALL the world scrolls instead of the player (the camera never
|
|
415
|
+
* scrolls back — the classic one-way camera). */
|
|
416
|
+
pad = padsCurrent(cur_player);
|
|
417
|
+
delta = 0;
|
|
418
|
+
if (pad & KEY_RIGHT) {
|
|
419
|
+
if (px < SCROLL_WALL) px += MOVE_SPEED;
|
|
420
|
+
else { scroll_x += MOVE_SPEED; delta = MOVE_SPEED; }
|
|
421
|
+
}
|
|
422
|
+
if ((pad & KEY_LEFT) && px > 8) px -= MOVE_SPEED;
|
|
423
|
+
if ((pad & (KEY_B | KEY_A)) && !(prev_padP & (KEY_B | KEY_A)) && on_ground) {
|
|
424
|
+
vy_q44 = JUMP_VEL_Q44;
|
|
425
|
+
on_ground = 0;
|
|
426
|
+
if (sound_ok) sfx_play(1); /* jump blip */
|
|
427
|
+
}
|
|
428
|
+
prev_padP = pad;
|
|
429
|
+
|
|
430
|
+
/* World objects drift left as the level scrolls (world-anchored). */
|
|
431
|
+
if (delta) {
|
|
432
|
+
dist_sub += delta;
|
|
433
|
+
if (dist_sub >= 64) { /* distance pay */
|
|
434
|
+
dist_sub -= 64;
|
|
435
|
+
++p_score[cur_player];
|
|
436
|
+
draw_hud();
|
|
437
|
+
}
|
|
438
|
+
for (i = 0; i < NUM_COINS; i++) {
|
|
439
|
+
if (coin_x[i] < 16 + delta) respawn_coin(i);
|
|
440
|
+
else coin_x[i] -= delta;
|
|
441
|
+
}
|
|
442
|
+
for (i = 0; i < NUM_SPIKES; i++) {
|
|
443
|
+
if (!spike_active[i]) continue;
|
|
444
|
+
if (spike_x[i] < 16 + delta) spike_active[i] = 0;
|
|
445
|
+
else spike_x[i] -= delta;
|
|
66
446
|
}
|
|
67
|
-
|
|
447
|
+
}
|
|
448
|
+
for (i = 0; i < NUM_SPIKES; i++)
|
|
449
|
+
if (!spike_active[i]) try_spawn_spike(i);
|
|
450
|
+
|
|
451
|
+
/* Physics: gravity + sub-pixel Y. */
|
|
452
|
+
if (vy_q44 < MAX_VY_Q44) vy_q44 += GRAVITY_Q44;
|
|
453
|
+
py_q44 += vy_q44;
|
|
454
|
+
y8 = (u8)(py_q44 >> 4);
|
|
455
|
+
|
|
456
|
+
/* Fell into a pit (below the screen) → lose the turn. */
|
|
457
|
+
if (y8 >= 232) {
|
|
458
|
+
kill_player();
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/* Landing — probe the two level columns under the player's feet. */
|
|
463
|
+
if (vy_q44 >= 0) {
|
|
464
|
+
feet = (u8)(y8 + 8);
|
|
465
|
+
c0 = (u8)(px + scroll_x) >> 3;
|
|
466
|
+
c1 = (u8)(px + scroll_x + 7) >> 3;
|
|
467
|
+
top = land_top(c0, feet);
|
|
468
|
+
if (top == 0) top = land_top(c1, feet);
|
|
469
|
+
if (top) {
|
|
470
|
+
py_q44 = (u16)(top - 8) << 4;
|
|
471
|
+
vy_q44 = 0;
|
|
472
|
+
on_ground = 1;
|
|
473
|
+
} else {
|
|
474
|
+
on_ground = 0; /* walked off */
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/* Coins (collect) + spikes (death). */
|
|
479
|
+
for (i = 0; i < NUM_COINS; i++) {
|
|
480
|
+
if (dist8(coin_x[i], px) < 8 && dist8(coin_y[i], y8) < 8) {
|
|
481
|
+
p_score[cur_player] += 10;
|
|
482
|
+
if (sound_ok) sfx_play(1); /* coin ping */
|
|
483
|
+
draw_hud();
|
|
484
|
+
respawn_coin(i);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
killed = 0;
|
|
488
|
+
for (i = 0; i < NUM_SPIKES; i++) {
|
|
489
|
+
if (!spike_active[i]) continue;
|
|
490
|
+
if (dist8(spike_x[i], px) < 7 && dist8(SPIKE_Y, y8) < 7) {
|
|
491
|
+
killed = 1;
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (killed) { kill_player(); return; }
|
|
496
|
+
|
|
497
|
+
stage_actors();
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/* Headless-test telemetry — written once per frame into the bank-$7E telem
|
|
501
|
+
* block (data.asm). A test harness finds it by scanning WRAM for the
|
|
502
|
+
* "CG"+0xBD signature, then plays the game from real state instead of
|
|
503
|
+
* parsing pixels. spike_x is always even (spawns at 248, drifts by 2), so
|
|
504
|
+
* its bit 0 carries the active flag. Costs ~20 byte-writes; delete freely. */
|
|
505
|
+
static void telem_update(void) {
|
|
506
|
+
telem[0] = 'C'; telem[1] = 'G'; telem[2] = 0xBD;
|
|
507
|
+
telem[3] = state;
|
|
508
|
+
telem[4] = (u8)((sound_ok << 7) | (two_player << 1) | cur_player);
|
|
509
|
+
telem[5] = p_lives[0];
|
|
510
|
+
telem[6] = p_lives[1];
|
|
511
|
+
telem[7] = px;
|
|
512
|
+
telem[8] = (u8)(py_q44 >> 4);
|
|
513
|
+
telem[9] = scroll_x;
|
|
514
|
+
telem[10] = on_ground;
|
|
515
|
+
telem[11] = (u8)p_score[0]; telem[12] = (u8)(p_score[0] >> 8);
|
|
516
|
+
telem[13] = (u8)p_score[1]; telem[14] = (u8)(p_score[1] >> 8);
|
|
517
|
+
telem[15] = turn_pause;
|
|
518
|
+
telem[16] = (u8)(spike_x[0] | spike_active[0]);
|
|
519
|
+
telem[17] = (u8)(spike_x[1] | spike_active[1]);
|
|
68
520
|
}
|
|
69
521
|
|
|
70
522
|
int main(void) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (vy > 0) {
|
|
141
|
-
for (i = 0; i < N_PLATFORMS; i++) {
|
|
142
|
-
p = &platforms[i];
|
|
143
|
-
if (ipy + 8 <= p->y && npy + 8 >= p->y
|
|
144
|
-
&& ipx + 8 > p->x && ipx < p->x + p->w) {
|
|
145
|
-
py = (p->y - 8) << 4;
|
|
146
|
-
vy = 0;
|
|
147
|
-
goto done;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
py = np;
|
|
152
|
-
if (py > 224 << 4) { py = 0; vy = 0; }
|
|
153
|
-
done: ;
|
|
154
|
-
|
|
155
|
-
/* Camera follows the player, centered, clamped to the world.
|
|
156
|
-
* bgSetScroll moves the BG in hardware; the player draws in
|
|
157
|
-
* SCREEN space (worldX - camX). */
|
|
158
|
-
camX = (px >> 4) - (SCREEN_W / 2 - 4);
|
|
159
|
-
if (camX < 0) camX = 0;
|
|
160
|
-
if (camX > WORLD_W - SCREEN_W) camX = WORLD_W - SCREEN_W;
|
|
161
|
-
bgSetScroll(0, camX, 0);
|
|
162
|
-
|
|
163
|
-
oamSetXY(0, (px >> 4) - camX, py >> 4);
|
|
164
|
-
oamUpdate();
|
|
165
|
-
WaitForVBlank();
|
|
166
|
-
consoleVblank();
|
|
523
|
+
u16 pad;
|
|
524
|
+
|
|
525
|
+
/* ── HARDWARE IDIOM (load-bearing — see TROUBLESHOOTING) ──
|
|
526
|
+
* Init order: console text pointers FIRST, then mode, then VRAM uploads
|
|
527
|
+
* while the screen is still off (forced blank — VRAM DMA during active
|
|
528
|
+
* display is lost or corrupts). consoleInitText DMAs the font but does
|
|
529
|
+
* NOT set the PPU BG base registers — bgSetGfxPtr/bgSetMapPtr must agree
|
|
530
|
+
* with the console pointers or text renders as garbage tiles. */
|
|
531
|
+
consoleSetTextMapPtr(0x6800);
|
|
532
|
+
consoleSetTextGfxPtr(0x3000);
|
|
533
|
+
consoleSetTextOffset(0x0000);
|
|
534
|
+
consoleInitText(0, 16 * 2, &tilfont, &palfont);
|
|
535
|
+
setMode(BG_MODE1, 0);
|
|
536
|
+
bgSetGfxPtr(0, 0x3000);
|
|
537
|
+
bgSetMapPtr(0, 0x6800, SC_32x32);
|
|
538
|
+
|
|
539
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
540
|
+
* The two-layer split (see the header essay): BG0 = HUD/text, scroll
|
|
541
|
+
* pinned at (0,0); BG1 = the level, moved by one bgSetScroll per frame.
|
|
542
|
+
* palbg loads into CGRAM block 0 AFTER the font palette and is a superset
|
|
543
|
+
* of it (colour 1 stays white) — so HUD ink and level tiles share the
|
|
544
|
+
* block without fighting. BG2 carries power-on garbage in Mode 1 — keep
|
|
545
|
+
* it disabled. */
|
|
546
|
+
paint_level();
|
|
547
|
+
bgInitTileSet(1, (u8 *)&tilbg, (u8 *)&palbg, 0, 4 * 32, 32, BG_16COLORS, 0x2000);
|
|
548
|
+
bgInitMapSet(1, (u8 *)bg_map, sizeof(bg_map), SC_32x32, 0x4000);
|
|
549
|
+
bgSetEnable(1);
|
|
550
|
+
bgSetDisable(2);
|
|
551
|
+
|
|
552
|
+
/* OBJ: 8x8 sprites (player, coins, spikes) at VRAM $0000. */
|
|
553
|
+
oamInitGfxSet(&tilsprite, 4 * 32, &palsprite, 32, 0, 0x0000, OBJ_SIZE8_L16);
|
|
554
|
+
|
|
555
|
+
setScreenOn();
|
|
556
|
+
|
|
557
|
+
/* ── HARDWARE IDIOM (load-bearing) — sfx_init AFTER setScreenOn, and CHECK
|
|
558
|
+
* the return: a wedged SPC700 must not take the video down with it. ── */
|
|
559
|
+
sound_ok = (sfx_init() == 0);
|
|
560
|
+
/* ── HARDWARE IDIOM (load-bearing) — one frame between init and the first
|
|
561
|
+
* command. sfx_init returns the instant the SPC echoes the jump command,
|
|
562
|
+
* but the driver then spends ~50 port writes initialising the DSP BEFORE
|
|
563
|
+
* it seeds its command edge-detector from $2140. Send a command in that
|
|
564
|
+
* window and the seed swallows it — music silently never starts. A
|
|
565
|
+
* WaitForVBlank is thousands of SPC cycles — deterministic cure. ── */
|
|
566
|
+
WaitForVBlank();
|
|
567
|
+
if (sound_ok) sfx_music_play();
|
|
568
|
+
|
|
569
|
+
hiscore = hi_load(); /* battery SRAM — 0 on first boot */
|
|
570
|
+
prev_pad0 = prev_padP = 0;
|
|
571
|
+
title_enter();
|
|
572
|
+
|
|
573
|
+
while (1) {
|
|
574
|
+
pad = padsCurrent(0);
|
|
575
|
+
|
|
576
|
+
if (state == ST_TITLE) {
|
|
577
|
+
/* attract: the level drifts by under the title — the scroll register
|
|
578
|
+
* demo, and the first thing a fork breaks if the layers get swapped */
|
|
579
|
+
attract_sub ^= 1;
|
|
580
|
+
if (attract_sub) scroll_x++;
|
|
581
|
+
if ((pad & KEY_A && !(prev_pad0 & KEY_A)) ||
|
|
582
|
+
(pad & KEY_START && !(prev_pad0 & KEY_START))) {
|
|
583
|
+
start_game(0);
|
|
584
|
+
} else if (pad & KEY_B && !(prev_pad0 & KEY_B)) {
|
|
585
|
+
start_game(1);
|
|
586
|
+
}
|
|
587
|
+
} else if (state == ST_PLAY) {
|
|
588
|
+
play_update();
|
|
589
|
+
} else { /* ST_OVER */
|
|
590
|
+
if ((pad & (KEY_START | KEY_A)) && !(prev_pad0 & (KEY_START | KEY_A)))
|
|
591
|
+
title_enter();
|
|
167
592
|
}
|
|
168
|
-
|
|
593
|
+
prev_pad0 = pad;
|
|
594
|
+
telem_update();
|
|
595
|
+
oamUpdate();
|
|
596
|
+
|
|
597
|
+
WaitForVBlank();
|
|
598
|
+
/* ── HARDWARE IDIOM (load-bearing) — scroll + text commits in vblank.
|
|
599
|
+
* bgSetScroll writes the BG1 scroll registers directly; mid-frame the
|
|
600
|
+
* beam would render the top of the frame with the old value and the
|
|
601
|
+
* bottom with the new (a shear). BG0 gets NO scroll write, ever —
|
|
602
|
+
* that's the whole fixed-HUD trick. ── */
|
|
603
|
+
bgSetScroll(1, scroll_x, 0);
|
|
604
|
+
consoleVblank();
|
|
605
|
+
}
|
|
606
|
+
return 0;
|
|
169
607
|
}
|