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,167 +1,785 @@
|
|
|
1
|
-
/* ── platformer.c — Genesis
|
|
1
|
+
/* ── platformer.c — Genesis side-scrolling platformer (complete example game) ─
|
|
2
2
|
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
3
|
+
* CINDER SPRINT — 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 (cartridge SRAM), music +
|
|
7
|
+
* SFX, and the Genesis's signature feature: DUAL-PLANE PARALLAX with
|
|
8
|
+
* per-strip (cell) horizontal scroll — a dusk mountain ridge that slides at
|
|
9
|
+
* HALF the foreground speed, under a hardware-fixed WINDOW-plane HUD.
|
|
8
10
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
+
* THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
|
|
12
|
+
* very different one. The markers tell you what's what:
|
|
13
|
+
* HARDWARE IDIOM (load-bearing) — dodges a documented Genesis footgun;
|
|
14
|
+
* reshape your gameplay around it (see TROUBLESHOOTING before changing).
|
|
15
|
+
* GAME LOGIC (clay) — level layout, physics tuning, scoring, art: reshape
|
|
16
|
+
* freely.
|
|
11
17
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* the
|
|
18
|
+
* What depends on what:
|
|
19
|
+
* genesis_sfx.{h,c} — PSG sound wrapper (tones + noise + a background
|
|
20
|
+
* melody loop). For full FM music, see the xgm2_demo template
|
|
21
|
+
* (XGM2_loadDriver + XGM2_play + a .xgc blob incbin'd via a data.s
|
|
22
|
+
* sibling) — we use the PSG path here so the platformer stays a
|
|
23
|
+
* single-file game; the swap is three lines plus the data.s sibling.
|
|
24
|
+
* rom_header.c (SGDK) — the Sega header at $100. Its 'RA' block at $1B0
|
|
25
|
+
* DECLARES the cartridge SRAM that hiscore_load/save below depend on
|
|
26
|
+
* (see the SRAM idiom). The build assembles it automatically.
|
|
27
|
+
*
|
|
28
|
+
* The level: a 512-px-wide COLUMN MAP (ground height + one-way slabs + pits)
|
|
29
|
+
* painted once into plane A. The plane is exactly 512 px (64 cells) wide and
|
|
30
|
+
* the VDP scroll WRAPS within the plane, so a forever-incrementing camera
|
|
31
|
+
* loops the level seamlessly — an endless run of pits, slabs, coins and
|
|
32
|
+
* spikes with ZERO tilemap writes per frame (hardware scroll is free;
|
|
33
|
+
* rewriting tilemaps in the loop is the #1 "choppy movement" bug).
|
|
34
|
+
*
|
|
35
|
+
* Frame budget (NTSC, 60 fps): player physics + a two-column ground probe +
|
|
36
|
+
* (3 coins + 2 spikes) of AABB + 56 hscroll words + 6 SAT entries queued for
|
|
37
|
+
* vblank DMA — a tiny fraction of the 68000's frame. The vblank DMA budget
|
|
38
|
+
* (~7 KB/frame in H40) is the real ceiling on Genesis; we use < 200 bytes.
|
|
21
39
|
*/
|
|
22
40
|
|
|
23
41
|
#include <genesis.h>
|
|
24
42
|
#include "genesis_sfx.h"
|
|
25
43
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
#define
|
|
29
|
-
|
|
44
|
+
/* The title screen renders this — examples({op:'fork'}) stamps your game's
|
|
45
|
+
* name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
|
|
46
|
+
#define GAME_TITLE "CINDER SPRINT"
|
|
47
|
+
|
|
48
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
49
|
+
* CONTROLLER MAPPING — two layers, both bite:
|
|
50
|
+
*
|
|
51
|
+
* On the pad: SGDK's JOY_readJoypad(JOY_1/JOY_2) returns BUTTON_A/B/C/
|
|
52
|
+
* START/UP/DOWN/LEFT/RIGHT as a bitmask. Jump is BUTTON_A or BUTTON_C
|
|
53
|
+
* (real Genesis games map action buttons generously — thumbs rest on C).
|
|
54
|
+
*
|
|
55
|
+
* Driving this game HEADLESSLY through an emulator (libretro/gpgx): the
|
|
56
|
+
* core maps Genesis A/B/C onto libretro Y/B/A. So setInput({y:true})
|
|
57
|
+
* presses GENESIS A (jump/start here), setInput({b:true}) presses GENESIS
|
|
58
|
+
* B (2P select), and setInput({a:true}) presses GENESIS C — NOT Genesis A.
|
|
59
|
+
* Getting this wrong looks like "the game ignores input". START is start.
|
|
60
|
+
*/
|
|
61
|
+
#define BTN_JUMP (BUTTON_A | BUTTON_C)
|
|
62
|
+
|
|
63
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
64
|
+
* Tile art. Genesis tiles are 4bpp: each u32 row = 8 pixels, one hex nibble
|
|
65
|
+
* per pixel = a colour index into the tile's palette line (0 = transparent).
|
|
66
|
+
* Sprites use PAL0, plane A (world) PAL1, plane B (backdrop) PAL2. */
|
|
67
|
+
#define T_GRASS (TILE_USER_INDEX + 0) /* plane A: ground surface */
|
|
68
|
+
#define T_DIRT (TILE_USER_INDEX + 1) /* plane A: ground body */
|
|
69
|
+
#define T_SLAB (TILE_USER_INDEX + 2) /* plane A: one-way platform */
|
|
70
|
+
#define T_SKY (TILE_USER_INDEX + 3) /* plane B: dusk sky */
|
|
71
|
+
#define T_CLOUD (TILE_USER_INDEX + 4) /* plane B: drifting cloud */
|
|
72
|
+
#define T_PEAK (TILE_USER_INDEX + 5) /* plane B: mountain ridge tip */
|
|
73
|
+
#define T_MOUNT (TILE_USER_INDEX + 6) /* plane B: mountain body */
|
|
74
|
+
#define T_HUDBAND (TILE_USER_INDEX + 7) /* plane B: flat band behind HUD */
|
|
75
|
+
#define T_PLAYER (TILE_USER_INDEX + 8) /* sprite: idle */
|
|
76
|
+
#define T_JUMP (TILE_USER_INDEX + 9) /* sprite: airborne */
|
|
77
|
+
#define T_COIN (TILE_USER_INDEX + 10) /* sprite: pickup */
|
|
78
|
+
#define T_SPIKE (TILE_USER_INDEX + 11) /* sprite: hazard */
|
|
30
79
|
|
|
31
|
-
static const u32
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
80
|
+
static const u32 tile_grass[8] = { /* grass lip over speckled dirt */
|
|
81
|
+
0x11111111, 0x11111111, 0x22222222, 0x22322222,
|
|
82
|
+
0x22222232, 0x22222222, 0x23222222, 0x22222322,
|
|
83
|
+
};
|
|
84
|
+
static const u32 tile_dirt[8] = { /* speckles make motion visible */
|
|
85
|
+
0x22222222, 0x22232222, 0x22222222, 0x32222223,
|
|
86
|
+
0x22222222, 0x22223222, 0x22222222, 0x23222222,
|
|
87
|
+
};
|
|
88
|
+
static const u32 tile_slab[8] = { /* thin one-way platform (top */
|
|
89
|
+
0x44444444, 0x45555554, 0x55555555, /* half only — jump up through */
|
|
90
|
+
0x05555550, 0x00000000, 0x00000000, /* the transparent bottom) */
|
|
91
|
+
0x00000000, 0x00000000,
|
|
92
|
+
};
|
|
93
|
+
static const u32 tile_sky[8] = {
|
|
94
|
+
0x11111111, 0x11111111, 0x11111111, 0x11111111,
|
|
95
|
+
0x11111111, 0x11111111, 0x11111111, 0x11111111,
|
|
96
|
+
};
|
|
97
|
+
static const u32 tile_cloud[8] = {
|
|
98
|
+
0x11111111, 0x11222111, 0x12222221, 0x22222222,
|
|
99
|
+
0x12222221, 0x11111111, 0x11111111, 0x11111111,
|
|
100
|
+
};
|
|
101
|
+
static const u32 tile_peak[8] = { /* ridge tip: triangle over sky */
|
|
102
|
+
0x11133111, 0x11333311, 0x13333331, 0x33333333,
|
|
103
|
+
0x33433333, 0x33333333, 0x33333433, 0x33333333,
|
|
104
|
+
};
|
|
105
|
+
static const u32 tile_mount[8] = { /* body speckled for parallax */
|
|
106
|
+
0x33333333, 0x33343333, 0x33333333, /* visibility — a flat colour */
|
|
107
|
+
0x33333343, 0x43333333, 0x33333333, /* shifted N px looks identical */
|
|
108
|
+
0x33334333, 0x33333333, /* to itself (motion invisible) */
|
|
109
|
+
};
|
|
110
|
+
static const u32 tile_hudband[8] = {
|
|
111
|
+
0x55555555, 0x55555555, 0x55555555, 0x55555555,
|
|
112
|
+
0x55555555, 0x55555555, 0x55555555, 0x55555555,
|
|
35
113
|
};
|
|
36
|
-
static const u32 tile_player[8]
|
|
37
|
-
|
|
38
|
-
|
|
114
|
+
static const u32 tile_player[8] = { /* ember sprite, idle */
|
|
115
|
+
0x00222200, 0x02222220, 0x22322322, 0x22222222,
|
|
116
|
+
0x22222222, 0x02222220, 0x02200220, 0x02200220,
|
|
39
117
|
};
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
118
|
+
static const u32 tile_jump[8] = { /* arms up, legs tucked */
|
|
119
|
+
0x22000022, 0x22222222, 0x02322320, 0x02222220,
|
|
120
|
+
0x02222220, 0x00222200, 0x02000020, 0x20000002,
|
|
121
|
+
};
|
|
122
|
+
static const u32 tile_coin[8] = {
|
|
123
|
+
0x00444400, 0x04555540, 0x45444454, 0x45444454,
|
|
124
|
+
0x45444454, 0x45444454, 0x04555540, 0x00444400,
|
|
125
|
+
};
|
|
126
|
+
static const u32 tile_spike[8] = {
|
|
127
|
+
0x00000000, 0x00077000, 0x00077000, 0x00766700,
|
|
128
|
+
0x00766700, 0x07666670, 0x07666670, 0x76666667,
|
|
44
129
|
};
|
|
45
130
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
131
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
132
|
+
* The level — a 64-column map; world x = (screen x + camera) mod 512.
|
|
133
|
+
* ground_row[c] — plane row of the grass top, 0xFF = pit.
|
|
134
|
+
* plat_row[c] — row of a one-way slab, 0 = none.
|
|
135
|
+
* Rows are PLANE rows (y = row*8). Rows 0-1 sit under the HUD window;
|
|
136
|
+
* playfield rows are 2..27 (the visible 224-px screen is 28 rows). */
|
|
137
|
+
#define NO_GROUND 0xFF
|
|
138
|
+
#define GROUND_R 24 /* default ground surface row */
|
|
139
|
+
static const u8 ground_row[64] = {
|
|
140
|
+
24, 24, 24, 24, 24, 24, 24, 24, /* start runway */
|
|
141
|
+
24, 24, 24, 24, 24, 24, 24, 24,
|
|
142
|
+
24, 24, 24, 24, NO_GROUND, NO_GROUND, NO_GROUND, /* pit 1 (24 px) */
|
|
143
|
+
24, 24, 24, 24, 24, 24, 24, 24, 24,
|
|
144
|
+
24, 24, NO_GROUND, NO_GROUND, 24, 24, 24, /* pit 2 (16 px) */
|
|
145
|
+
24, 24, 24, 24, 24, 24, 24, 24,
|
|
146
|
+
NO_GROUND, NO_GROUND, NO_GROUND, 24, 24, 24, /* pit 3 (24 px) */
|
|
147
|
+
24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
|
|
148
|
+
};
|
|
149
|
+
static const u8 plat_row[64] = {
|
|
150
|
+
0, 0, 0, 0, 0, 0, 21, 21, /* warm-up slab */
|
|
151
|
+
21, 0, 0, 0, 0, 0, 0, 0,
|
|
152
|
+
0, 19, 19, 19, 19, 0, 0, 0, /* bridge over pit 1*/
|
|
153
|
+
0, 0, 0, 21, 21, 0, 0, 0,
|
|
154
|
+
0, 0, 19, 19, 0, 0, 0, 0, /* hop over pit 2 */
|
|
155
|
+
0, 21, 21, 0, 0, 17, 17, 17, /* stairs up... */
|
|
156
|
+
0, 0, 19, 19, 0, 0, 0, 0, /* ...and over pit 3*/
|
|
157
|
+
0, 0, 0, 0, 21, 21, 21, 0,
|
|
158
|
+
};
|
|
159
|
+
/* Mountain ridge silhouette for plane B — 16-column period (128 px), so a
|
|
160
|
+
* half-speed shift is unambiguous to the eye (and to a headless pixel
|
|
161
|
+
* probe). Values are the plane row of each column's ridge tip. */
|
|
162
|
+
static const u8 ridge_top[16] = {
|
|
163
|
+
13, 12, 11, 10, 10, 11, 12, 13, 14, 13, 11, 10, 11, 12, 13, 14,
|
|
61
164
|
};
|
|
62
|
-
#define N_PLATFORMS (sizeof(platforms) / sizeof(platforms[0]))
|
|
63
165
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
166
|
+
/* ── GAME LOGIC (clay) — physics + tuning (Q4.4 fixed point: 16 = 1 px) ── */
|
|
167
|
+
#define GRAVITY_Q44 1 /* +1/16 px per frame per frame */
|
|
168
|
+
#define JUMP_VEL_Q44 (-40) /* launch vy → ~50 px apex (~6 tile rows) */
|
|
169
|
+
#define MAX_VY_Q44 80 /* terminal 5 px/frame — MUST stay under 6: *
|
|
170
|
+
* the landing probe's 6-px window can't *
|
|
171
|
+
* catch a faster fall (tunnelling) */
|
|
172
|
+
#define MOVE_SPEED 2 /* px/frame walk + scroll speed */
|
|
173
|
+
#define SCROLL_WALL 144 /* px: past this the world scrolls, not you */
|
|
174
|
+
#define GROUND_TOP 192 /* GROUND_R * 8 */
|
|
175
|
+
#define SPIKE_Y 184 /* spikes stand on the ground */
|
|
176
|
+
#define SCREEN_W 320 /* H40 mode */
|
|
177
|
+
#define NUM_COINS 3
|
|
178
|
+
#define NUM_SPIKES 2
|
|
179
|
+
#define START_LIVES 3
|
|
180
|
+
#define HUD_ROWS 2 /* window rows reserved for the HUD */
|
|
181
|
+
|
|
182
|
+
static s16 px; /* player screen x */
|
|
183
|
+
static u16 py_q44; /* player y, Q4.4 — gravity adds <1 px/frame *
|
|
184
|
+
* near the apex; integer y would stick */
|
|
185
|
+
static s16 vy_q44;
|
|
186
|
+
static u8 on_ground;
|
|
187
|
+
static u16 cam; /* camera/world scroll. NEVER reset mid-run *
|
|
188
|
+
* and never wrapped by hand: the plane is *
|
|
189
|
+
* 512 px and the VDP masks scroll values to *
|
|
190
|
+
* the plane, and 65536 is a multiple of 512 *
|
|
191
|
+
* (and of 512*2 and 512*8), so plain u16 *
|
|
192
|
+
* overflow keeps BOTH parallax layers *
|
|
193
|
+
* seamless forever. */
|
|
194
|
+
static u8 dist_sub; /* sub-counter: 64 px scrolled = +1 point */
|
|
195
|
+
static s16 coin_x[NUM_COINS];
|
|
196
|
+
static s16 coin_y[NUM_COINS];
|
|
197
|
+
static s16 spike_x[NUM_SPIKES];
|
|
198
|
+
static u8 spike_active[NUM_SPIKES];
|
|
199
|
+
|
|
200
|
+
/* Players: index 0 = P1 (controller 1), 1 = P2 (controller 2 — alternating
|
|
201
|
+
* turns, arcade-classic style). Each has own score + own lives; the HUD
|
|
202
|
+
* shows the CURRENT player's numbers. */
|
|
203
|
+
static u8 two_player;
|
|
204
|
+
static u8 cur_player;
|
|
205
|
+
static u8 p_lives[2];
|
|
206
|
+
static u16 p_score[2];
|
|
207
|
+
static u16 hiscore;
|
|
208
|
+
static u8 turn_pause; /* freeze frames after a turn change */
|
|
209
|
+
static u16 rng = 0xC0DE;
|
|
210
|
+
|
|
211
|
+
/* Game states — the shell every example shares: title → play → game over. */
|
|
212
|
+
#define ST_TITLE 0
|
|
213
|
+
#define ST_PLAY 1
|
|
214
|
+
#define ST_OVER 2
|
|
215
|
+
static u8 state;
|
|
216
|
+
static u16 prev_pad;
|
|
217
|
+
|
|
218
|
+
/* ── GAME LOGIC (clay) — xorshift16 PRNG (a few 68k instructions) ── */
|
|
219
|
+
static u8 random8(void) {
|
|
220
|
+
u16 r = rng;
|
|
221
|
+
r ^= r << 7;
|
|
222
|
+
r ^= r >> 9;
|
|
223
|
+
r ^= r << 8;
|
|
224
|
+
rng = r;
|
|
225
|
+
return (u8)r;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
229
|
+
* CARTRIDGE SRAM — the Genesis battery-save mechanism, three parts:
|
|
230
|
+
*
|
|
231
|
+
* 1. The ROM HEADER declares it: bytes $1B0.. hold 'R','A', a type word
|
|
232
|
+
* ($F820 = battery-backed, byte-wide on ODD addresses — the classic
|
|
233
|
+
* cart wiring), then start/end addresses $200000/$20FFFF. SGDK's
|
|
234
|
+
* rom_header.c (assembled into every build) already declares exactly
|
|
235
|
+
* this — no linker work needed. Emulators allocate the save RAM by
|
|
236
|
+
* READING THIS HEADER; no 'RA' block = writes to $200000+ go nowhere.
|
|
237
|
+
* 2. The MAPPER GATE: writing 1 to $A130F1 banks SRAM into $200000+,
|
|
238
|
+
* 0 banks the ROM back in. SGDK's SRAM_enable()/SRAM_disable() do
|
|
239
|
+
* this. ALWAYS disable after access — on carts >2 MB the SRAM window
|
|
240
|
+
* shadows ROM, and leaving it enabled corrupts later ROM fetches.
|
|
241
|
+
* 3. ODD-BYTE ADDRESSING: SRAM_readByte/writeByte(offset) access 68k
|
|
242
|
+
* address $200001 + offset*2. Headlessly, the emulator's save_ram
|
|
243
|
+
* region interleaves with dead even bytes: SGDK offset k lives at
|
|
244
|
+
* save_ram[k*2 + 1] (the even bytes read back $FF).
|
|
245
|
+
*
|
|
246
|
+
* Hi-score record layout (SGDK offsets): 0='H' 1='S' 2=lo 3=hi
|
|
247
|
+
* 4=checksum(lo^hi^$A5). Fresh SRAM is all $FF — the magic+checksum
|
|
248
|
+
* rejects it (and any corruption) so first boot shows 0, not 65535.
|
|
249
|
+
*
|
|
250
|
+
* Emulator note (verified against gpgx): the core sizes its save_ram
|
|
251
|
+
* region by scanning for the last non-$FF byte, so the region reads as
|
|
252
|
+
* EMPTY until the first write below lands — that's why hiscore_init runs
|
|
253
|
+
* at the very top of main(). Real hardware and .srm-restoring frontends
|
|
254
|
+
* have no such wrinkle. */
|
|
255
|
+
static u16 hiscore_load(void) {
|
|
256
|
+
u8 m0, m1, lo, hi, ck;
|
|
257
|
+
SRAM_enableRO();
|
|
258
|
+
m0 = SRAM_readByte(0);
|
|
259
|
+
m1 = SRAM_readByte(1);
|
|
260
|
+
lo = SRAM_readByte(2);
|
|
261
|
+
hi = SRAM_readByte(3);
|
|
262
|
+
ck = SRAM_readByte(4);
|
|
263
|
+
SRAM_disable();
|
|
264
|
+
if (m0 == 'H' && m1 == 'S' && ck == (u8)(lo ^ hi ^ 0xA5))
|
|
265
|
+
return ((u16)hi << 8) | lo;
|
|
266
|
+
return 0;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
static void hiscore_save(u16 sc) {
|
|
270
|
+
u8 lo = (u8)sc, hi = (u8)(sc >> 8);
|
|
271
|
+
SRAM_enable();
|
|
272
|
+
SRAM_writeByte(0, 'H');
|
|
273
|
+
SRAM_writeByte(1, 'S');
|
|
274
|
+
SRAM_writeByte(2, lo);
|
|
275
|
+
SRAM_writeByte(3, hi);
|
|
276
|
+
SRAM_writeByte(4, (u8)(lo ^ hi ^ 0xA5));
|
|
277
|
+
SRAM_disable();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/* Format-on-first-boot: if the magic is absent (fresh battery), write a
|
|
281
|
+
* valid zero record immediately so the save file exists from frame one. */
|
|
282
|
+
static void hiscore_init(void) {
|
|
283
|
+
hiscore = hiscore_load();
|
|
284
|
+
if (hiscore == 0) hiscore_save(0);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
288
|
+
* DUAL-PLANE PARALLAX + per-strip scroll — THE Genesis signature. The VDP
|
|
289
|
+
* composites two independent tilemap planes (A above B), each with its own
|
|
290
|
+
* horizontal scroll, and the scroll can vary DOWN THE SCREEN:
|
|
291
|
+
*
|
|
292
|
+
* VDP_setScrollingMode(HSCROLL_TILE, VSCROLL_PLANE) tells the VDP to
|
|
293
|
+
* fetch one hscroll entry per 8-LINE STRIP from the hscroll table in
|
|
294
|
+
* VRAM (28 strips cover the 224-line screen) instead of one per frame.
|
|
295
|
+
* Each strip entry is a pair of words: plane A's offset, then plane B's.
|
|
296
|
+
*
|
|
297
|
+
* Per frame we queue two 28-word tables:
|
|
298
|
+
* plane A: every strip = -cam (the world, 1:1 with the camera)
|
|
299
|
+
* plane B: sky strips = -(cam / 8) (far: barely moves)
|
|
300
|
+
* ridge strips= -(cam / 2) (the half-speed mountain layer)
|
|
301
|
+
* Three speeds from two planes — banding ONE plane by strip is how real
|
|
302
|
+
* carts faked 3+ layers. POSITIVE camera = NEGATIVE scroll value (the
|
|
303
|
+
* scroll offset slides the plane right; we want the world to slide left).
|
|
304
|
+
*
|
|
305
|
+
* DELUXE VARIANT (not used here, same table): HSCROLL_LINE gives one entry
|
|
306
|
+
* per SCANLINE — 224 s16s per plane. Fill them with smooth per-line speeds
|
|
307
|
+
* for a sky gradient, or add sin(line+frame) ripple inside a water band
|
|
308
|
+
* (VDP_setHorizontalScrollLine). Costs ~1.8 KB/frame of vblank DMA versus
|
|
309
|
+
* our 224 bytes, so budget it (H40 vblank fits ~7 KB).
|
|
310
|
+
*
|
|
311
|
+
* Requires: HSCROLL_TILE mode set BEFORE the first table write; BOTH
|
|
312
|
+
* tables queued every frame you move the camera (a stale plane-A table
|
|
313
|
+
* shears the world); DMA_QUEUE so the VRAM writes land in vblank, never
|
|
314
|
+
* mid-frame (SYS_doVBlankProcess flushes the queue — mid-frame writes
|
|
315
|
+
* tear the strip boundary); the value arrays static (the queue reads
|
|
316
|
+
* them AT FLUSH TIME — stack arrays are gone by then, shipping garbage).
|
|
317
|
+
* Plane-size note: A and B share ONE size setting (default 64x32 cells =
|
|
318
|
+
* 512x256 px). You can't size them independently. */
|
|
319
|
+
#define SKY_STRIPS 10 /* strips 0-9 = HUD band + sky */
|
|
320
|
+
static s16 hsA[28];
|
|
321
|
+
static s16 hsB[28];
|
|
322
|
+
static void apply_camera(void) {
|
|
323
|
+
u16 i;
|
|
324
|
+
for (i = 0; i < 28; i++) {
|
|
325
|
+
hsA[i] = -(s16)cam;
|
|
326
|
+
hsB[i] = (i < SKY_STRIPS) ? -(s16)(cam >> 3) : -(s16)(cam >> 1);
|
|
327
|
+
}
|
|
328
|
+
VDP_setHorizontalScrollTile(BG_A, 0, hsA, 28, DMA_QUEUE);
|
|
329
|
+
VDP_setHorizontalScrollTile(BG_B, 0, hsB, 28, DMA_QUEUE);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
333
|
+
* WINDOW-PLANE HUD — the fixed status bar. The window is a third tilemap
|
|
334
|
+
* that REPLACES plane A wherever it's shown and IGNORES ALL SCROLLING —
|
|
335
|
+
* a hardware-fixed HUD with zero per-frame cost. (The NES needs a sprite-0
|
|
336
|
+
* raster trick for this; on Genesis it's one register.)
|
|
337
|
+
* VDP_setWindowOnTop(2) shows it on the top 2 cell rows; text goes in with
|
|
338
|
+
* VDP_drawTextBG(WINDOW, ...). Two footguns:
|
|
339
|
+
* - The window only lives at screen edges (top/bottom N rows or left/
|
|
340
|
+
* right N columns) — it cannot float mid-screen.
|
|
341
|
+
* - It replaces plane A ONLY: plane B and sprites still render behind/
|
|
342
|
+
* over it. We paint plane B's top rows with a flat dark band so HUD
|
|
343
|
+
* text always reads, and nothing in the game flies above y=16. */
|
|
344
|
+
static void hud_init(void) {
|
|
345
|
+
VDP_setWindowOnTop(HUD_ROWS);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/* ── GAME LOGIC (clay) — HUD text (window plane, redrawn only on change) ── */
|
|
349
|
+
static void draw_u16(VDPPlane plane, u16 v, u16 x, u16 y) {
|
|
350
|
+
char buf[8];
|
|
351
|
+
uintToStr(v, buf, 5);
|
|
352
|
+
VDP_drawTextBG(plane, buf, x, y);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
static void draw_hud(void) {
|
|
356
|
+
char b[4];
|
|
357
|
+
b[0] = 'P'; b[1] = '1' + cur_player; b[2] = 0;
|
|
358
|
+
VDP_drawTextBG(WINDOW, b, 1, 0);
|
|
359
|
+
b[0] = 'x'; b[1] = '0' + p_lives[cur_player]; b[2] = 0;
|
|
360
|
+
VDP_drawTextBG(WINDOW, b, 4, 0);
|
|
361
|
+
VDP_drawTextBG(WINDOW, "SC", 8, 0);
|
|
362
|
+
draw_u16(WINDOW, p_score[cur_player], 11, 0);
|
|
363
|
+
VDP_drawTextBG(WINDOW, "HI", 18, 0);
|
|
364
|
+
draw_u16(WINDOW, hiscore, 21, 0);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
static void draw_hud_title(void) {
|
|
368
|
+
VDP_clearTextAreaBG(WINDOW, 0, 0, 40, HUD_ROWS);
|
|
369
|
+
VDP_drawTextBG(WINDOW, "HI", 18, 0);
|
|
370
|
+
draw_u16(WINDOW, hiscore, 21, 0);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/* ── GAME LOGIC (clay) — paint the two planes ───────────────────────────────
|
|
374
|
+
* Plane B (backdrop) is painted ONCE at boot and never touched again.
|
|
375
|
+
* Plane A is repainted on state changes only (title text ↔ the level).
|
|
376
|
+
* NOTHING repaints inside the frame loop — scroll is hardware. */
|
|
377
|
+
static void paint_backdrop(void) {
|
|
378
|
+
u16 c, r;
|
|
379
|
+
/* Flat dark band behind the window HUD (rows 0-1). */
|
|
380
|
+
VDP_fillTileMapRect(BG_B, TILE_ATTR_FULL(PAL2, 0, 0, 0, T_HUDBAND),
|
|
381
|
+
0, 0, 64, HUD_ROWS);
|
|
382
|
+
for (c = 0; c < 64; c++) {
|
|
383
|
+
u16 top = ridge_top[c & 15];
|
|
384
|
+
/* Dusk sky with deterministic clouds (PRNG would repaint different
|
|
385
|
+
* art after a console reset — fine, but determinism helps tests). */
|
|
386
|
+
for (r = HUD_ROWS; r < top; r++) {
|
|
387
|
+
u16 t = (((r * 5 + c * 11) & 31) == 0 && r >= 3) ? T_CLOUD : T_SKY;
|
|
388
|
+
VDP_setTileMapXY(BG_B, TILE_ATTR_FULL(PAL2, 0, 0, 0, t), c, r);
|
|
389
|
+
}
|
|
390
|
+
VDP_setTileMapXY(BG_B, TILE_ATTR_FULL(PAL2, 0, 0, 0, T_PEAK), c, top);
|
|
391
|
+
VDP_fillTileMapRect(BG_B, TILE_ATTR_FULL(PAL2, 0, 0, 0, T_MOUNT),
|
|
392
|
+
c, top + 1, 1, 32 - (top + 1));
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
static void paint_level(void) {
|
|
397
|
+
u16 c;
|
|
398
|
+
VDP_clearPlane(BG_A, TRUE);
|
|
399
|
+
for (c = 0; c < 64; c++) {
|
|
400
|
+
u8 g = ground_row[c];
|
|
401
|
+
if (g != NO_GROUND) {
|
|
402
|
+
VDP_setTileMapXY(BG_A, TILE_ATTR_FULL(PAL1, 0, 0, 0, T_GRASS), c, g);
|
|
403
|
+
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL1, 0, 0, 0, T_DIRT),
|
|
404
|
+
c, g + 1, 1, 32 - (g + 1));
|
|
405
|
+
}
|
|
406
|
+
if (plat_row[c])
|
|
407
|
+
VDP_setTileMapXY(BG_A, TILE_ATTR_FULL(PAL1, 0, 0, 0, T_SLAB),
|
|
408
|
+
c, plat_row[c]);
|
|
68
409
|
}
|
|
69
|
-
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/* ── GAME LOGIC (clay) — the title screen (text on plane A, scroll 0) ── */
|
|
413
|
+
static void paint_title(void) {
|
|
414
|
+
VDP_clearPlane(BG_A, TRUE);
|
|
415
|
+
VDP_drawTextBG(BG_A, GAME_TITLE, (40 - (sizeof(GAME_TITLE) - 1)) / 2, 8);
|
|
416
|
+
VDP_drawTextBG(BG_A, "1P START - A", 14, 14);
|
|
417
|
+
VDP_drawTextBG(BG_A, "2P TURNS - B", 14, 16);
|
|
418
|
+
VDP_drawTextBG(BG_A, "JUMP THE PITS - GRAB THE COINS", 5, 21);
|
|
419
|
+
draw_hud_title();
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/* ── GAME LOGIC (clay) — the game-over results screen ── */
|
|
423
|
+
static void paint_over(void) {
|
|
424
|
+
VDP_clearPlane(BG_A, TRUE);
|
|
425
|
+
VDP_drawTextBG(BG_A, "GAME OVER", 15, 8);
|
|
426
|
+
VDP_drawTextBG(BG_A, "P1", 13, 12);
|
|
427
|
+
draw_u16(BG_A, p_score[0], 17, 12);
|
|
428
|
+
if (two_player) {
|
|
429
|
+
VDP_drawTextBG(BG_A, "P2", 13, 14);
|
|
430
|
+
draw_u16(BG_A, p_score[1], 17, 14);
|
|
431
|
+
}
|
|
432
|
+
VDP_drawTextBG(BG_A, "HI", 13, 17);
|
|
433
|
+
draw_u16(BG_A, hiscore, 17, 17);
|
|
434
|
+
VDP_drawTextBG(BG_A, "START - TITLE", 13, 21);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/* ── GAME LOGIC (clay) — coins + spikes (sprite objects in the world) ── */
|
|
438
|
+
static const s16 coin_heights[4] = { 168, 144, 120, 152 };
|
|
439
|
+
static void respawn_coin(u16 i) {
|
|
440
|
+
coin_x[i] = SCREEN_W + 8 + (random8() & 31); /* enter at the right */
|
|
441
|
+
coin_y[i] = coin_heights[random8() & 3];
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
static void try_spawn_spike(u16 i) {
|
|
445
|
+
/* Anchor only over ground: an inactive spike rolls a low per-frame
|
|
446
|
+
* chance, and only spawns if the level column entering at the right
|
|
447
|
+
* edge has ground under it (never floats over a pit). */
|
|
448
|
+
u16 c = ((u16)(SCREEN_W + 8 + cam) >> 3) & 63;
|
|
449
|
+
if (ground_row[c] == NO_GROUND) return;
|
|
450
|
+
if (random8() > 4) return;
|
|
451
|
+
spike_x[i] = SCREEN_W + 8;
|
|
452
|
+
spike_active[i] = 1;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/* ── GAME LOGIC (clay) — start a turn / a run ── */
|
|
456
|
+
static void begin_turn(void) {
|
|
457
|
+
u16 i;
|
|
458
|
+
px = 24;
|
|
459
|
+
py_q44 = (u16)(GROUND_TOP - 8) << 4;
|
|
460
|
+
vy_q44 = 0;
|
|
461
|
+
on_ground = 1;
|
|
462
|
+
cam = 0;
|
|
463
|
+
dist_sub = 0;
|
|
464
|
+
coin_x[0] = 120; coin_y[0] = 168;
|
|
465
|
+
coin_x[1] = 200; coin_y[1] = 144;
|
|
466
|
+
coin_x[2] = 280; coin_y[2] = 120;
|
|
467
|
+
for (i = 0; i < NUM_SPIKES; i++) spike_active[i] = 0;
|
|
468
|
+
spike_x[0] = 232; spike_active[0] = 1; /* runway columns — always */
|
|
469
|
+
spike_x[1] = 304; spike_active[1] = 1; /* ground at cam 0 */
|
|
470
|
+
turn_pause = 48; /* "P1/P2 ready" breather */
|
|
471
|
+
prev_pad = 0xFFFF; /* swallow held buttons across*
|
|
472
|
+
* the turn change */
|
|
473
|
+
apply_camera();
|
|
474
|
+
draw_hud();
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
static void start_game(u8 players) {
|
|
478
|
+
two_player = players;
|
|
479
|
+
cur_player = 0;
|
|
480
|
+
p_score[0] = p_score[1] = 0;
|
|
481
|
+
p_lives[0] = START_LIVES;
|
|
482
|
+
p_lives[1] = players ? START_LIVES : 0;
|
|
483
|
+
paint_level();
|
|
484
|
+
begin_turn();
|
|
485
|
+
sfx_tone(0, 523, 10); /* start jingle (C5) */
|
|
486
|
+
state = ST_PLAY;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
static void game_over(void) {
|
|
490
|
+
u16 best = p_score[0];
|
|
491
|
+
if (two_player && p_score[1] > best) best = p_score[1];
|
|
492
|
+
if (best > hiscore) {
|
|
493
|
+
hiscore = best;
|
|
494
|
+
hiscore_save(hiscore); /* battery SRAM — see the SRAM idiom */
|
|
495
|
+
}
|
|
496
|
+
state = ST_OVER;
|
|
497
|
+
cam = 0;
|
|
498
|
+
apply_camera();
|
|
499
|
+
draw_hud(); /* refresh the window HUD — HI may have just changed */
|
|
500
|
+
paint_over();
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/* ── GAME LOGIC (clay) — death + alternating-turn handoff ── */
|
|
504
|
+
static void kill_player(void) {
|
|
505
|
+
u8 other;
|
|
506
|
+
sfx_noise(14);
|
|
507
|
+
if (p_lives[cur_player] > 0) --p_lives[cur_player];
|
|
508
|
+
if (two_player) {
|
|
509
|
+
other = cur_player ^ 1;
|
|
510
|
+
if (p_lives[other] > 0) cur_player = other; /* swap turns */
|
|
511
|
+
else if (p_lives[cur_player] == 0) { game_over(); return; }
|
|
512
|
+
} else if (p_lives[0] == 0) {
|
|
513
|
+
game_over();
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
begin_turn();
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/* ── GAME LOGIC (clay) — landing probe against the column map ──────────────
|
|
520
|
+
* One-way platforms, arcade-classic style: only catch the player while
|
|
521
|
+
* FALLING through a narrow window at the surface: top-1 (the standing snap
|
|
522
|
+
* parks feet exactly at top, and gravity's sub-pixel trickle doesn't move
|
|
523
|
+
* the integer y every frame — without the -1 slack the player "stands"
|
|
524
|
+
* with on_ground=0 most frames and jumps only register on lucky frames)
|
|
525
|
+
* through top+4 (so a 5 px/frame terminal fall can't step over it). */
|
|
526
|
+
static s16 land_top(u16 c, s16 feet) {
|
|
527
|
+
u8 r;
|
|
528
|
+
s16 top;
|
|
529
|
+
r = plat_row[c];
|
|
530
|
+
if (r) {
|
|
531
|
+
top = (s16)r << 3;
|
|
532
|
+
if (feet + 1 >= top && feet <= top + 4) return top;
|
|
533
|
+
}
|
|
534
|
+
r = ground_row[c];
|
|
535
|
+
if (r != NO_GROUND) {
|
|
536
|
+
top = (s16)r << 3;
|
|
537
|
+
if (feet + 1 >= top && feet <= top + 4) return top;
|
|
538
|
+
}
|
|
539
|
+
return 0;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/* ── GAME LOGIC (clay) — stage this frame's sprites ─────────────────────────
|
|
543
|
+
* Fixed SAT slots: 0 = player, 1-3 = coins, 4-5 = spikes. Hidden sprites
|
|
544
|
+
* park at y = -16 (above the screen). NEVER hide with x = -128..0 — a SAT
|
|
545
|
+
* x of 0 is the VDP's sprite-masking trigger and silently blanks every
|
|
546
|
+
* lower-priority sprite on those scanlines. */
|
|
547
|
+
#define HIDE_Y (-16)
|
|
548
|
+
static void stage_sprites(void) {
|
|
549
|
+
u16 i;
|
|
550
|
+
s16 player_y = (s16)(py_q44 >> 4);
|
|
551
|
+
if (state != ST_PLAY) /* no actors off the field */
|
|
552
|
+
VDP_setSprite(0, px, HIDE_Y, SPRITE_SIZE(1, 1),
|
|
553
|
+
TILE_ATTR_FULL(PAL0, 1, 0, 0, T_PLAYER));
|
|
554
|
+
else if (turn_pause == 0 || (turn_pause & 4)) /* blink on handoff */
|
|
555
|
+
VDP_setSprite(0, px, player_y, SPRITE_SIZE(1, 1),
|
|
556
|
+
TILE_ATTR_FULL(PAL0, 1, 0, 0, on_ground ? T_PLAYER : T_JUMP));
|
|
557
|
+
else
|
|
558
|
+
VDP_setSprite(0, px, HIDE_Y, SPRITE_SIZE(1, 1),
|
|
559
|
+
TILE_ATTR_FULL(PAL0, 1, 0, 0, T_PLAYER));
|
|
560
|
+
for (i = 0; i < NUM_COINS; i++) {
|
|
561
|
+
s16 cx = coin_x[i];
|
|
562
|
+
VDP_setSprite(1 + i, (state == ST_PLAY && cx < SCREEN_W) ? cx : (s16)SCREEN_W,
|
|
563
|
+
(state == ST_PLAY && cx < SCREEN_W) ? coin_y[i] : (s16)HIDE_Y,
|
|
564
|
+
SPRITE_SIZE(1, 1), TILE_ATTR_FULL(PAL0, 1, 0, 0, T_COIN));
|
|
565
|
+
}
|
|
566
|
+
for (i = 0; i < NUM_SPIKES; i++) {
|
|
567
|
+
u8 vis = (state == ST_PLAY) && spike_active[i] && spike_x[i] < SCREEN_W;
|
|
568
|
+
VDP_setSprite(4 + i, spike_x[i], vis ? (s16)SPIKE_Y : (s16)HIDE_Y,
|
|
569
|
+
SPRITE_SIZE(1, 1), TILE_ATTR_FULL(PAL0, 1, 0, 0, T_SPIKE));
|
|
570
|
+
}
|
|
571
|
+
/* ── HARDWARE IDIOM (load-bearing) — CHAIN the sprite list before
|
|
572
|
+
* uploading. VDP_setSprite does NOT set the SAT link byte, and link 0
|
|
573
|
+
* means "end of list": skip this and the VDP draws sprite 0 only.
|
|
574
|
+
* VDP_linkSprites(0, 6) links slots 0..5; the queued DMA flushes the
|
|
575
|
+
* 6 SAT entries during vblank. ── */
|
|
576
|
+
VDP_linkSprites(0, 6);
|
|
577
|
+
VDP_updateSprites(6, DMA_QUEUE);
|
|
70
578
|
}
|
|
71
579
|
|
|
72
580
|
int main(bool hard) {
|
|
581
|
+
u16 i, pad, fresh;
|
|
582
|
+
s16 delta, y, feet, top;
|
|
583
|
+
u16 c0, c1;
|
|
73
584
|
(void)hard;
|
|
74
585
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
VDP_loadTileData(tile_blank, T_BLANK, 1, DMA);
|
|
82
|
-
VDP_loadTileData(tile_platform, T_PLATFORM, 1, DMA);
|
|
83
|
-
VDP_loadTileData(tile_player, T_PLAYER, 1, DMA);
|
|
84
|
-
VDP_loadTileData(tile_bg, T_BG, 1, DMA);
|
|
85
|
-
|
|
86
|
-
/* Plane A: the foreground world — platforms painted in world coords. */
|
|
87
|
-
for (u16 i = 0; i < N_PLATFORMS; i++) {
|
|
88
|
-
const Rect* p = &platforms[i];
|
|
89
|
-
VDP_fillTileMapRect(BG_A,
|
|
90
|
-
TILE_ATTR_FULL(PAL1, 0, 0, 0, T_PLATFORM),
|
|
91
|
-
p->x >> 3, p->y >> 3, (p->w + 7) >> 3, (p->h + 7) >> 3);
|
|
92
|
-
}
|
|
93
|
-
/* Plane B: a sparse parallax backdrop — every 4th cell, upper area. */
|
|
94
|
-
for (u16 cx = 0; cx < 64; cx += 4)
|
|
95
|
-
for (u16 cy = 2; cy < 20; cy += 4)
|
|
96
|
-
VDP_setTileMapXY(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, T_BG), cx, cy);
|
|
586
|
+
/* SRAM first — before any VDP work. The save file then exists within
|
|
587
|
+
* the game's first frames of life, which is what lets a frontend (or
|
|
588
|
+
* a headless host) see a non-empty save_ram region as early as
|
|
589
|
+
* possible (see the SRAM idiom note on gpgx's size scan). */
|
|
590
|
+
hiscore_init();
|
|
97
591
|
|
|
98
|
-
|
|
592
|
+
/* ── HARDWARE IDIOM (load-bearing — see TROUBLESHOOTING) ──
|
|
593
|
+
* Init order: scrolling MODE before scroll VALUES, tiles + palettes
|
|
594
|
+
* before tilemaps that reference them, window size before window text.
|
|
595
|
+
* SGDK's boot already did the dangerous part (VDP regs, Z80, vblank
|
|
596
|
+
* int) — keep VDP_setScrollingMode FIRST here so every later
|
|
597
|
+
* apply_camera() writes the table layout the VDP is actually reading. */
|
|
598
|
+
VDP_setScrollingMode(HSCROLL_TILE, VSCROLL_PLANE);
|
|
599
|
+
hud_init();
|
|
99
600
|
|
|
100
|
-
|
|
601
|
+
/* Palettes: PAL0 sprites + HUD text, PAL1 plane A, PAL2 plane B.
|
|
602
|
+
* Colours are BGR, 3 bits per channel: 0x0BGR with E = full. */
|
|
603
|
+
PAL_setColor( 2, 0x028E); /* player ember orange */
|
|
604
|
+
PAL_setColor( 3, 0x0008); /* player eyes dark red */
|
|
605
|
+
PAL_setColor( 4, 0x02CE); /* coin gold */
|
|
606
|
+
PAL_setColor( 5, 0x008C); /* coin shading */
|
|
607
|
+
PAL_setColor( 6, 0x0888); /* spike steel */
|
|
608
|
+
PAL_setColor( 7, 0x0CCC); /* spike highlight */
|
|
609
|
+
PAL_setColor(15, 0x0EEE); /* font white (index 15 = SGDK font colour) */
|
|
610
|
+
PAL_setColor(16 + 1, 0x04A2); /* grass dusk green */
|
|
611
|
+
PAL_setColor(16 + 2, 0x0248); /* dirt brown */
|
|
612
|
+
PAL_setColor(16 + 3, 0x0136); /* dirt speckle */
|
|
613
|
+
PAL_setColor(16 + 4, 0x0666); /* slab stone */
|
|
614
|
+
PAL_setColor(16 + 5, 0x0AAA); /* slab lip */
|
|
615
|
+
PAL_setColor(32 + 1, 0x036C); /* dusk sky orange */
|
|
616
|
+
PAL_setColor(32 + 2, 0x08AE); /* cloud pink */
|
|
617
|
+
PAL_setColor(32 + 3, 0x0413); /* mountain dark purple */
|
|
618
|
+
PAL_setColor(32 + 4, 0x0635); /* mountain speckle */
|
|
619
|
+
PAL_setColor(32 + 5, 0x0202); /* HUD band near-black */
|
|
101
620
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
621
|
+
VDP_loadTileData(tile_grass, T_GRASS, 1, DMA);
|
|
622
|
+
VDP_loadTileData(tile_dirt, T_DIRT, 1, DMA);
|
|
623
|
+
VDP_loadTileData(tile_slab, T_SLAB, 1, DMA);
|
|
624
|
+
VDP_loadTileData(tile_sky, T_SKY, 1, DMA);
|
|
625
|
+
VDP_loadTileData(tile_cloud, T_CLOUD, 1, DMA);
|
|
626
|
+
VDP_loadTileData(tile_peak, T_PEAK, 1, DMA);
|
|
627
|
+
VDP_loadTileData(tile_mount, T_MOUNT, 1, DMA);
|
|
628
|
+
VDP_loadTileData(tile_hudband, T_HUDBAND, 1, DMA);
|
|
629
|
+
VDP_loadTileData(tile_player, T_PLAYER, 1, DMA);
|
|
630
|
+
VDP_loadTileData(tile_jump, T_JUMP, 1, DMA);
|
|
631
|
+
VDP_loadTileData(tile_coin, T_COIN, 1, DMA);
|
|
632
|
+
VDP_loadTileData(tile_spike, T_SPIKE, 1, DMA);
|
|
105
633
|
|
|
106
|
-
|
|
107
|
-
|
|
634
|
+
paint_backdrop(); /* plane B: painted once, scrolled forever */
|
|
635
|
+
sfx_init(); /* PSG: sfx channels + background melody */
|
|
636
|
+
|
|
637
|
+
state = ST_TITLE;
|
|
638
|
+
cam = 0;
|
|
639
|
+
apply_camera();
|
|
640
|
+
paint_title();
|
|
108
641
|
|
|
109
642
|
while (TRUE) {
|
|
110
|
-
|
|
643
|
+
if (state == ST_TITLE) {
|
|
644
|
+
/* ── GAME LOGIC (clay) — title: A = 1P, B = 2P turns ──
|
|
645
|
+
* The camera drifts so the title sells the parallax: plane B
|
|
646
|
+
* slides at two speeds while the plane-A title text holds
|
|
647
|
+
* still (its strip values stay 0 — only B's get the drift). */
|
|
648
|
+
cam += 1;
|
|
649
|
+
for (i = 0; i < 28; i++) {
|
|
650
|
+
hsA[i] = 0;
|
|
651
|
+
hsB[i] = (i < SKY_STRIPS) ? -(s16)(cam >> 3) : -(s16)(cam >> 1);
|
|
652
|
+
}
|
|
653
|
+
VDP_setHorizontalScrollTile(BG_A, 0, hsA, 28, DMA_QUEUE);
|
|
654
|
+
VDP_setHorizontalScrollTile(BG_B, 0, hsB, 28, DMA_QUEUE);
|
|
655
|
+
stage_sprites();
|
|
656
|
+
pad = JOY_readJoypad(JOY_1);
|
|
657
|
+
fresh = pad & ~prev_pad;
|
|
658
|
+
if (fresh & (BUTTON_A | BUTTON_C | BUTTON_START)) start_game(0);
|
|
659
|
+
else if (fresh & BUTTON_B) start_game(1);
|
|
660
|
+
prev_pad = pad;
|
|
661
|
+
sfx_update();
|
|
662
|
+
SYS_doVBlankProcess();
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
111
665
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
666
|
+
if (state == ST_OVER) {
|
|
667
|
+
/* Results screen; START or A returns to the title. */
|
|
668
|
+
stage_sprites();
|
|
669
|
+
pad = JOY_readJoypad(JOY_1);
|
|
670
|
+
fresh = pad & ~prev_pad;
|
|
671
|
+
if (fresh & (BUTTON_START | BUTTON_A | BUTTON_C)) {
|
|
672
|
+
state = ST_TITLE;
|
|
673
|
+
cam = 0;
|
|
674
|
+
prev_pad = 0xFFFF; /* swallow the held START */
|
|
675
|
+
paint_title();
|
|
676
|
+
} else {
|
|
677
|
+
prev_pad = pad;
|
|
678
|
+
}
|
|
679
|
+
sfx_update();
|
|
680
|
+
SYS_doVBlankProcess();
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/* ── ST_PLAY ──────────────────────────────────────────────────── */
|
|
685
|
+
stage_sprites();
|
|
686
|
+
apply_camera();
|
|
115
687
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
688
|
+
if (turn_pause) { /* freeze gameplay, keep frames */
|
|
689
|
+
--turn_pause; /* honest (sprites/scroll staged) */
|
|
690
|
+
sfx_update();
|
|
691
|
+
SYS_doVBlankProcess();
|
|
692
|
+
continue;
|
|
120
693
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (ipy + 8 <= p->y && npy + 8 >= p->y
|
|
139
|
-
&& ipx + 8 > p->x && ipx < p->x + p->w) {
|
|
140
|
-
py = (p->y - 8) << 4; vy = 0; landed = TRUE;
|
|
141
|
-
sfx_tone(2, 700, 3); break;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
694
|
+
|
|
695
|
+
/* ── GAME LOGIC (clay) from here down ─────────────────────────────
|
|
696
|
+
* Input — the CURRENT player's controller (alternating turns: P2
|
|
697
|
+
* is on controller 2). Past SCROLL_WALL the world scrolls instead
|
|
698
|
+
* of the player (the camera never scrolls back — the classic
|
|
699
|
+
* one-way runner camera). */
|
|
700
|
+
pad = JOY_readJoypad(cur_player ? JOY_2 : JOY_1);
|
|
701
|
+
delta = 0;
|
|
702
|
+
if (pad & BUTTON_RIGHT) {
|
|
703
|
+
if (px < SCROLL_WALL) px += MOVE_SPEED;
|
|
704
|
+
else { cam += MOVE_SPEED; delta = MOVE_SPEED; }
|
|
705
|
+
}
|
|
706
|
+
if ((pad & BUTTON_LEFT) && px > 8) px -= MOVE_SPEED;
|
|
707
|
+
if ((pad & BTN_JUMP) && !(prev_pad & BTN_JUMP) && on_ground) {
|
|
708
|
+
vy_q44 = JUMP_VEL_Q44;
|
|
709
|
+
on_ground = 0;
|
|
710
|
+
sfx_tone(0, 784, 8); /* jump whoop */
|
|
144
711
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
712
|
+
prev_pad = pad;
|
|
713
|
+
|
|
714
|
+
/* World objects drift left as the level scrolls (world-anchored). */
|
|
715
|
+
if (delta) {
|
|
716
|
+
dist_sub += delta;
|
|
717
|
+
if (dist_sub >= 64) { /* distance pay */
|
|
718
|
+
dist_sub -= 64;
|
|
719
|
+
++p_score[cur_player];
|
|
720
|
+
draw_hud();
|
|
721
|
+
}
|
|
722
|
+
for (i = 0; i < NUM_COINS; i++) {
|
|
723
|
+
coin_x[i] -= delta;
|
|
724
|
+
if (coin_x[i] < 8) respawn_coin(i);
|
|
725
|
+
}
|
|
726
|
+
for (i = 0; i < NUM_SPIKES; i++) {
|
|
727
|
+
if (!spike_active[i]) continue;
|
|
728
|
+
spike_x[i] -= delta;
|
|
729
|
+
if (spike_x[i] < 8) spike_active[i] = 0;
|
|
730
|
+
}
|
|
148
731
|
}
|
|
732
|
+
for (i = 0; i < NUM_SPIKES; i++)
|
|
733
|
+
if (!spike_active[i]) try_spawn_spike(i);
|
|
149
734
|
|
|
150
|
-
/*
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
735
|
+
/* Physics: gravity + sub-pixel y. */
|
|
736
|
+
if (vy_q44 < MAX_VY_Q44) vy_q44 += GRAVITY_Q44;
|
|
737
|
+
py_q44 += vy_q44;
|
|
738
|
+
y = (s16)(py_q44 >> 4);
|
|
154
739
|
|
|
155
|
-
/*
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
740
|
+
/* Fell into a pit (below the screen) → lose the turn. */
|
|
741
|
+
if (y >= 224) {
|
|
742
|
+
kill_player();
|
|
743
|
+
sfx_update();
|
|
744
|
+
SYS_doVBlankProcess();
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
160
747
|
|
|
161
|
-
/*
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
748
|
+
/* Landing — probe the two level columns under the player's feet. */
|
|
749
|
+
if (vy_q44 >= 0) {
|
|
750
|
+
feet = y + 8;
|
|
751
|
+
c0 = ((u16)(px + cam) >> 3) & 63;
|
|
752
|
+
c1 = ((u16)(px + cam + 7) >> 3) & 63;
|
|
753
|
+
top = land_top(c0, feet);
|
|
754
|
+
if (top == 0) top = land_top(c1, feet);
|
|
755
|
+
if (top) {
|
|
756
|
+
py_q44 = (u16)(top - 8) << 4;
|
|
757
|
+
vy_q44 = 0;
|
|
758
|
+
if (!on_ground) sfx_tone(1, 196, 4); /* landing thud */
|
|
759
|
+
on_ground = 1;
|
|
760
|
+
} else {
|
|
761
|
+
on_ground = 0; /* walked off */
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/* Coins (collect) + spikes (death). AABB on 8x8 sprites. */
|
|
766
|
+
for (i = 0; i < NUM_COINS; i++) {
|
|
767
|
+
if (coin_x[i] < px + 8 && coin_x[i] + 8 > px &&
|
|
768
|
+
coin_y[i] < y + 8 && coin_y[i] + 8 > y) {
|
|
769
|
+
p_score[cur_player] += 10;
|
|
770
|
+
sfx_tone(0, 1047, 6); /* coin ping */
|
|
771
|
+
draw_hud();
|
|
772
|
+
respawn_coin(i);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
for (i = 0; i < NUM_SPIKES; i++) {
|
|
776
|
+
if (!spike_active[i]) continue;
|
|
777
|
+
if (spike_x[i] < px + 7 && spike_x[i] + 7 > px &&
|
|
778
|
+
SPIKE_Y < y + 7 && SPIKE_Y + 7 > y) {
|
|
779
|
+
kill_player();
|
|
780
|
+
break;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
165
783
|
|
|
166
784
|
sfx_update();
|
|
167
785
|
SYS_doVBlankProcess();
|