romdevtools 0.28.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +53 -43
- package/CHANGELOG.md +91 -0
- package/README.md +3 -3
- package/examples/README.md +7 -7
- package/examples/atari2600/templates/platformer.asm +1225 -332
- package/examples/atari2600/templates/puzzle.asm +1056 -0
- package/examples/atari2600/templates/racing.asm +906 -275
- package/examples/atari2600/templates/shmup.asm +1031 -239
- package/examples/atari2600/templates/sports.asm +1135 -253
- package/examples/atari7800/templates/platformer.c +991 -156
- package/examples/atari7800/templates/puzzle.c +1091 -148
- package/examples/atari7800/templates/racing.c +952 -124
- package/examples/atari7800/templates/shmup.c +812 -134
- package/examples/atari7800/templates/sports.c +820 -184
- package/examples/c64/templates/platformer.c +879 -164
- package/examples/c64/templates/puzzle.c +855 -178
- package/examples/c64/templates/racing.c +873 -97
- package/examples/c64/templates/shmup.c +757 -161
- package/examples/c64/templates/sports.c +755 -100
- package/examples/gb/templates/platformer.c +841 -179
- package/examples/gb/templates/puzzle.c +986 -246
- package/examples/gb/templates/racing.c +754 -174
- package/examples/gb/templates/shmup.c +673 -175
- package/examples/gb/templates/sports.c +790 -159
- package/examples/gba/templates/platformer.c +626 -165
- package/examples/gba/templates/puzzle.c +519 -269
- package/examples/gba/templates/racing.c +511 -206
- package/examples/gba/templates/shmup.c +564 -179
- package/examples/gba/templates/sports.c +454 -174
- package/examples/gbc/templates/platformer.c +944 -180
- package/examples/gbc/templates/puzzle.c +363 -109
- package/examples/gbc/templates/racing.c +884 -180
- package/examples/gbc/templates/shmup.c +821 -185
- package/examples/gbc/templates/sports.c +870 -162
- package/examples/genesis/templates/platformer.c +747 -129
- package/examples/genesis/templates/puzzle.c +694 -261
- package/examples/genesis/templates/racing.c +726 -203
- package/examples/genesis/templates/shmup.c +535 -142
- package/examples/genesis/templates/sports.c +495 -158
- package/examples/gg/templates/platformer.c +880 -215
- package/examples/gg/templates/puzzle.c +875 -216
- package/examples/gg/templates/racing.c +915 -172
- package/examples/gg/templates/shmup.c +714 -191
- package/examples/gg/templates/sports.c +732 -129
- package/examples/lynx/templates/platformer.c +604 -69
- package/examples/lynx/templates/puzzle.c +498 -158
- package/examples/lynx/templates/racing.c +538 -102
- package/examples/lynx/templates/shmup.c +458 -131
- package/examples/lynx/templates/sports.c +496 -72
- package/examples/msx/platformer/main.c +649 -162
- package/examples/msx/puzzle/main.c +742 -240
- package/examples/msx/racing/main.c +669 -178
- package/examples/msx/shmup/main.c +460 -178
- package/examples/msx/sports/main.c +592 -126
- package/examples/nes/templates/platformer.c +589 -171
- package/examples/nes/templates/puzzle.c +563 -242
- package/examples/nes/templates/racing.c +502 -208
- package/examples/nes/templates/shmup.c +339 -145
- package/examples/nes/templates/sports.c +341 -183
- package/examples/pce/platformer/main.c +874 -205
- package/examples/pce/puzzle/main.c +802 -287
- package/examples/pce/racing/main.c +783 -208
- package/examples/pce/shmup/main.c +638 -212
- package/examples/pce/sports/main.c +586 -169
- package/examples/porting-across-platforms/README.md +1 -1
- package/examples/sms/templates/platformer.c +762 -177
- package/examples/sms/templates/puzzle.c +752 -212
- package/examples/sms/templates/racing.c +808 -145
- package/examples/sms/templates/shmup.c +599 -162
- package/examples/sms/templates/sports.c +630 -122
- package/examples/snes/templates/music_demo.c +7 -0
- package/examples/snes/templates/platformer-data.asm +123 -24
- package/examples/snes/templates/platformer-hdr.asm +57 -0
- package/examples/snes/templates/platformer.c +586 -165
- package/examples/snes/templates/puzzle-data.asm +116 -21
- package/examples/snes/templates/puzzle-hdr.asm +57 -0
- package/examples/snes/templates/puzzle.c +614 -235
- package/examples/snes/templates/racing-data.asm +390 -32
- package/examples/snes/templates/racing-hdr.asm +57 -0
- package/examples/snes/templates/racing.c +807 -196
- package/examples/snes/templates/shmup-data.asm +87 -29
- package/examples/snes/templates/shmup-hdr.asm +57 -0
- package/examples/snes/templates/shmup.c +459 -198
- package/examples/snes/templates/sports-data.asm +48 -2
- package/examples/snes/templates/sports-hdr.asm +57 -0
- package/examples/snes/templates/sports.c +414 -163
- package/package.json +12 -12
- package/src/cores/wasm/bluemsx_libretro.js +1 -1
- package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
- package/src/cores/wasm/fceumm_libretro.js +1 -1
- package/src/cores/wasm/fceumm_libretro.wasm +0 -0
- package/src/cores/wasm/gambatte_libretro.js +1 -1
- package/src/cores/wasm/gambatte_libretro.wasm +0 -0
- package/src/cores/wasm/geargrafx_libretro.js +1 -1
- package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
- package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
- package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
- package/src/cores/wasm/handy_libretro.js +1 -1
- package/src/cores/wasm/handy_libretro.wasm +0 -0
- package/src/cores/wasm/mgba_libretro.js +1 -1
- package/src/cores/wasm/mgba_libretro.wasm +0 -0
- package/src/cores/wasm/prosystem_libretro.js +1 -1
- package/src/cores/wasm/prosystem_libretro.wasm +0 -0
- package/src/cores/wasm/snes9x_libretro.js +1 -1
- package/src/cores/wasm/snes9x_libretro.wasm +0 -0
- package/src/cores/wasm/stella2014_libretro.js +1 -1
- package/src/cores/wasm/stella2014_libretro.wasm +0 -0
- package/src/cores/wasm/vice_x64_libretro.js +1 -1
- package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
- package/src/host/LibretroHost.js +84 -8
- package/src/http/tool-registry.js +11 -11
- package/src/mcp/tools/cheats.js +2 -1
- package/src/mcp/tools/frame.js +3 -2
- package/src/mcp/tools/index.js +3 -3
- package/src/mcp/tools/input.js +5 -4
- package/src/mcp/tools/lifecycle.js +6 -4
- package/src/mcp/tools/memory.js +131 -24
- package/src/mcp/tools/platform-docs.js +1 -1
- package/src/mcp/tools/preview-tile.js +6 -2
- package/src/mcp/tools/project.js +1098 -130
- package/src/mcp/tools/record.js +6 -7
- package/src/mcp/tools/rom-id.js +5 -1
- package/src/mcp/tools/run-until.js +12 -4
- package/src/mcp/tools/snippets.js +6 -6
- package/src/mcp/tools/sprite-pipeline.js +14 -2
- package/src/mcp/tools/state.js +2 -1
- package/src/mcp/tools/tile-inspect.js +8 -1
- package/src/mcp/tools/toolchain.js +12 -1
- package/src/mcp/tools/watch-memory.js +53 -10
- package/src/observer/bus.js +73 -0
- package/src/observer/livestream.html +4 -2
- package/src/observer/tool-wrap.js +17 -14
- package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +32 -3
- package/src/platforms/atari7800/MENTAL_MODEL.md +5 -5
- package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
- package/src/platforms/c64/MENTAL_MODEL.md +11 -4
- package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
- package/src/platforms/gb/MENTAL_MODEL.md +3 -3
- package/src/platforms/gb/TROUBLESHOOTING.md +61 -8
- package/src/platforms/gb/lib/c/README.md +10 -11
- package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
- package/src/platforms/gb/lib/c/patch-header.js +13 -3
- package/src/platforms/gba/MENTAL_MODEL.md +4 -4
- package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
- package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
- package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
- package/src/platforms/gbc/MENTAL_MODEL.md +4 -4
- package/src/platforms/gbc/TROUBLESHOOTING.md +4 -4
- package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gbc/lib/c/README.md +10 -11
- package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
- package/src/platforms/gbc/lib/c/patch-header.js +13 -3
- package/src/platforms/genesis/MENTAL_MODEL.md +3 -3
- package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
- package/src/platforms/gg/MENTAL_MODEL.md +4 -4
- package/src/platforms/gg/TROUBLESHOOTING.md +3 -3
- package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gg/lib/c/joypad_read.c +29 -0
- package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
- package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
- package/src/platforms/msx/MENTAL_MODEL.md +5 -5
- package/src/platforms/msx/TROUBLESHOOTING.md +2 -2
- package/src/platforms/msx/lib/c/msx_hw.h +1 -0
- package/src/platforms/msx/lib/c/msx_vdp.c +25 -0
- package/src/platforms/nes/MENTAL_MODEL.md +2 -2
- package/src/platforms/nes/lib/c/nes_runtime.c +149 -34
- package/src/platforms/nes/lib/c/nes_runtime.h +34 -1
- package/src/platforms/pce/MENTAL_MODEL.md +5 -5
- package/src/platforms/pce/TROUBLESHOOTING.md +1 -1
- package/src/platforms/pce/lib/c/pce_hw.h +11 -0
- package/src/platforms/pce/lib/c/pce_video.c +32 -0
- package/src/platforms/sms/MENTAL_MODEL.md +6 -6
- package/src/platforms/snes/MENTAL_MODEL.md +2 -2
- package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
- package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
- package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
- package/src/toolchains/index.js +27 -11
|
@@ -1,109 +1,129 @@
|
|
|
1
|
-
/* ── shmup.c — NES vertical
|
|
1
|
+
/* ── shmup.c — NES vertical shooter (complete example game) ──────────────────
|
|
2
2
|
*
|
|
3
|
-
* A
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* - Enemy wave spawner: one enemy per ~32 frames from the top
|
|
7
|
-
* - Linear bullet/enemy movement + AABB collision (8x8 vs 8x8)
|
|
8
|
-
* - Score (16-bit, packed BCD-ish) in zero page; not yet rendered
|
|
3
|
+
* A COMPLETE, working game — title screen, 1P and 2P co-op modes, lives,
|
|
4
|
+
* score + persistent hi-score (battery SRAM), music + SFX, and the NES's
|
|
5
|
+
* signature sprite-0-hit split (fixed HUD bar over a drifting starfield).
|
|
9
6
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
7
|
+
* THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
|
|
8
|
+
* very different one. The markers tell you what's what:
|
|
9
|
+
* HARDWARE IDIOM (load-bearing) — dodges a documented NES footgun; reshape
|
|
10
|
+
* your gameplay around it (see TROUBLESHOOTING before changing).
|
|
11
|
+
* GAME LOGIC (clay) — enemy patterns, scoring, tuning, art: reshape freely.
|
|
14
12
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* What depends on what:
|
|
14
|
+
* nes_runtime.{h,c} — rendering/input/sound/text/hi-score library.
|
|
15
|
+
* chr-ram-runtime.crt0.s — boot + NMI + iNES header (BATTERY bit feeds
|
|
16
|
+
* hiscore_load/save). Load-bearing; edit with TROUBLESHOOTING open.
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* NES gotchas you'll trip on first.
|
|
18
|
+
* Frame budget (NTSC, 60fps): the whole update (2 ships × 6 bullets × 6
|
|
19
|
+
* enemies AABB ≈ 72 checks worst case) is comfortably inside one frame.
|
|
21
20
|
*/
|
|
22
21
|
|
|
23
22
|
#include "nes_runtime.h"
|
|
24
23
|
|
|
25
|
-
/*
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
/* The title screen renders this — examples({op:'fork'}) stamps your game's
|
|
25
|
+
* name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
|
|
26
|
+
#define GAME_TITLE "NOVA SENTRY"
|
|
27
|
+
|
|
28
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
29
|
+
* Tile art. Each 8x8 tile = 16 bytes: 8 plane-0 rows then 8 plane-1 rows
|
|
30
|
+
* (2bpp — plane0-only pixels use colour 1, both planes = colour 3). */
|
|
28
31
|
static const uint8_t tile_blank[16] = { 0 };
|
|
29
32
|
static const uint8_t tile_ship[16] = {
|
|
30
|
-
0x18, 0x3C, 0x7E, 0xFF, 0xFF, 0x7E, 0x3C, 0x18,
|
|
33
|
+
0x18, 0x3C, 0x7E, 0xFF, 0xFF, 0x7E, 0x3C, 0x18,
|
|
31
34
|
0, 0, 0, 0, 0, 0, 0, 0,
|
|
32
35
|
};
|
|
33
36
|
static const uint8_t tile_bullet[16] = {
|
|
34
|
-
0x00, 0x18, 0x3C, 0x3C, 0x3C, 0x3C, 0x18, 0x00,
|
|
37
|
+
0x00, 0x18, 0x3C, 0x3C, 0x3C, 0x3C, 0x18, 0x00,
|
|
35
38
|
0, 0, 0, 0, 0, 0, 0, 0,
|
|
36
39
|
};
|
|
37
40
|
static const uint8_t tile_enemy[16] = {
|
|
38
|
-
0x81, 0x42, 0x24, 0xFF, 0xFF, 0x24, 0x42, 0x81,
|
|
41
|
+
0x81, 0x42, 0x24, 0xFF, 0xFF, 0x24, 0x42, 0x81,
|
|
39
42
|
0, 0, 0, 0, 0, 0, 0, 0,
|
|
40
43
|
};
|
|
41
|
-
/* BG
|
|
42
|
-
*
|
|
43
|
-
* the BACKGROUND pattern table ($1000), separate from the sprite tiles above
|
|
44
|
-
* (the runtime puts BG at $1000, sprites at $0000).
|
|
45
|
-
*
|
|
46
|
-
* BG_DUST — a faint checkerboard "space dust" that tiles seamlessly; the
|
|
47
|
-
* base layer that covers the whole field so it never reads blank.
|
|
48
|
-
* BG_STAR — three small stars (colour 1) sprinkled over the dust.
|
|
49
|
-
* BG_BRITE — a single bright + star (colour 2) for the rare close star. */
|
|
44
|
+
/* Starfield BG tiles (BACKGROUND pattern table $1000 — separate from the
|
|
45
|
+
* sprite table at $0000; the runtime's PPUCTRL setup makes that split). */
|
|
50
46
|
static const uint8_t tile_dust[16] = {
|
|
51
|
-
0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA,
|
|
47
|
+
0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA,
|
|
52
48
|
0, 0, 0, 0, 0, 0, 0, 0,
|
|
53
49
|
};
|
|
54
50
|
static const uint8_t tile_star[16] = {
|
|
55
|
-
0x00, 0x08, 0x00, 0x42, 0x00, 0x00, 0x20, 0x01,
|
|
51
|
+
0x00, 0x08, 0x00, 0x42, 0x00, 0x00, 0x20, 0x01,
|
|
56
52
|
0, 0, 0, 0, 0, 0, 0, 0,
|
|
57
53
|
};
|
|
58
54
|
static const uint8_t tile_brite[16] = {
|
|
59
|
-
0x00, 0x00, 0x10, 0x38, 0x10, 0x00, 0x00, 0x00,
|
|
60
|
-
0x00, 0x00, 0x10, 0x38, 0x10, 0x00, 0x00, 0x00,
|
|
55
|
+
0x00, 0x00, 0x10, 0x38, 0x10, 0x00, 0x00, 0x00,
|
|
56
|
+
0x00, 0x00, 0x10, 0x38, 0x10, 0x00, 0x00, 0x00,
|
|
61
57
|
};
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
58
|
+
/* A solid tile for the HUD bar — sprite 0 must overlap an OPAQUE BG pixel
|
|
59
|
+
* for the sprite-0 hit to fire (see the split idiom below). */
|
|
60
|
+
static const uint8_t tile_hudbar[16] = {
|
|
61
|
+
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
|
62
|
+
0, 0, 0, 0, 0, 0, 0, 0,
|
|
63
|
+
};
|
|
64
|
+
#define BG_DUST 1
|
|
65
|
+
#define BG_STAR 2
|
|
66
|
+
#define BG_BRITE 3
|
|
67
|
+
#define BG_HUDBAR 4
|
|
65
68
|
|
|
66
|
-
/* ── Palette ─────────────────────────────────────────────────────── */
|
|
67
69
|
static const uint8_t palette[32] = {
|
|
68
|
-
/*
|
|
69
|
-
0x0F, 0x10, 0x20, 0x30,
|
|
70
|
+
/* BG: near-black backdrop, dim white stars; pal 1 = HUD (dark bar) */
|
|
70
71
|
0x0F, 0x10, 0x20, 0x30,
|
|
72
|
+
0x0F, 0x00, 0x10, 0x30,
|
|
71
73
|
0x0F, 0x10, 0x20, 0x30,
|
|
72
74
|
0x0F, 0x10, 0x20, 0x30,
|
|
73
|
-
/*
|
|
74
|
-
0x0F, 0x21, 0x32, 0x30,
|
|
75
|
-
0x0F, 0x37, 0x27, 0x16,
|
|
76
|
-
0x0F, 0x16, 0x06, 0x36,
|
|
75
|
+
/* Sprites: ship1 blue/white, bullets yellow, enemies red, ship2 green */
|
|
76
|
+
0x0F, 0x21, 0x32, 0x30,
|
|
77
|
+
0x0F, 0x37, 0x27, 0x16,
|
|
78
|
+
0x0F, 0x16, 0x06, 0x36,
|
|
77
79
|
0x0F, 0x2A, 0x1A, 0x0A,
|
|
78
80
|
};
|
|
79
81
|
|
|
80
|
-
/* ──
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
83
|
+
* Object pools — fixed slots, no allocation (there is no heap worth having
|
|
84
|
+
* on a 1.79MHz CPU with 2KB of work RAM). */
|
|
85
|
+
#define MAX_BULLETS 6
|
|
86
|
+
#define MAX_ENEMIES 6
|
|
83
87
|
#define TILE_SHIP 1
|
|
84
88
|
#define TILE_BULLET 2
|
|
85
89
|
#define TILE_ENEMY 3
|
|
86
|
-
#define
|
|
90
|
+
#define SHIP1_PAL 0
|
|
91
|
+
#define SHIP2_PAL 3
|
|
87
92
|
#define BULLET_PAL 1
|
|
88
93
|
#define ENEMY_PAL 2
|
|
94
|
+
#define START_LIVES 3
|
|
95
|
+
/* HUD layout (mind the OVERSCAN: most NTSC displays/cores crop the top 8
|
|
96
|
+
* scanlines, so nametable row 0 is invisible — never put text there):
|
|
97
|
+
* row 0 — blank (cropped by overscan)
|
|
98
|
+
* row 1 — HUD text (LV / SC / HI)
|
|
99
|
+
* row 2 — solid bar: the visual divider AND sprite 0's opaque anchor
|
|
100
|
+
* row 3+ — the scrolling playfield */
|
|
101
|
+
#define HUD_ROWS 3
|
|
89
102
|
|
|
90
103
|
static uint8_t bullet_active[MAX_BULLETS];
|
|
91
104
|
static uint8_t bullet_x[MAX_BULLETS];
|
|
92
105
|
static uint8_t bullet_y[MAX_BULLETS];
|
|
93
|
-
|
|
94
106
|
static uint8_t enemy_active[MAX_ENEMIES];
|
|
95
107
|
static uint8_t enemy_x[MAX_ENEMIES];
|
|
96
108
|
static uint8_t enemy_y[MAX_ENEMIES];
|
|
97
109
|
|
|
98
|
-
/*
|
|
99
|
-
static uint8_t ship_x
|
|
100
|
-
static uint8_t
|
|
101
|
-
static uint8_t
|
|
102
|
-
static
|
|
103
|
-
static uint16_t
|
|
104
|
-
static
|
|
110
|
+
/* Players: index 0 = P1, 1 = P2 (only in 2P co-op mode). */
|
|
111
|
+
static uint8_t ship_x[2], ship_y[2], ship_alive[2], fire_cd[2];
|
|
112
|
+
static uint8_t two_player; /* mode chosen on the title screen */
|
|
113
|
+
static uint8_t lives; /* shared pool in co-op (arcade style) */
|
|
114
|
+
static uint16_t score;
|
|
115
|
+
static uint16_t hiscore;
|
|
116
|
+
static uint8_t spawn_timer;
|
|
117
|
+
static uint8_t scroll_x; /* starfield drift (split-scrolled below HUD) */
|
|
118
|
+
static uint16_t rng = 0xACE1;
|
|
119
|
+
|
|
120
|
+
/* Game states — the shell every example shares: title → play → game over. */
|
|
121
|
+
#define ST_TITLE 0
|
|
122
|
+
#define ST_PLAY 1
|
|
123
|
+
#define ST_OVER 2
|
|
124
|
+
static uint8_t state;
|
|
105
125
|
|
|
106
|
-
/*
|
|
126
|
+
/* ── GAME LOGIC (clay) — xorshift16 PRNG (~tens of cycles per call) ── */
|
|
107
127
|
static uint8_t random8(void) {
|
|
108
128
|
uint16_t r = rng;
|
|
109
129
|
r ^= r << 7;
|
|
@@ -113,13 +133,14 @@ static uint8_t random8(void) {
|
|
|
113
133
|
return (uint8_t)r;
|
|
114
134
|
}
|
|
115
135
|
|
|
116
|
-
static void fire_bullet(
|
|
136
|
+
static void fire_bullet(uint8_t p) {
|
|
117
137
|
uint8_t i;
|
|
118
138
|
for (i = 0; i < MAX_BULLETS; i++) {
|
|
119
139
|
if (!bullet_active[i]) {
|
|
120
140
|
bullet_active[i] = 1;
|
|
121
|
-
bullet_x[i] = ship_x;
|
|
122
|
-
bullet_y[i] = ship_y - 4;
|
|
141
|
+
bullet_x[i] = ship_x[p];
|
|
142
|
+
bullet_y[i] = ship_y[p] - 4;
|
|
143
|
+
sound_play_tone(0, 0x100, 6, 4);
|
|
123
144
|
return;
|
|
124
145
|
}
|
|
125
146
|
}
|
|
@@ -130,127 +151,276 @@ static void spawn_enemy(void) {
|
|
|
130
151
|
for (i = 0; i < MAX_ENEMIES; i++) {
|
|
131
152
|
if (!enemy_active[i]) {
|
|
132
153
|
enemy_active[i] = 1;
|
|
133
|
-
enemy_x[i] = 16 + (random8() & 0x7F);
|
|
134
|
-
enemy_y[i] =
|
|
154
|
+
enemy_x[i] = 16 + (random8() & 0x7F);
|
|
155
|
+
enemy_y[i] = HUD_ROWS * 8 + 8; /* spawn below the HUD bar */
|
|
135
156
|
return;
|
|
136
157
|
}
|
|
137
158
|
}
|
|
138
159
|
}
|
|
139
160
|
|
|
140
|
-
/* AABB
|
|
161
|
+
/* AABB, both boxes 8x8. */
|
|
141
162
|
static uint8_t hits(uint8_t ax, uint8_t ay, uint8_t bx, uint8_t by) {
|
|
142
|
-
uint8_t dx
|
|
143
|
-
|
|
144
|
-
dy = (ay > by) ? (ay - by) : (by - ay);
|
|
163
|
+
uint8_t dx = (ax > bx) ? (ax - bx) : (bx - ax);
|
|
164
|
+
uint8_t dy = (ay > by) ? (ay - by) : (by - ay);
|
|
145
165
|
return (dx < 8) && (dy < 8);
|
|
146
166
|
}
|
|
147
167
|
|
|
168
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
169
|
+
* Sprite-0-hit split scroll — THE classic NES technique (the fixed
|
|
170
|
+
* status bar over a scrolling field in countless NES classics). The PPU has ONE scroll for the whole
|
|
171
|
+
* frame; to keep the HUD fixed while the playfield scrolls, you change the
|
|
172
|
+
* scroll MID-FRAME, and sprite 0 is your timing signal:
|
|
173
|
+
*
|
|
174
|
+
* 1. Sprite 0 (the FIRST sprite staged each frame) sits inside the HUD,
|
|
175
|
+
* overlapping an OPAQUE background pixel (our solid HUD bar tile).
|
|
176
|
+
* 2. The NMI commits scroll (0,0) at vblank — the HUD renders unscrolled.
|
|
177
|
+
* 3. After ppu_wait_nmi(), spin on PPUSTATUS bit 6: it sets at the exact
|
|
178
|
+
* pixel where sprite 0's opaque pixel overlaps opaque background.
|
|
179
|
+
* 4. THEN write the playfield scroll to PPUSCROLL — everything below the
|
|
180
|
+
* HUD renders with the new scroll.
|
|
181
|
+
*
|
|
182
|
+
* Requires: sprite 0 staged FIRST (oam_spr call order = OAM order), an
|
|
183
|
+
* opaque BG pixel under it, ppu_scroll(0,0) left as the frame scroll, and
|
|
184
|
+
* this poll running EVERY frame (miss a frame and the field jumps).
|
|
185
|
+
* Mid-frame X-scroll needs only the two PPUSCROLL writes below. (Mid-frame
|
|
186
|
+
* Y needs the 4-write $2006/$2005 dance — see TROUBLESHOOTING before
|
|
187
|
+
* attempting; X covers the HUD-over-scrolling-field pattern.)
|
|
188
|
+
* The spin costs a few scanlines of CPU each frame — budget for it. */
|
|
189
|
+
#define PPUSTATUS_REG (*(volatile uint8_t *)0x2002)
|
|
190
|
+
#define PPUSCROLL_REG (*(volatile uint8_t *)0x2005)
|
|
191
|
+
static void split_after_hud(void) {
|
|
192
|
+
uint8_t timeout = 240;
|
|
193
|
+
/* FOOTGUN: the hit flag from the frame JUST RENDERED stays set all the
|
|
194
|
+
* way through vblank — it only clears at the next pre-render line. We're
|
|
195
|
+
* called right after ppu_wait_nmi() (i.e. inside vblank), so polling for
|
|
196
|
+
* "set" alone can exit INSTANTLY on the stale flag and the PPUSCROLL
|
|
197
|
+
* write lands during vblank — scrolling the WHOLE next frame, HUD
|
|
198
|
+
* included (a subtle shear that looks like HUD drift). The classic fix
|
|
199
|
+
* is the two-phase poll: wait for the stale flag to CLEAR (pre-render),
|
|
200
|
+
* then wait for THIS frame's hit to SET. */
|
|
201
|
+
while (PPUSTATUS_REG & 0x40) {
|
|
202
|
+
if (--timeout == 0) return; /* flag stuck: bail, keep scroll (0,0) */
|
|
203
|
+
}
|
|
204
|
+
timeout = 240;
|
|
205
|
+
while (!(PPUSTATUS_REG & 0x40)) {
|
|
206
|
+
if (--timeout == 0) return; /* rendering off / sprite-0 missing: bail */
|
|
207
|
+
}
|
|
208
|
+
PPUSCROLL_REG = scroll_x; /* playfield X scroll (below the HUD) */
|
|
209
|
+
PPUSCROLL_REG = 0;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* Stage sprite 0 = an 8x8 opaque block over the HUD BAR row (OAM y is
|
|
213
|
+
* scanline-1, so y=16 renders scanlines 17-24 = nametable row 2 = the bar —
|
|
214
|
+
* opaque-on-opaque, so the hit fires INSIDE the bar and the scroll change
|
|
215
|
+
* lands below it, never shearing the text row). Must be the FIRST oam_spr
|
|
216
|
+
* call of the frame (OAM order = call order; the split needs index 0). */
|
|
217
|
+
static void stage_sprite0(void) {
|
|
218
|
+
oam_spr(4, (HUD_ROWS - 1) * 8, TILE_BULLET, 1);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/* ── GAME LOGIC (clay) — HUD text (queued writes; NMI commits next vblank) ── */
|
|
222
|
+
static void draw_hud(void) {
|
|
223
|
+
text_draw_u16(0, 9, 1, score);
|
|
224
|
+
text_draw_u16(0, 22, 1, hiscore);
|
|
225
|
+
tile_set(0, 3, 1, 0x40 + lives); /* lives as a digit */
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
static void draw_hud_labels(void) {
|
|
229
|
+
text_draw(0, 0, 1, "LV");
|
|
230
|
+
text_draw(0, 6, 1, "SC");
|
|
231
|
+
text_draw(0, 16, 1, "HI");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/* ── GAME LOGIC (clay) — the title screen ──────────────────────────────────
|
|
235
|
+
* Painted with the PPU OFF (text_draw_unsafe = raw VRAM writes; the queued
|
|
236
|
+
* variant would deadlock with rendering disabled — see TROUBLESHOOTING). */
|
|
237
|
+
static void paint_title(void) {
|
|
238
|
+
uint8_t r, c;
|
|
239
|
+
ppu_off();
|
|
240
|
+
/* Clear both HUD + field area to the dust backdrop. */
|
|
241
|
+
for (r = 0; r < 30; r++)
|
|
242
|
+
for (c = 0; c < 32; c++)
|
|
243
|
+
vram_unsafe_set((uint16_t)(0x2000 + r * 32 + c), (r == 0 || r == 1) ? 0 : BG_DUST);
|
|
244
|
+
text_draw_unsafe(0x2000 + 8 * 32 + ((32 - sizeof(GAME_TITLE) + 1) / 2), GAME_TITLE);
|
|
245
|
+
text_draw_unsafe(0x2000 + 13 * 32 + 8, "1P START - A");
|
|
246
|
+
text_draw_unsafe(0x2000 + 15 * 32 + 8, "2P CO-OP - B");
|
|
247
|
+
text_draw_unsafe(0x2000 + 20 * 32 + 10, "HI");
|
|
248
|
+
/* hiscore digits painted by hand (queued text needs rendering on) */
|
|
249
|
+
{
|
|
250
|
+
uint16_t v = hiscore;
|
|
251
|
+
uint8_t d[5], i;
|
|
252
|
+
for (i = 0; i < 5; i++) { d[i] = v % 10; v /= 10; }
|
|
253
|
+
for (i = 0; i < 5; i++) vram_unsafe_set((uint16_t)(0x2000 + 20 * 32 + 13 + i), (uint8_t)(0x40 + d[4 - i]));
|
|
254
|
+
}
|
|
255
|
+
ppu_scroll(0, 0);
|
|
256
|
+
oam_clear();
|
|
257
|
+
ppu_on_all();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/* ── GAME LOGIC (clay) — start a run ── */
|
|
261
|
+
static void paint_field(void) {
|
|
262
|
+
uint8_t r, c, tile;
|
|
263
|
+
ppu_off();
|
|
264
|
+
for (c = 0; c < 32; c++) {
|
|
265
|
+
vram_unsafe_set((uint16_t)(0x2000 + 0 * 32 + c), 0); /* row 0: overscan-cropped */
|
|
266
|
+
vram_unsafe_set((uint16_t)(0x2000 + 1 * 32 + c), 0); /* row 1: HUD text (queued draws fill it) */
|
|
267
|
+
vram_unsafe_set((uint16_t)(0x2000 + 2 * 32 + c), BG_HUDBAR); /* row 2: bar = divider + sprite-0 anchor */
|
|
268
|
+
}
|
|
269
|
+
for (r = HUD_ROWS; r < 30; r++) {
|
|
270
|
+
for (c = 0; c < 32; c++) {
|
|
271
|
+
tile = BG_DUST;
|
|
272
|
+
if (((r * 5 + c * 3) % 7) == 0) tile = BG_STAR;
|
|
273
|
+
if (((r * 3 + c * 7) % 23) == 0) tile = BG_BRITE;
|
|
274
|
+
vram_unsafe_set((uint16_t)(0x2000 + r * 32 + c), tile);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
ppu_scroll(0, 0);
|
|
278
|
+
oam_clear();
|
|
279
|
+
ppu_on_all();
|
|
280
|
+
/* Labels go through the queued path once rendering is on. */
|
|
281
|
+
draw_hud_labels();
|
|
282
|
+
draw_hud();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
static void start_game(uint8_t players) {
|
|
286
|
+
uint8_t i;
|
|
287
|
+
two_player = players;
|
|
288
|
+
for (i = 0; i < MAX_BULLETS; i++) bullet_active[i] = 0;
|
|
289
|
+
for (i = 0; i < MAX_ENEMIES; i++) enemy_active[i] = 0;
|
|
290
|
+
ship_x[0] = two_player ? 96 : 120; ship_y[0] = 200; ship_alive[0] = 1; fire_cd[0] = 0;
|
|
291
|
+
ship_x[1] = 144; ship_y[1] = 200; ship_alive[1] = two_player; fire_cd[1] = 0;
|
|
292
|
+
lives = START_LIVES;
|
|
293
|
+
score = 0;
|
|
294
|
+
spawn_timer = 0;
|
|
295
|
+
scroll_x = 0;
|
|
296
|
+
paint_field();
|
|
297
|
+
state = ST_PLAY;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
static void game_over(void) {
|
|
301
|
+
if (score > hiscore) {
|
|
302
|
+
hiscore = score;
|
|
303
|
+
/* ── HARDWARE IDIOM (load-bearing) — persists via battery PRG-RAM at
|
|
304
|
+
* $6000; works because the crt0's iNES header sets the BATTERY bit.
|
|
305
|
+
* See nes_runtime.c for the magic+checksum layout. ── */
|
|
306
|
+
hiscore_save(hiscore);
|
|
307
|
+
}
|
|
308
|
+
state = ST_OVER;
|
|
309
|
+
text_draw(0, 11, 14, "GAME OVER");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/* ── GAME LOGIC (clay) — per-player update ── */
|
|
313
|
+
static void update_ship(uint8_t p) {
|
|
314
|
+
uint8_t pad = pad_poll(p);
|
|
315
|
+
if (!ship_alive[p]) return;
|
|
316
|
+
if ((pad & PAD_LEFT) && ship_x[p] > 8) --ship_x[p];
|
|
317
|
+
if ((pad & PAD_RIGHT) && ship_x[p] < 240) ++ship_x[p];
|
|
318
|
+
if ((pad & PAD_UP) && ship_y[p] > (HUD_ROWS * 8 + 8)) --ship_y[p];
|
|
319
|
+
if ((pad & PAD_DOWN) && ship_y[p] < 216) ++ship_y[p];
|
|
320
|
+
if ((pad & PAD_A) && fire_cd[p] == 0) {
|
|
321
|
+
fire_bullet(p);
|
|
322
|
+
fire_cd[p] = 8;
|
|
323
|
+
}
|
|
324
|
+
if (fire_cd[p] > 0) --fire_cd[p];
|
|
325
|
+
}
|
|
326
|
+
|
|
148
327
|
void main(void) {
|
|
149
328
|
uint8_t i, pad, prev_pad = 0;
|
|
150
329
|
|
|
330
|
+
/* ── HARDWARE IDIOM (load-bearing — see TROUBLESHOOTING) ──
|
|
331
|
+
* Init order: PPU off → CHR upload → palette → nametable (raw writes) →
|
|
332
|
+
* OAM clear → rendering on. CHR/palette/nametable writes REQUIRE the PPU
|
|
333
|
+
* off (raw $2007 traffic during rendering corrupts the address latch
|
|
334
|
+
* mid-frame). The runtime's ppu_off/ppu_on_all pair owns the PPUCTRL/
|
|
335
|
+
* PPUMASK bits — don't poke those registers directly alongside it. */
|
|
151
336
|
ppu_off();
|
|
152
|
-
|
|
153
|
-
/* Upload tile data — blank in slot 0 + 3 sprite tiles in slots 1..3,
|
|
154
|
-
* all in the SPRITE pattern table ($0000). */
|
|
155
337
|
chr_ram_upload(0x0000, tile_blank, 16);
|
|
156
338
|
chr_ram_upload(0x0010, tile_ship, 16);
|
|
157
339
|
chr_ram_upload(0x0020, tile_bullet, 16);
|
|
158
340
|
chr_ram_upload(0x0030, tile_enemy, 16);
|
|
159
|
-
/* Upload the starfield tiles to the BACKGROUND pattern table
|
|
160
|
-
* ($1010/$1020/$1030 = BG slots 1/2/3). */
|
|
161
341
|
chr_ram_upload(0x1010, tile_dust, 16);
|
|
162
342
|
chr_ram_upload(0x1020, tile_star, 16);
|
|
163
343
|
chr_ram_upload(0x1030, tile_brite, 16);
|
|
164
|
-
|
|
344
|
+
chr_ram_upload(0x1040, tile_hudbar, 16);
|
|
345
|
+
font_upload();
|
|
165
346
|
palette_load(palette);
|
|
166
|
-
|
|
167
|
-
/* Paint a full starfield directly into the nametable while the PPU is off
|
|
168
|
-
* (vram_unsafe_set = raw write; tile_set's NMI queue would deadlock
|
|
169
|
-
* before ppu_on). Every one of the 32×30 cells gets the faint "dust" base,
|
|
170
|
-
* with small stars sprinkled every few cells and the odd bright star — a
|
|
171
|
-
* deterministic scatter so the backdrop is unambiguously "space", densely
|
|
172
|
-
* filled rather than flat black. */
|
|
173
|
-
{
|
|
174
|
-
uint16_t r, cc;
|
|
175
|
-
uint8_t tile;
|
|
176
|
-
for (r = 0; r < 30; r++) {
|
|
177
|
-
for (cc = 0; cc < 32; cc++) {
|
|
178
|
-
tile = BG_DUST; /* base dust everywhere */
|
|
179
|
-
if (((r * 5 + cc * 3) % 7) == 0) tile = BG_STAR; /* sprinkle stars */
|
|
180
|
-
if (((r * 3 + cc * 7) % 23) == 0) tile = BG_BRITE; /* rare bright one */
|
|
181
|
-
vram_unsafe_set((uint16_t)(0x2000 + r * 32 + cc), tile);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
oam_clear();
|
|
187
|
-
ppu_on_all();
|
|
188
347
|
sound_init();
|
|
189
348
|
|
|
190
|
-
/*
|
|
191
|
-
|
|
192
|
-
|
|
349
|
+
hiscore = hiscore_load(); /* battery SRAM — 0 on first boot */
|
|
350
|
+
state = ST_TITLE;
|
|
351
|
+
paint_title();
|
|
193
352
|
|
|
194
353
|
for (;;) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
oam_spr(bullet_x[i], bullet_y[i], TILE_BULLET, BULLET_PAL);
|
|
207
|
-
}
|
|
354
|
+
if (state == ST_TITLE) {
|
|
355
|
+
/* ── GAME LOGIC (clay) — title: A = 1P, B = 2P co-op ── */
|
|
356
|
+
oam_clear();
|
|
357
|
+
ppu_wait_nmi();
|
|
358
|
+
sound_music_tick();
|
|
359
|
+
pad = pad_poll(0);
|
|
360
|
+
if ((pad & PAD_A) && !(prev_pad & PAD_A)) start_game(0);
|
|
361
|
+
else if ((pad & PAD_B) && !(prev_pad & PAD_B)) start_game(1);
|
|
362
|
+
else if ((pad & PAD_START) && !(prev_pad & PAD_START)) start_game(0);
|
|
363
|
+
prev_pad = pad;
|
|
364
|
+
continue;
|
|
208
365
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
366
|
+
|
|
367
|
+
if (state == ST_OVER) {
|
|
368
|
+
/* Freeze the final frame; START or A returns to the title. */
|
|
369
|
+
oam_clear();
|
|
370
|
+
stage_sprite0();
|
|
371
|
+
ppu_wait_nmi();
|
|
372
|
+
split_after_hud();
|
|
373
|
+
sound_music_tick();
|
|
374
|
+
pad = pad_poll(0);
|
|
375
|
+
if ((pad & (PAD_START | PAD_A)) && !(prev_pad & (PAD_START | PAD_A))) {
|
|
376
|
+
state = ST_TITLE;
|
|
377
|
+
paint_title();
|
|
212
378
|
}
|
|
379
|
+
prev_pad = pad;
|
|
380
|
+
continue;
|
|
213
381
|
}
|
|
214
382
|
|
|
215
|
-
|
|
383
|
+
/* ── ST_PLAY ─────────────────────────────────────────────────────── */
|
|
384
|
+
|
|
385
|
+
/* ── HARDWARE IDIOM (load-bearing — see TROUBLESHOOTING) ──
|
|
386
|
+
* Stage ALL sprites BEFORE ppu_wait_nmi(). The NMI DMAs shadow OAM →
|
|
387
|
+
* real OAM at the START of vblank, copying whatever shadow OAM holds AT
|
|
388
|
+
* THAT MOMENT. Stage-then-wait; flipping it shows stale/empty sprites.
|
|
389
|
+
* Sprite 0 (the split marker) must be staged FIRST — OAM order is
|
|
390
|
+
* oam_spr call order, and the split idiom needs it at index 0. */
|
|
391
|
+
oam_clear();
|
|
392
|
+
stage_sprite0();
|
|
393
|
+
for (i = 0; i < 2; i++)
|
|
394
|
+
if (ship_alive[i]) oam_spr(ship_x[i], ship_y[i], TILE_SHIP, i ? SHIP2_PAL : SHIP1_PAL);
|
|
395
|
+
for (i = 0; i < MAX_BULLETS; i++)
|
|
396
|
+
if (bullet_active[i]) oam_spr(bullet_x[i], bullet_y[i], TILE_BULLET, BULLET_PAL);
|
|
397
|
+
for (i = 0; i < MAX_ENEMIES; i++)
|
|
398
|
+
if (enemy_active[i]) oam_spr(enemy_x[i], enemy_y[i], TILE_ENEMY, ENEMY_PAL);
|
|
216
399
|
|
|
217
|
-
|
|
400
|
+
ppu_wait_nmi();
|
|
401
|
+
split_after_hud(); /* the sprite-0 split — every frame */
|
|
218
402
|
sound_music_tick();
|
|
219
|
-
pad = pad_poll(0);
|
|
220
|
-
if ((pad & PAD_LEFT) && ship_x > 8) --ship_x;
|
|
221
|
-
if ((pad & PAD_RIGHT) && ship_x < 240) ++ship_x;
|
|
222
|
-
if ((pad & PAD_UP) && ship_y > 16) --ship_y;
|
|
223
|
-
if ((pad & PAD_DOWN) && ship_y < 216) ++ship_y;
|
|
224
|
-
|
|
225
|
-
if ((pad & PAD_A) && fire_cooldown == 0) {
|
|
226
|
-
fire_bullet();
|
|
227
|
-
fire_cooldown = 8; /* 8-frame cooldown */
|
|
228
|
-
sound_play_tone(0, 0x100, 6, 4); /* short pew */
|
|
229
|
-
}
|
|
230
|
-
if (fire_cooldown > 0) --fire_cooldown;
|
|
231
|
-
prev_pad = pad;
|
|
232
403
|
|
|
233
|
-
/* ──
|
|
404
|
+
/* ── GAME LOGIC (clay) from here down ── */
|
|
405
|
+
update_ship(0);
|
|
406
|
+
if (two_player) update_ship(1);
|
|
407
|
+
|
|
408
|
+
/* Starfield drift (the split makes this not move the HUD). */
|
|
409
|
+
if ((spawn_timer & 3) == 0) ++scroll_x;
|
|
410
|
+
|
|
234
411
|
for (i = 0; i < MAX_BULLETS; i++) {
|
|
235
412
|
if (!bullet_active[i]) continue;
|
|
236
|
-
if (bullet_y[i] < 4)
|
|
237
|
-
|
|
238
|
-
} else {
|
|
239
|
-
bullet_y[i] -= 4;
|
|
240
|
-
}
|
|
413
|
+
if (bullet_y[i] < HUD_ROWS * 8 + 4) bullet_active[i] = 0;
|
|
414
|
+
else bullet_y[i] -= 4;
|
|
241
415
|
}
|
|
242
416
|
|
|
243
|
-
/* ── Update enemies (move down, despawn off-screen) ──────── */
|
|
244
417
|
for (i = 0; i < MAX_ENEMIES; i++) {
|
|
245
418
|
if (!enemy_active[i]) continue;
|
|
246
|
-
if (enemy_y[i] >=
|
|
247
|
-
|
|
248
|
-
} else {
|
|
249
|
-
++enemy_y[i];
|
|
250
|
-
}
|
|
419
|
+
if (enemy_y[i] >= 224) enemy_active[i] = 0;
|
|
420
|
+
else ++enemy_y[i];
|
|
251
421
|
}
|
|
252
422
|
|
|
253
|
-
/*
|
|
423
|
+
/* Bullets ↔ enemies. */
|
|
254
424
|
{
|
|
255
425
|
uint8_t b, e;
|
|
256
426
|
for (b = 0; b < MAX_BULLETS; b++) {
|
|
@@ -260,15 +430,39 @@ void main(void) {
|
|
|
260
430
|
if (hits(bullet_x[b], bullet_y[b], enemy_x[e], enemy_y[e])) {
|
|
261
431
|
bullet_active[b] = 0;
|
|
262
432
|
enemy_active[e] = 0;
|
|
263
|
-
++score;
|
|
264
|
-
sound_play_noise(8, 8, 6);
|
|
433
|
+
++score;
|
|
434
|
+
sound_play_noise(8, 8, 6);
|
|
435
|
+
draw_hud();
|
|
265
436
|
break;
|
|
266
437
|
}
|
|
267
438
|
}
|
|
268
439
|
}
|
|
269
440
|
}
|
|
270
441
|
|
|
271
|
-
/*
|
|
442
|
+
/* Enemies ↔ ships: shared life pool (arcade co-op). */
|
|
443
|
+
{
|
|
444
|
+
uint8_t e, p;
|
|
445
|
+
for (e = 0; e < MAX_ENEMIES; e++) {
|
|
446
|
+
if (!enemy_active[e]) continue;
|
|
447
|
+
for (p = 0; p < 2; p++) {
|
|
448
|
+
if (!ship_alive[p]) continue;
|
|
449
|
+
if (hits(enemy_x[e], enemy_y[e], ship_x[p], ship_y[p])) {
|
|
450
|
+
enemy_active[e] = 0;
|
|
451
|
+
sound_play_noise(12, 12, 12);
|
|
452
|
+
if (lives > 0) --lives;
|
|
453
|
+
draw_hud();
|
|
454
|
+
if (lives == 0) {
|
|
455
|
+
game_over();
|
|
456
|
+
} else {
|
|
457
|
+
/* respawn knockback */
|
|
458
|
+
ship_y[p] = 200;
|
|
459
|
+
ship_x[p] = p ? 144 : (two_player ? 96 : 120);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
272
466
|
++spawn_timer;
|
|
273
467
|
if (spawn_timer >= 32) {
|
|
274
468
|
spawn_timer = 0;
|