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,228 +1,836 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
1
|
+
/* ── shmup.c — C64 horizontal shooter (complete example game) ─────────────────
|
|
2
|
+
*
|
|
3
|
+
* A COMPLETE, working game — title screen, 1P and 2P co-op modes, lives,
|
|
4
|
+
* score + hi-score, SID music with the C64's signature filter sweep, SFX,
|
|
5
|
+
* and the C64's signature raster-IRQ split (a fixed score bar over a
|
|
6
|
+
* scrolling starfield).
|
|
7
|
+
*
|
|
8
|
+
* THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
|
|
9
|
+
* very different one. The markers tell you what's what:
|
|
10
|
+
* HARDWARE IDIOM (load-bearing) — dodges a documented C64 footgun; reshape
|
|
11
|
+
* your gameplay around it (see TROUBLESHOOTING before changing).
|
|
12
|
+
* GAME LOGIC (clay) — enemy patterns, scoring, tuning, art: reshape freely.
|
|
13
|
+
*
|
|
14
|
+
* What depends on what:
|
|
15
|
+
* c64_registers.h — VIC-II / SID / CIA symbolic addresses (header only).
|
|
16
|
+
* c64_sfx.{h,c} — one-shot SID sound effects on voice 2.
|
|
17
|
+
* The BASIC stub + crt0 come from cc65's c64 target: the .prg loads at
|
|
18
|
+
* $0801, a tiny BASIC line does SYS into the C runtime, and the KERNAL
|
|
19
|
+
* stays banked in (we lean on that for the IRQ vector — see below).
|
|
20
|
+
*
|
|
21
|
+
* Memory map this file assumes (VIC bank 0 = $0000-$3FFF):
|
|
22
|
+
* $0400 screen RAM (40×25 chars) $D800 color RAM (static texture)
|
|
23
|
+
* $0801 this program (code+data grow up from here)
|
|
24
|
+
* $3F00 sprite images (3 × 64 bytes) — NOT $0800, which collides with
|
|
25
|
+
* the .prg load address, and NOT $1000-$1FFF, where the VIC sees
|
|
26
|
+
* the character ROM instead of RAM (a classic invisible-sprite trap).
|
|
27
|
+
* Keep the program under ~14 KB so it stays below $3F00.
|
|
28
|
+
*
|
|
29
|
+
* Frame budget (PAL, 50fps, ~19656 CPU cycles/frame): the coarse starfield
|
|
30
|
+
* shift (22 rows × 39 bytes, every 8th frame) is the big-ticket item at
|
|
31
|
+
* ~13k cycles; it's scheduled right after the bottom-of-frame IRQ so it
|
|
32
|
+
* outruns the raster beam (see scroll_field_left). Everything else fits.
|
|
33
|
+
*/
|
|
14
34
|
|
|
15
35
|
#include "c64_registers.h"
|
|
16
36
|
#include "c64_sfx.h"
|
|
17
37
|
#include <stdint.h>
|
|
18
38
|
|
|
39
|
+
/* cc65 KERNAL disk-I/O prototypes. We DON'T #include <cbm.h> — it drags in
|
|
40
|
+
* <c64.h>, whose VIC/SID/JOY macros collide with this project's
|
|
41
|
+
* c64_registers.h (cc65 errors "macro redefinition is not identical"). These
|
|
42
|
+
* four are the stable cc65 ABI; declaring them directly avoids the clash. */
|
|
43
|
+
unsigned char __fastcall__ cbm_open(unsigned char lfn, unsigned char device,
|
|
44
|
+
unsigned char sec_addr, const char *name);
|
|
45
|
+
void __fastcall__ cbm_close(unsigned char lfn);
|
|
46
|
+
int __fastcall__ cbm_read(unsigned char lfn, void *buffer, unsigned int size);
|
|
47
|
+
int __fastcall__ cbm_write(unsigned char lfn, const void *buffer, unsigned int size);
|
|
48
|
+
|
|
49
|
+
/* The title screen renders this — examples({op:'fork'}) stamps your game's
|
|
50
|
+
* name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
|
|
51
|
+
#define GAME_TITLE "ION SQUALL"
|
|
52
|
+
|
|
19
53
|
#define POKE(addr, val) (*(volatile uint8_t*)(addr) = (val))
|
|
20
54
|
#define PEEK(addr) (*(volatile uint8_t*)(addr))
|
|
21
55
|
|
|
22
56
|
#define SCREEN ((volatile uint8_t*)0x0400)
|
|
23
57
|
#define COLORS ((volatile uint8_t*)0xD800)
|
|
58
|
+
#define SPRITE_POINTERS ((volatile uint8_t*)0x07F8) /* last 8 bytes of screen RAM */
|
|
24
59
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
60
|
+
/* ── Screen layout (the raster split divides bar from field) ────────────────
|
|
61
|
+
* char row 0 — score bar text: SC / HI / LV / mode (FIXED, never scrolls)
|
|
62
|
+
* char row 1 — solid divider line (FIXED)
|
|
63
|
+
* char row 2 — blank spacer: the split lands mid-row HERE, where a few
|
|
64
|
+
* raster lines of IRQ jitter are invisible (uniform color)
|
|
65
|
+
* char rows 3-24 — the scrolling starfield playfield
|
|
66
|
+
* PAL raster geometry: with YSCROLL=3 (the power-on default) text row r
|
|
67
|
+
* occupies raster lines 51+8r .. 58+8r. So the spacer row 2 = lines 67-74,
|
|
68
|
+
* and the playfield's first row 3 starts at line 75. */
|
|
69
|
+
#define FIELD_TOP 3
|
|
70
|
+
#define SPLIT_LINE 68 /* inside spacer row 2 (67-74): jitter-proof */
|
|
71
|
+
#define BOTTOM_LINE 251 /* first line below the 25-row text window (ends 250) */
|
|
72
|
+
/* $D016 values for the two halves of the frame. Bit 3 CLEAR = 38-column mode
|
|
73
|
+
* (masks the garbage column fine-X scrolling exposes at the edges — keep all
|
|
74
|
+
* text inside columns 1-38). Low 3 bits = fine X scroll 0-7. */
|
|
75
|
+
#define D016_BAR 0xC0 /* fine X = 0, 38 cols — the fixed bar */
|
|
34
76
|
|
|
35
|
-
|
|
36
|
-
#define
|
|
77
|
+
/* ── GAME LOGIC (clay — reshape freely) ── object pools, no heap ── */
|
|
78
|
+
#define MAX_BULLETS 2 /* one VIC sprite each — see the slot map below */
|
|
79
|
+
#define MAX_ENEMIES 4
|
|
80
|
+
#define START_LIVES 3
|
|
37
81
|
|
|
38
|
-
/* 8 hardware sprite slots:
|
|
39
|
-
* 0
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
#define
|
|
43
|
-
#define
|
|
82
|
+
/* 8 VIC-II hardware sprite slots — ALL of them, the C64's full budget:
|
|
83
|
+
* 0 = P1 ship 1 = P2 ship 2-3 = bullets 4-7 = enemies
|
|
84
|
+
* (More objects than 8 needs raster-time sprite multiplexing — a deep
|
|
85
|
+
* rabbit hole; this game designs its gameplay inside the budget instead.) */
|
|
86
|
+
#define SLOT_P1 0
|
|
87
|
+
#define SLOT_P2 1
|
|
88
|
+
#define SLOT_BULLET0 2
|
|
44
89
|
#define SLOT_ENEMY0 4
|
|
45
90
|
|
|
91
|
+
/* Sprite images live at $3F00 (top of VIC bank 0). Pointer byte = addr/64. */
|
|
92
|
+
#define SPR_DATA(img) (0x3F00 + (img) * 64)
|
|
93
|
+
#define SPR_PTR(img) (uint8_t)(SPR_DATA(img) / 64) /* $3F00/64 = $FC */
|
|
94
|
+
#define IMG_SHIP 0
|
|
95
|
+
#define IMG_BULLET 1
|
|
96
|
+
#define IMG_ENEMY 2
|
|
97
|
+
|
|
98
|
+
/* ── GAME LOGIC (clay) — sprite art (24×21, 3 bytes/row, 64-byte blocks) ── */
|
|
46
99
|
static const uint8_t ship_sprite[64] = {
|
|
47
|
-
0x00,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
0x00,0x3C,0x00, 0,
|
|
100
|
+
0x60,0x00,0x00, 0x78,0x00,0x00, 0x7E,0x00,0x00, 0x1F,0x80,0x00,
|
|
101
|
+
0x1F,0xE0,0x00, 0x3F,0xF8,0x00, 0x7F,0xFE,0x00, 0xFF,0xFF,0x80,
|
|
102
|
+
0xFF,0xFF,0xE0, 0xFF,0xFF,0x80, 0x7F,0xFE,0x00, 0x3F,0xF8,0x00,
|
|
103
|
+
0x1F,0xE0,0x00, 0x1F,0x80,0x00, 0x7E,0x00,0x00, 0x78,0x00,0x00,
|
|
104
|
+
0x60,0x00,0x00, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,
|
|
53
105
|
};
|
|
54
106
|
static const uint8_t bullet_sprite[64] = {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
0x00,0x3C,0x00, 0x00,0x3C,0x00, 0x00,0x18,0x00,
|
|
107
|
+
0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0,
|
|
108
|
+
0x3F,0xC0,0x00, 0x7F,0xE0,0x00, 0x3F,0xC0,0x00,
|
|
58
109
|
0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,
|
|
59
110
|
};
|
|
60
111
|
static const uint8_t enemy_sprite[64] = {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
0x18,0x18,0x18, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,
|
|
112
|
+
0x03,0xC0,0x00, 0x0F,0xF0,0x00, 0x3C,0x3C,0x00, 0x73,0xCE,0x00,
|
|
113
|
+
0xE7,0xE7,0x00, 0xFF,0xFF,0x00, 0xE7,0xE7,0x00, 0x73,0xCE,0x00,
|
|
114
|
+
0x3C,0x3C,0x00, 0x0F,0xF0,0x00, 0x03,0xC0,0x00,
|
|
115
|
+
0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,
|
|
66
116
|
};
|
|
67
117
|
|
|
68
|
-
|
|
118
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
119
|
+
* THE RASTER-IRQ SPLIT — the C64's classic "fixed status bar over a moving
|
|
120
|
+
* world" trick (and the gateway drug to all raster effects). The VIC-II has
|
|
121
|
+
* ONE $D016 fine-scroll for the whole frame; to scroll the playfield while
|
|
122
|
+
* the score bar stays put, you change $D016 MID-FRAME, at an exact raster
|
|
123
|
+
* line, from an interrupt. Two IRQs ping-pong per frame:
|
|
124
|
+
*
|
|
125
|
+
* line 68 (inside the blank spacer row): $D016 = playfield scroll
|
|
126
|
+
* → everything drawn below this line scrolls
|
|
127
|
+
* line 251 (just past the text window): $D016 = 0 scroll
|
|
128
|
+
* → next frame's bar rows render fixed; this IRQ is also the
|
|
129
|
+
* game's frame heartbeat (increments frame_count)
|
|
130
|
+
*
|
|
131
|
+
* The handshake, register by register:
|
|
132
|
+
* $D012 raster compare line (low 8 bits)
|
|
133
|
+
* $D011 b7 raster compare bit 8 — MUST be 0 here (both lines < 256).
|
|
134
|
+
* Forgetting this bit is the classic "my IRQ fires on the
|
|
135
|
+
* wrong line / twice" bug when lines ≥ 256 get involved.
|
|
136
|
+
* $D01A b0 raster IRQ enable
|
|
137
|
+
* $D019 IRQ latch. ACK by WRITING THE BITS BACK (write-1-to-clear).
|
|
138
|
+
* THE LOAD-BEARING LINE: skip the ack and the IRQ re-fires the
|
|
139
|
+
* instant it returns, forever — the main loop starves and the
|
|
140
|
+
* machine looks hung.
|
|
141
|
+
* $0314/15 the KERNAL's IRQ indirection. The hardware vector ($FFFE)
|
|
142
|
+
* points into KERNAL ROM, which saves A/X/Y and jumps through
|
|
143
|
+
* $0314 — so with the KERNAL banked in (cc65 default) we just
|
|
144
|
+
* repoint $0314. Exit via jmp $EA81 (KERNAL: restore regs +
|
|
145
|
+
* rti), SKIPPING $EA31's jiffy-clock/keyboard scan. If you
|
|
146
|
+
* ever bank the KERNAL out, you own $FFFE and the register
|
|
147
|
+
* save/restore yourself.
|
|
148
|
+
* $DC0D CIA1 interrupt control. The KERNAL leaves a 60Hz CIA timer
|
|
149
|
+
* IRQ running (the jiffy clock); disable it ($7F = clear all
|
|
150
|
+
* sources) and ack it (read $DC0D) or it shares the IRQ line
|
|
151
|
+
* with the raster and fires our handler at random lines.
|
|
152
|
+
*
|
|
153
|
+
* JITTER: an IRQ only starts after the current instruction finishes, so the
|
|
154
|
+
* handler begins 0-7 cycles late, plus the KERNAL thunk (~35 cycles) — the
|
|
155
|
+
* $D016 write lands one-to-two raster lines after SPLIT_LINE, at an
|
|
156
|
+
* unpredictable X position. We hide that by splitting inside a UNIFORM
|
|
157
|
+
* blank row, where shifting the (invisible) pixels mid-line changes
|
|
158
|
+
* nothing. Splits next to visible detail need cycle-exact stabilization
|
|
159
|
+
* (double-IRQ trick) — don't go there until you need to.
|
|
160
|
+
*
|
|
161
|
+
* The handler is ASSEMBLY-IN-C on purpose: cc65's generated C uses shared
|
|
162
|
+
* zero-page scratch registers, so a C-level IRQ body would corrupt whatever
|
|
163
|
+
* the main loop was computing. These asm lines touch only A + the flags
|
|
164
|
+
* (which the KERNAL thunk already saved). requires: KERNAL banked in,
|
|
165
|
+
* frame_count/field_d016 file-scope NON-static (asm %v needs the symbol). */
|
|
166
|
+
volatile uint8_t frame_count; /* bumped by the bottom IRQ — frame heartbeat */
|
|
167
|
+
volatile uint8_t field_d016; /* playfield $D016 value, precomputed by main */
|
|
168
|
+
|
|
169
|
+
void raster_irq(void) {
|
|
170
|
+
asm("lda $d019"); /* read VIC IRQ latch... */
|
|
171
|
+
asm("sta $d019"); /* ...write it back = ACK (write-1-to-clear).
|
|
172
|
+
* THE line you must not lose (see above). */
|
|
173
|
+
asm("lda $d012"); /* which raster line woke us? (self-correcting
|
|
174
|
+
* dispatch — no phase variable to desync) */
|
|
175
|
+
asm("cmp #150");
|
|
176
|
+
asm("bcs %g", at_bottom); /* ≥150 → we're at BOTTOM_LINE */
|
|
177
|
+
/* — split point (line ~68, inside the blank spacer row) — */
|
|
178
|
+
asm("lda %v", field_d016);
|
|
179
|
+
asm("sta $d016"); /* playfield fine-X from here down */
|
|
180
|
+
asm("lda #251"); /* = BOTTOM_LINE (cc65's asm %b only takes */
|
|
181
|
+
asm("sta $d012"); /* signed bytes, so these are literals — the */
|
|
182
|
+
asm("jmp $ea81"); /* #if below keeps them honest) */
|
|
183
|
+
at_bottom:
|
|
184
|
+
asm("lda #$C0"); /* = D016_BAR */
|
|
185
|
+
asm("sta $d016"); /* bar scroll for the top of the NEXT frame */
|
|
186
|
+
asm("inc %v", frame_count);/* frame heartbeat for the main loop */
|
|
187
|
+
asm("lda #%b", SPLIT_LINE);
|
|
188
|
+
asm("sta $d012"); /* next stop: the split line */
|
|
189
|
+
asm("jmp $ea81"); /* KERNAL: pla/tay/pla/tax/pla/rti */
|
|
190
|
+
}
|
|
191
|
+
#if BOTTOM_LINE != 251 || D016_BAR != 0xC0
|
|
192
|
+
#error raster_irq's asm immediates are out of sync with BOTTOM_LINE / D016_BAR
|
|
193
|
+
#endif
|
|
194
|
+
|
|
195
|
+
static void install_raster_irq(void) {
|
|
196
|
+
asm("sei"); /* no IRQs while we rewire them */
|
|
197
|
+
POKE(CIA1_PRA + 0x0D, 0x7F); /* $DC0D: disable ALL CIA1 IRQ sources
|
|
198
|
+
* (kills the KERNAL jiffy/keyboard IRQ
|
|
199
|
+
* — we read the sticks ourselves) */
|
|
200
|
+
(void)PEEK(CIA1_PRA + 0x0D); /* reading $DC0D acks anything pending */
|
|
201
|
+
POKE(0x0314, (uint8_t)((unsigned)raster_irq & 0xFF));
|
|
202
|
+
POKE(0x0315, (uint8_t)((unsigned)raster_irq >> 8));
|
|
203
|
+
POKE(VIC_CTRL1, 0x1B); /* $D011 = power-on default: screen on,
|
|
204
|
+
* 25 rows, YSCROLL=3, and bit 7 (raster
|
|
205
|
+
* compare bit 8) = 0 — both our lines
|
|
206
|
+
* are < 256 */
|
|
207
|
+
POKE(VIC_RASTER, SPLIT_LINE); /* first stop */
|
|
208
|
+
POKE(VIC_IRQ_ENA, 0x01); /* raster IRQ on */
|
|
209
|
+
POKE(VIC_IRQ, 0xFF); /* clear any stale latch bits */
|
|
210
|
+
asm("cli");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* Wait for the bottom IRQ's heartbeat. Replaces the usual poll-$D012 loop —
|
|
214
|
+
* the IRQ owns the raster now, the main loop just paces itself on it. */
|
|
215
|
+
static void wait_frame(void) {
|
|
216
|
+
uint8_t f = frame_count;
|
|
217
|
+
while (frame_count == f) { }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/* ── HARDWARE IDIOM (load-bearing — see TROUBLESHOOTING) ── reading BOTH
|
|
221
|
+
* joystick ports. CIA1 port A ($DC00) = control port 2, port B ($DC01) =
|
|
222
|
+
* control port 1. Active-low: a pressed switch reads 0, so invert and mask
|
|
223
|
+
* to bits 0-4 (up/down/left/right/fire).
|
|
224
|
+
*
|
|
225
|
+
* THE PORT-1 GOTCHA: $DC01 is ALSO the keyboard row register — the matrix
|
|
226
|
+
* hangs off the same CIA lines. Writing $FF to $DC00 first deselects every
|
|
227
|
+
* keyboard column, so held keys can't pull $DC01 rows low and ghost into
|
|
228
|
+
* the port-1 stick. (The reverse ghost still exists on real hardware:
|
|
229
|
+
* port-2 stick presses pull $DC00 columns low and can fake keypresses /
|
|
230
|
+
* bleed into port 1 while keys are held. That's the real reason "port 2 is
|
|
231
|
+
* the C64 game port" — P1 lives there by convention, and this game puts
|
|
232
|
+
* the SECOND player on port 1.) requires: install_raster_irq already
|
|
233
|
+
* disabled the KERNAL's keyboard scan, so nothing else rewrites $DC00. */
|
|
234
|
+
static uint8_t read_stick_port2(void) { /* player 1 */
|
|
235
|
+
POKE(CIA1_PRA, 0xFF);
|
|
236
|
+
return (uint8_t)(~PEEK(CIA1_PRA) & 0x1F);
|
|
237
|
+
}
|
|
238
|
+
static uint8_t read_stick_port1(void) { /* player 2 */
|
|
239
|
+
POKE(CIA1_PRA, 0xFF);
|
|
240
|
+
return (uint8_t)(~PEEK(CIA1_PRB) & 0x1F);
|
|
241
|
+
}
|
|
242
|
+
#define JOY_UP 0x01
|
|
243
|
+
#define JOY_DOWN 0x02
|
|
244
|
+
#define JOY_LEFT 0x04
|
|
245
|
+
#define JOY_RIGHT 0x08
|
|
246
|
+
#define JOY_FIRE 0x10
|
|
247
|
+
|
|
248
|
+
/* ── HARDWARE IDIOM (load-bearing) — hi-score persistence: DISK SAVE ─────────
|
|
249
|
+
* The C64 has no battery SRAM — the honest save medium is the FLOPPY. A game
|
|
250
|
+
* persists by writing a file to drive 8; VICE commits it into the live 1541
|
|
251
|
+
* disk image (true-drive GCR write-back), so a save survives a power cycle
|
|
252
|
+
* exactly as it did on real hardware. (To capture it headlessly the host does
|
|
253
|
+
* state({op:'exportDisk', path}); re-loading that .d64 restores the save.)
|
|
254
|
+
*
|
|
255
|
+
* REQUIRES THE GAME RUN FROM A DISK: build/package it as a .d64 and load THAT
|
|
256
|
+
* (loadMedia autostarts it). A bare .prg injected straight into RAM has no
|
|
257
|
+
* mounted disk to save to, so the save is a silent no-op — still honest (the
|
|
258
|
+
* hi-score just stays in-session), it simply has nowhere to persist.
|
|
259
|
+
*
|
|
260
|
+
* We keep a 2-byte record in a SEQ file "HI" on drive 8. cbm_open/read/close
|
|
261
|
+
* for load; cbm_save (KERNAL SAVE) for the write — SAVE is the simplest path
|
|
262
|
+
* that VICE's true-drive emulation commits to the image. These are the STABLE
|
|
263
|
+
* SEAM: the game calls hiscore_load at boot and hiscore_save on a new record;
|
|
264
|
+
* reshape the record format freely, just keep the two function signatures. */
|
|
265
|
+
#define SAVE_NAME "@0:HI,S,W" /* @ = replace-if-exists; S=SEQ, W=write */
|
|
266
|
+
#define LOAD_NAME "0:HI,S,R"
|
|
267
|
+
|
|
268
|
+
static uint16_t hiscore_load(void) {
|
|
269
|
+
uint16_t v = 0;
|
|
270
|
+
uint8_t buf[2];
|
|
271
|
+
/* logical file 2, drive 8, secondary 2 (a data channel, not load-addr). */
|
|
272
|
+
if (cbm_open(2, 8, 2, LOAD_NAME) == 0) {
|
|
273
|
+
if (cbm_read(2, buf, 2) == 2) v = (uint16_t)buf[0] | ((uint16_t)buf[1] << 8);
|
|
274
|
+
cbm_close(2);
|
|
275
|
+
}
|
|
276
|
+
return v; /* 0 if the file isn't there yet (first ever boot) */
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
static void hiscore_save(uint16_t v) {
|
|
280
|
+
uint8_t buf[2];
|
|
281
|
+
buf[0] = (uint8_t)(v & 0xFF);
|
|
282
|
+
buf[1] = (uint8_t)(v >> 8);
|
|
283
|
+
if (cbm_open(2, 8, 2, SAVE_NAME) == 0) {
|
|
284
|
+
cbm_write(2, buf, 2);
|
|
285
|
+
cbm_close(2);
|
|
286
|
+
}
|
|
287
|
+
/* If no disk is mounted (ran as a bare .prg), cbm_open fails and this is a
|
|
288
|
+
* silent no-op — the hi-score simply stays in-session. */
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/* ── GAME LOGIC (clay) — SID music: 2 voices + THE filter sweep ─────────────
|
|
292
|
+
* Voice 0 = melody (pulse), voice 1 = bass (sawtooth THROUGH THE FILTER),
|
|
293
|
+
* voice 2 is reserved for sound effects (c64_sfx). Each voice walks a
|
|
294
|
+
* (freq, frames) note table once per frame; end wraps → continuous loop.
|
|
295
|
+
*
|
|
296
|
+
* THE SID FILTER — the C64's sonic signature, and the part most "music
|
|
297
|
+
* drivers ported from other chips" miss. One analog-modeled filter, shared
|
|
298
|
+
* by all voices, four registers:
|
|
299
|
+
* $D415 cutoff low 3 bits $D416 cutoff high 8 bits (11-bit total)
|
|
300
|
+
* $D417 high nibble = resonance 0-15; low 3 bits ROUTE voices into the
|
|
301
|
+
* filter (bit0=voice0, bit1=voice1, bit2=voice2)
|
|
302
|
+
* $D418 bit4=lowpass bit5=bandpass bit6=highpass — AND master volume in
|
|
303
|
+
* bits 0-3. Volume and filter mode share a register: any "set
|
|
304
|
+
* volume" helper that writes plain $0F silently turns the filter
|
|
305
|
+
* OFF (c64_sfx's sfx_init does exactly that, so music_init runs
|
|
306
|
+
* AFTER it and re-asserts the mode bits).
|
|
307
|
+
* FOOTGUN: bit 7 of $D418 is "3OFF" — it MUTES voice 3 entirely.
|
|
308
|
+
* Set it by accident and all your sound effects vanish.
|
|
309
|
+
* The sweep: a triangle LFO walks the cutoff up and down each frame over
|
|
310
|
+
* the resonant lowpass — the bass goes from muffled to snarling and back,
|
|
311
|
+
* the "wah" that screams Commodore. Hear it change: that IS the chip. */
|
|
312
|
+
#define N_A2 0x0F3Cu
|
|
313
|
+
#define N_C3 0x1199u
|
|
314
|
+
#define N_D3 0x13EEu
|
|
315
|
+
#define N_E3 0x1666u
|
|
316
|
+
#define N_F3 0x1798u
|
|
317
|
+
#define N_G3 0x1AE6u
|
|
318
|
+
#define N_A3 0x1E78u
|
|
319
|
+
#define N_B3 0x2253u
|
|
320
|
+
#define N_C4 0x2333u
|
|
321
|
+
#define N_D4 0x27DDu
|
|
322
|
+
#define N_E4 0x2CCCu
|
|
323
|
+
#define N_F4 0x2F30u
|
|
324
|
+
#define N_G4 0x35CCu
|
|
325
|
+
#define N_A4 0x3CF1u
|
|
326
|
+
#define N_B4 0x44A7u
|
|
327
|
+
#define N_C5 0x4666u
|
|
328
|
+
#define N_D5 0x4FBAu
|
|
329
|
+
#define N_E5 0x5998u
|
|
330
|
+
#define N_G5 0x6B99u
|
|
331
|
+
#define N_REST 0u
|
|
332
|
+
#define STEP 9 /* frames per melodic eighth-note (~140 BPM PAL) */
|
|
333
|
+
|
|
334
|
+
typedef struct { uint16_t freq; uint8_t len; } Note;
|
|
335
|
+
|
|
336
|
+
/* The table IS the song — edit these to rescore your fork. Am F C G loop. */
|
|
337
|
+
static const Note melody[] = {
|
|
338
|
+
{ N_A4, STEP*2 }, { N_C5, STEP }, { N_E5, STEP }, { N_C5, STEP*2 }, { N_E5, STEP*2 },
|
|
339
|
+
{ N_F4, STEP*2 }, { N_A4, STEP }, { N_C5, STEP }, { N_A4, STEP*2 }, { N_REST, STEP*2 },
|
|
340
|
+
{ N_C5, STEP*2 }, { N_E5, STEP }, { N_G5, STEP }, { N_E5, STEP*2 }, { N_C5, STEP*2 },
|
|
341
|
+
{ N_G4, STEP*2 }, { N_B4, STEP }, { N_D5, STEP }, { N_B4, STEP*2 }, { N_REST, STEP*2 },
|
|
342
|
+
{ N_A4, STEP }, { N_E4, STEP }, { N_A4, STEP }, { N_C5, STEP }, { N_E5, STEP*2 }, { N_D5, STEP }, { N_C5, STEP },
|
|
343
|
+
{ N_F4, STEP }, { N_C4, STEP }, { N_F4, STEP }, { N_A4, STEP }, { N_C5, STEP*2 }, { N_B4, STEP }, { N_A4, STEP },
|
|
344
|
+
{ N_E4, STEP }, { N_G4, STEP }, { N_C5, STEP }, { N_E5, STEP*2 }, { N_G5, STEP*2 }, { N_E5, STEP },
|
|
345
|
+
{ N_D5, STEP }, { N_B4, STEP }, { N_G4, STEP }, { N_D4, STEP*2 }, { N_B3, STEP*2 }, { N_REST, STEP },
|
|
346
|
+
};
|
|
347
|
+
static const Note bassline[] = {
|
|
348
|
+
/* Octave-pumping bass — the filter sweep chews on this. */
|
|
349
|
+
{ N_A2, STEP*3 }, { N_A3, STEP }, { N_A2, STEP*2 }, { N_A3, STEP*2 },
|
|
350
|
+
{ N_F3, STEP*3 }, { N_C3, STEP }, { N_F3, STEP*2 }, { N_C4, STEP*2 },
|
|
351
|
+
{ N_C3, STEP*3 }, { N_G3, STEP }, { N_C3, STEP*2 }, { N_E3, STEP*2 },
|
|
352
|
+
{ N_G3, STEP*3 }, { N_D3, STEP }, { N_G3, STEP*2 }, { N_B3, STEP*2 },
|
|
353
|
+
};
|
|
354
|
+
#define MELODY_LEN (sizeof(melody) / sizeof(melody[0]))
|
|
355
|
+
#define BASS_LEN (sizeof(bassline) / sizeof(bassline[0]))
|
|
356
|
+
|
|
357
|
+
static uint8_t m_pos[2], m_left[2];
|
|
358
|
+
static uint16_t filter_cut; /* 11-bit cutoff, 0-2047 */
|
|
359
|
+
static uint8_t filter_up;
|
|
360
|
+
|
|
361
|
+
static void music_trigger(uint8_t v, uint16_t freq, uint8_t wave) {
|
|
362
|
+
if (freq == N_REST) {
|
|
363
|
+
POKE(SID_CTRL(v), wave); /* gate off: release tail plays */
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
POKE(SID_FREQ_LO(v), (uint8_t)(freq & 0xFF));
|
|
367
|
+
POKE(SID_FREQ_HI(v), (uint8_t)(freq >> 8));
|
|
368
|
+
POKE(SID_CTRL(v), wave); /* gate OFF then ON — the 6581/8580 */
|
|
369
|
+
POKE(SID_CTRL(v), wave | SID_GATE); /* envelope only retriggers on the
|
|
370
|
+
* 0→1 gate edge */
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
static void music_init(void) {
|
|
374
|
+
/* Melody: pulse at 50% duty, snappy envelope. */
|
|
375
|
+
POKE(SID_PW_LO(0), 0x00); POKE(SID_PW_HI(0), 0x08);
|
|
376
|
+
POKE(SID_AD(0), 0x07); /* attack 0, decay 7 */
|
|
377
|
+
POKE(SID_SR(0), 0x84); /* sustain 8, release 4 */
|
|
378
|
+
/* Bass: sawtooth (harmonically rich — gives the filter teeth to chew). */
|
|
379
|
+
POKE(SID_AD(1), 0x06);
|
|
380
|
+
POKE(SID_SR(1), 0xA5);
|
|
381
|
+
/* Filter: route VOICE 1 ONLY into it (bit 1 of $D417), resonance 13/15. */
|
|
382
|
+
POKE(SID_RES_FILT, 0xD2);
|
|
383
|
+
/* Lowpass mode + master volume 15. NOTE bits shared with volume, and bit
|
|
384
|
+
* 7 (3OFF) stays 0 or voice-2 sound effects go silent — see block doc. */
|
|
385
|
+
POKE(SID_VOL_MODE, 0x1F);
|
|
386
|
+
filter_cut = 0x180; filter_up = 1;
|
|
387
|
+
m_pos[0] = m_pos[1] = 0;
|
|
388
|
+
m_left[0] = m_left[1] = 1; /* triggers both voices on the next update */
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
static void music_update(void) {
|
|
392
|
+
/* Note sequencing, one table per voice. */
|
|
393
|
+
if (--m_left[0] == 0) {
|
|
394
|
+
music_trigger(0, melody[m_pos[0]].freq, SID_PULSE);
|
|
395
|
+
m_left[0] = melody[m_pos[0]].len;
|
|
396
|
+
if (++m_pos[0] >= MELODY_LEN) m_pos[0] = 0;
|
|
397
|
+
}
|
|
398
|
+
if (--m_left[1] == 0) {
|
|
399
|
+
music_trigger(1, bassline[m_pos[1]].freq, SID_SAWTOOTH);
|
|
400
|
+
m_left[1] = bassline[m_pos[1]].len;
|
|
401
|
+
if (++m_pos[1] >= BASS_LEN) m_pos[1] = 0;
|
|
402
|
+
}
|
|
403
|
+
/* THE FILTER SWEEP — triangle LFO on the cutoff, ~10s round trip.
|
|
404
|
+
* 11-bit value split across two registers: low 3 bits in $D415,
|
|
405
|
+
* high 8 in $D416. */
|
|
406
|
+
if (filter_up) { filter_cut += 6; if (filter_cut >= 0x700) filter_up = 0; }
|
|
407
|
+
else { filter_cut -= 6; if (filter_cut <= 0x180) filter_up = 1; }
|
|
408
|
+
POKE(SID_FILTER_LO, (uint8_t)(filter_cut & 0x07));
|
|
409
|
+
POKE(SID_FILTER_HI, (uint8_t)(filter_cut >> 3));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/* ── GAME LOGIC (clay) — screen text. The C64 has NO VRAM port: screen RAM
|
|
413
|
+
* is plain memory, writable any time, mid-frame, no vblank dance (compare
|
|
414
|
+
* the NES's $2007 choreography). The only translation is ASCII → SCREEN
|
|
415
|
+
* CODES (not PETSCII!): A-Z land at 1-26; space through '?' (incl. digits)
|
|
416
|
+
* keep their ASCII values. ── */
|
|
417
|
+
static void draw_text(uint8_t row, uint8_t col, const char *s) {
|
|
418
|
+
uint16_t off = (uint16_t)row * 40 + col;
|
|
419
|
+
uint8_t ch;
|
|
420
|
+
while ((ch = (uint8_t)*s++) != 0) {
|
|
421
|
+
if (ch >= 'A' && ch <= 'Z') ch -= 64; /* A-Z → screen codes 1-26 */
|
|
422
|
+
SCREEN[off] = ch; /* 32-63 map straight through */
|
|
423
|
+
COLORS[off] = COLOR_WHITE;
|
|
424
|
+
++off;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
/* Blank the whole 40-col row, then draw `s` on it — a clean text BAND.
|
|
428
|
+
* Menu/message text sits over the starfield; drawing it raw leaves the
|
|
429
|
+
* surrounding star chars ('.' and reverse-space nebula) crowding the words.
|
|
430
|
+
* A blanked band reads cleanly on screen AND decodes cleanly from screen RAM. */
|
|
431
|
+
static void draw_text_band(uint8_t row, uint8_t col, const char *s) {
|
|
432
|
+
uint8_t c;
|
|
433
|
+
volatile uint8_t *p = SCREEN + (uint16_t)row * 40;
|
|
434
|
+
for (c = 0; c < 40; c++) p[c] = 0x20;
|
|
435
|
+
draw_text(row, col, s);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
static void draw_u16(uint8_t row, uint8_t col, uint16_t v) {
|
|
439
|
+
uint8_t i, d[5];
|
|
440
|
+
uint16_t off = (uint16_t)row * 40 + col;
|
|
441
|
+
for (i = 0; i < 5; i++) { d[i] = v % 10; v /= 10; }
|
|
442
|
+
for (i = 0; i < 5; i++) {
|
|
443
|
+
SCREEN[off + i] = (uint8_t)('0' + d[4 - i]); /* digit screen code = ASCII */
|
|
444
|
+
COLORS[off + i] = COLOR_WHITE;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/* ── GAME LOGIC (clay) — xorshift-style PRNG (cheap, period 255) ── */
|
|
449
|
+
static uint8_t rng_state = 0xB7;
|
|
450
|
+
static uint8_t rand8(void) {
|
|
451
|
+
uint8_t lsb = (uint8_t)(rng_state & 1);
|
|
452
|
+
rng_state >>= 1;
|
|
453
|
+
if (lsb) rng_state ^= 0xB8;
|
|
454
|
+
return rng_state;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/* ── GAME LOGIC (clay) — the starfield ──────────────────────────────────────
|
|
458
|
+
* Two-layer trick for one-layer hardware: screen RAM holds the MOVING chars
|
|
459
|
+
* (stars '.' + nebula blocks), color RAM holds a STATIC color texture. The
|
|
460
|
+
* coarse scroll shifts ONLY screen RAM (color RAM never moves — half the
|
|
461
|
+
* copy cost), so drifting chars pick up each cell's resident color as they
|
|
462
|
+
* pass: free twinkle, deliberately cheap. */
|
|
463
|
+
static uint8_t field_cell(void) {
|
|
464
|
+
uint8_t v = (uint8_t)(rand8() & 0x0F);
|
|
465
|
+
if (v < 5) return 0xA0; /* reverse-space nebula block */
|
|
466
|
+
if (v < 7) return 0x2E; /* '.' star */
|
|
467
|
+
return 0x20; /* empty space */
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/* Refill ONE field row with fresh stars + its color-texture stripe. Used by
|
|
471
|
+
* state transitions to erase a text band (see paint_field's budget note). */
|
|
472
|
+
static void repaint_field_row(uint8_t r) {
|
|
473
|
+
static const uint8_t tex[8] = {
|
|
474
|
+
COLOR_BLUE, COLOR_DARK_GRAY, COLOR_BLUE, COLOR_LIGHT_BLUE,
|
|
475
|
+
COLOR_BLUE, COLOR_DARK_GRAY, COLOR_WHITE, COLOR_MED_GRAY,
|
|
476
|
+
};
|
|
477
|
+
uint8_t c, t = (uint8_t)(r * 3);
|
|
478
|
+
volatile uint8_t *srow = SCREEN + (uint16_t)r * 40;
|
|
479
|
+
volatile uint8_t *crow = COLORS + (uint16_t)r * 40;
|
|
480
|
+
for (c = 0; c < 40; c++) {
|
|
481
|
+
srow[c] = field_cell();
|
|
482
|
+
crow[c] = tex[(uint8_t)(c + t) & 7];
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/* BUDGET NOTE — this full repaint runs ONCE, at boot. 880 cells of cc65-
|
|
487
|
+
* generated C (function calls per cell) costs ~50 frames: a WHOLE SECOND of
|
|
488
|
+
* frozen music and ignored input if you call it on every state change (this
|
|
489
|
+
* game's original sin — the title screen ate joystick presses for ~1s).
|
|
490
|
+
* Transitions instead repaint only the rows they wrote text on
|
|
491
|
+
* (repaint_field_row), which keeps every transition inside a few frames. */
|
|
492
|
+
static void paint_field(void) {
|
|
493
|
+
uint8_t r;
|
|
494
|
+
for (r = FIELD_TOP; r < 25; r++) repaint_field_row(r);
|
|
495
|
+
}
|
|
69
496
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
497
|
+
/* Coarse scroll: shift the playfield one char left, spawn a fresh column at
|
|
498
|
+
* the right edge. Runs on the frame the fine offset wraps (every 8th).
|
|
499
|
+
* SCHEDULING IS THE TRICK: called immediately after wait_frame(), i.e. just
|
|
500
|
+
* after the line-251 IRQ. The beam won't draw playfield row 3 until line 75
|
|
501
|
+
* of the NEXT frame (~8500 cycles away) and then takes 504 cycles per row;
|
|
502
|
+
* this loop spends ~600 cycles per row, so with that head start it stays
|
|
503
|
+
* ahead of the beam the whole way down — no tearing, no double buffer.
|
|
504
|
+
* (The grown-up alternative is page-flipping screen RAM via $D018.) */
|
|
505
|
+
static void scroll_field_left(void) {
|
|
506
|
+
uint8_t r, c;
|
|
507
|
+
volatile uint8_t *row = SCREEN + FIELD_TOP * 40;
|
|
508
|
+
for (r = FIELD_TOP; r < 25; r++) {
|
|
509
|
+
for (c = 0; c < 39; c++) row[c] = row[c + 1];
|
|
510
|
+
row[39] = field_cell();
|
|
511
|
+
row += 40;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/* ── GAME LOGIC (clay) — game state ── */
|
|
516
|
+
#define ST_TITLE 0
|
|
517
|
+
#define ST_PLAY 1
|
|
518
|
+
#define ST_OVER 2
|
|
519
|
+
static uint8_t state;
|
|
520
|
+
static uint8_t two_player;
|
|
521
|
+
static uint8_t lives;
|
|
522
|
+
static uint16_t score, hiscore;
|
|
523
|
+
static uint8_t cam; /* starfield scroll counter (low 3 bits = fine) */
|
|
524
|
+
|
|
525
|
+
static int16_t ship_x[2]; static uint8_t ship_y[2], ship_alive[2], ship_inv[2], fire_cd[2];
|
|
526
|
+
static int16_t bullet_x[MAX_BULLETS]; static uint8_t bullet_y[MAX_BULLETS], bullet_on[MAX_BULLETS];
|
|
527
|
+
static int16_t enemy_x[MAX_ENEMIES]; static uint8_t enemy_y[MAX_ENEMIES], enemy_on[MAX_ENEMIES];
|
|
73
528
|
static uint8_t spawn_timer;
|
|
74
|
-
static uint8_t prev_pad;
|
|
75
|
-
static uint16_t score;
|
|
76
529
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
530
|
+
/* Sprite coordinate limits (sprite coords: visible X 24-343, Y 50-249).
|
|
531
|
+
* The playfield starts at raster line 75 → keep ships/enemies below the bar. */
|
|
532
|
+
#define Y_MIN 78
|
|
533
|
+
#define Y_MAX 225
|
|
534
|
+
|
|
535
|
+
/* ── HARDWARE IDIOM (load-bearing) — staging sprites with the 9th X bit.
|
|
536
|
+
* VIC sprite X is 9 bits: low 8 in $D000+2n, bit 8 for ALL sprites packed
|
|
537
|
+
* into $D010. Forget $D010 and anything past X=255 wraps back to the left
|
|
538
|
+
* edge — the classic "my sprite teleports at two-thirds screen" bug. We
|
|
539
|
+
* accumulate the MSB bits while staging and commit the byte once. ── */
|
|
540
|
+
static uint8_t spr_msb, spr_ena;
|
|
541
|
+
static void stage_begin(void) { spr_msb = 0; spr_ena = 0; }
|
|
542
|
+
static void stage_sprite(uint8_t slot, int16_t x, uint8_t y) {
|
|
543
|
+
POKE(VIC_SPRITE_X(slot), (uint8_t)(x & 0xFF));
|
|
544
|
+
POKE(VIC_SPRITE_Y(slot), y);
|
|
545
|
+
if (x > 255) spr_msb |= (uint8_t)(1 << slot);
|
|
546
|
+
spr_ena |= (uint8_t)(1 << slot);
|
|
547
|
+
}
|
|
548
|
+
static void stage_commit(void) {
|
|
549
|
+
POKE(VIC_SPRITES_X8, spr_msb);
|
|
550
|
+
POKE(VIC_SPR_ENA, spr_ena); /* unstaged slots vanish — no stale sprites */
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/* ── GAME LOGIC (clay) — score bar (rows 0-1) ── */
|
|
554
|
+
static void draw_bar_labels(void) {
|
|
555
|
+
uint8_t c;
|
|
556
|
+
for (c = 0; c < 40; c++) { /* row 1: solid divider line */
|
|
557
|
+
SCREEN[40 + c] = 0xA0;
|
|
558
|
+
COLORS[40 + c] = COLOR_DARK_GRAY;
|
|
559
|
+
SCREEN[80 + c] = 0x20; /* row 2: the blank spacer the
|
|
560
|
+
* raster split hides in */
|
|
561
|
+
SCREEN[c] = 0x20;
|
|
562
|
+
}
|
|
563
|
+
draw_text(0, 1, "SC");
|
|
564
|
+
draw_text(0, 11, "HI");
|
|
565
|
+
draw_text(0, 21, "LV");
|
|
566
|
+
draw_text(0, 27, two_player ? "2P CO-OP" : "1P ");
|
|
567
|
+
}
|
|
568
|
+
static void draw_bar_stats(void) {
|
|
569
|
+
draw_u16(0, 4, score);
|
|
570
|
+
draw_u16(0, 14, hiscore);
|
|
571
|
+
SCREEN[24] = (uint8_t)('0' + lives);
|
|
572
|
+
COLORS[24] = COLOR_WHITE;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/* ── GAME LOGIC (clay) — title / game start / game over ──────────────────────
|
|
576
|
+
* Transition rule (see paint_field's budget note): never repaint the whole
|
|
577
|
+
* field here. The title draws its text on blanked BANDS over whatever
|
|
578
|
+
* starfield is already there; start_game erases exactly those bands back to
|
|
579
|
+
* stars. Every transition stays a few frames — music never hiccups, and a
|
|
580
|
+
* fire press is acted on (visibly) by the next frame or two. */
|
|
581
|
+
static void paint_title(void) {
|
|
582
|
+
draw_bar_labels();
|
|
583
|
+
draw_bar_stats();
|
|
584
|
+
draw_text_band(7, (40 - (sizeof(GAME_TITLE) - 1)) / 2, GAME_TITLE);
|
|
585
|
+
draw_text_band(11, 12, "PORT 2 FIRE - 1P");
|
|
586
|
+
draw_text_band(13, 9, "PORT 1 FIRE - 2P CO-OP");
|
|
587
|
+
draw_text_band(17, 16, "HI");
|
|
588
|
+
draw_u16(17, 19, hiscore);
|
|
589
|
+
field_d016 = D016_BAR; /* title field holds still (text lives in it) */
|
|
590
|
+
POKE(VIC_SPR_ENA, 0);
|
|
591
|
+
state = ST_TITLE;
|
|
80
592
|
}
|
|
81
593
|
|
|
82
|
-
|
|
594
|
+
/* The four rows paint_title wrote text bands on (game_over's two are a
|
|
595
|
+
* subset) — start_game turns them back into starfield. */
|
|
596
|
+
static void erase_text_bands(void) {
|
|
597
|
+
repaint_field_row(7);
|
|
598
|
+
repaint_field_row(11);
|
|
599
|
+
repaint_field_row(13);
|
|
600
|
+
repaint_field_row(17);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
static void start_game(uint8_t players) {
|
|
83
604
|
uint8_t i;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
605
|
+
two_player = players;
|
|
606
|
+
for (i = 0; i < MAX_BULLETS; i++) bullet_on[i] = 0;
|
|
607
|
+
for (i = 0; i < MAX_ENEMIES; i++) enemy_on[i] = 0;
|
|
608
|
+
ship_x[0] = 50; ship_y[0] = two_player ? 110 : 150;
|
|
609
|
+
ship_x[1] = 50; ship_y[1] = 190;
|
|
610
|
+
ship_alive[0] = 1; ship_alive[1] = players;
|
|
611
|
+
ship_inv[0] = ship_inv[1] = 0;
|
|
612
|
+
fire_cd[0] = fire_cd[1] = 0;
|
|
613
|
+
lives = START_LIVES;
|
|
614
|
+
score = 0;
|
|
615
|
+
spawn_timer = 0;
|
|
616
|
+
cam = 0;
|
|
617
|
+
erase_text_bands(); /* NOT paint_field — see its budget note */
|
|
618
|
+
draw_bar_labels();
|
|
619
|
+
draw_bar_stats();
|
|
620
|
+
state = ST_PLAY;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
static void game_over(void) {
|
|
624
|
+
/* Sprites off FIRST — this runs mid-frame (right after the bottom IRQ),
|
|
625
|
+
* and the beam redraws the screen while we're still writing the text
|
|
626
|
+
* bands below. Killing $D015 before any visible change means the one
|
|
627
|
+
* transition frame never shows sprites parked on top of the message. */
|
|
628
|
+
POKE(VIC_SPR_ENA, 0);
|
|
629
|
+
field_d016 = D016_BAR; /* freeze the field under the message */
|
|
630
|
+
if (score > hiscore) {
|
|
631
|
+
hiscore = score;
|
|
632
|
+
hiscore_save(hiscore); /* the persistence seam — see its block doc */
|
|
633
|
+
draw_bar_stats();
|
|
634
|
+
}
|
|
635
|
+
draw_text_band(11, 15, "GAME OVER");
|
|
636
|
+
draw_text_band(13, 13, "FIRE - TITLE");
|
|
637
|
+
sfx_noise(24);
|
|
638
|
+
state = ST_OVER;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/* ── GAME LOGIC (clay) — combat ── */
|
|
642
|
+
static void fire_bullet(uint8_t p) {
|
|
643
|
+
/* 1P mode: P1 owns both bullet slots. 2P: slot per player. */
|
|
644
|
+
uint8_t i = two_player ? p : 0;
|
|
645
|
+
uint8_t end = two_player ? (uint8_t)(p + 1) : MAX_BULLETS;
|
|
646
|
+
for (; i < end; i++) {
|
|
647
|
+
if (!bullet_on[i]) {
|
|
648
|
+
bullet_on[i] = 1;
|
|
649
|
+
bullet_x[i] = ship_x[p] + 20;
|
|
650
|
+
bullet_y[i] = ship_y[p];
|
|
651
|
+
sfx_tone(2, 0x60, 0x28, 4); /* pew — voice 2 (music owns 0+1) */
|
|
89
652
|
return;
|
|
90
653
|
}
|
|
91
654
|
}
|
|
92
655
|
}
|
|
93
656
|
|
|
94
|
-
static void
|
|
657
|
+
static void spawn_enemy(void) {
|
|
95
658
|
uint8_t i;
|
|
96
659
|
for (i = 0; i < MAX_ENEMIES; i++) {
|
|
97
|
-
if (!
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
660
|
+
if (!enemy_on[i]) {
|
|
661
|
+
enemy_on[i] = 1;
|
|
662
|
+
enemy_x[i] = 348; /* just off-screen right */
|
|
663
|
+
enemy_y[i] = (uint8_t)(Y_MIN + (rand8() % (Y_MAX - Y_MIN)));
|
|
101
664
|
return;
|
|
102
665
|
}
|
|
103
666
|
}
|
|
104
667
|
}
|
|
105
668
|
|
|
106
|
-
static
|
|
107
|
-
|
|
108
|
-
|
|
669
|
+
static uint8_t hits(int16_t ax, uint8_t ay, int16_t bx, uint8_t by) {
|
|
670
|
+
int16_t dx = ax - bx;
|
|
671
|
+
int8_t dy = (int8_t)(ay - by);
|
|
672
|
+
if (dx < 0) dx = -dx;
|
|
673
|
+
if (dy < 0) dy = -dy;
|
|
674
|
+
return (dx < 20) && (dy < 14);
|
|
109
675
|
}
|
|
110
676
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
for (i = 0; i < 1000; i++) {
|
|
121
|
-
SCREEN[i] = 0xA0; /* solid block fills the cell */
|
|
122
|
-
COLORS[i] = ((i ^ (i >> 5)) & 1) ? 0x06 : 0x0B; /* blue / dark grey */
|
|
123
|
-
}
|
|
124
|
-
/* Scatter stars on a coarse lattice so ~1 in 12 cells twinkles. */
|
|
125
|
-
for (r = 1; r < 25; r += 3) {
|
|
126
|
-
for (c = (uint8_t)(r * 5u % 7u); c < 40; c += 7) {
|
|
127
|
-
SCREEN[r * 40 + c] = 0x2E; /* '.' star glyph */
|
|
128
|
-
COLORS[r * 40 + c] = ((r + c) & 1) ? 0x01 : 0x0F; /* white / l.grey */
|
|
129
|
-
}
|
|
130
|
-
}
|
|
677
|
+
static void update_ship(uint8_t p, uint8_t pad) {
|
|
678
|
+
if (!ship_alive[p]) return;
|
|
679
|
+
if (ship_inv[p]) --ship_inv[p];
|
|
680
|
+
if ((pad & JOY_LEFT) && ship_x[p] > 26) ship_x[p] -= 2;
|
|
681
|
+
if ((pad & JOY_RIGHT) && ship_x[p] < 300) ship_x[p] += 2;
|
|
682
|
+
if ((pad & JOY_UP) && ship_y[p] > Y_MIN) ship_y[p] -= 2;
|
|
683
|
+
if ((pad & JOY_DOWN) && ship_y[p] < Y_MAX) ship_y[p] += 2;
|
|
684
|
+
if ((pad & JOY_FIRE) && fire_cd[p] == 0) { fire_bullet(p); fire_cd[p] = 10; }
|
|
685
|
+
if (fire_cd[p]) --fire_cd[p];
|
|
131
686
|
}
|
|
132
687
|
|
|
133
|
-
static void
|
|
688
|
+
static void copy_sprite_image(uint8_t img, const uint8_t *src) {
|
|
134
689
|
uint8_t i;
|
|
135
|
-
volatile uint8_t *dst = (volatile uint8_t*)(
|
|
136
|
-
for (i = 0; i < 64; i++) dst[i] =
|
|
690
|
+
volatile uint8_t *dst = (volatile uint8_t*)SPR_DATA(img);
|
|
691
|
+
for (i = 0; i < 64; i++) dst[i] = src[i];
|
|
137
692
|
}
|
|
138
693
|
|
|
139
694
|
void main(void) {
|
|
140
|
-
uint8_t i,
|
|
695
|
+
uint8_t i, p, pad0, pad1, prev0 = 0, prev1 = 0;
|
|
141
696
|
|
|
142
|
-
|
|
697
|
+
/* ── HARDWARE IDIOM (load-bearing) — boot order. VIC + SID config before
|
|
698
|
+
* the IRQ goes live; sfx_init BEFORE music_init (sfx_init writes a plain
|
|
699
|
+
* volume to $D418, music_init re-asserts the filter-mode bits on top). ── */
|
|
700
|
+
POKE(VIC_SPR_ENA, 0);
|
|
701
|
+
POKE(VIC_BORDER, COLOR_BLACK);
|
|
702
|
+
POKE(VIC_BG0, COLOR_BLACK);
|
|
703
|
+
POKE(VIC_CTRL2, D016_BAR); /* 38-col mode from the start */
|
|
704
|
+
copy_sprite_image(IMG_SHIP, ship_sprite);
|
|
705
|
+
copy_sprite_image(IMG_BULLET, bullet_sprite);
|
|
706
|
+
copy_sprite_image(IMG_ENEMY, enemy_sprite);
|
|
707
|
+
SPRITE_POINTERS[SLOT_P1] = SPR_PTR(IMG_SHIP);
|
|
708
|
+
SPRITE_POINTERS[SLOT_P2] = SPR_PTR(IMG_SHIP);
|
|
709
|
+
for (i = 0; i < MAX_BULLETS; i++) SPRITE_POINTERS[SLOT_BULLET0 + i] = SPR_PTR(IMG_BULLET);
|
|
710
|
+
for (i = 0; i < MAX_ENEMIES; i++) SPRITE_POINTERS[SLOT_ENEMY0 + i] = SPR_PTR(IMG_ENEMY);
|
|
711
|
+
POKE(VIC_SPR_COL(SLOT_P1), COLOR_CYAN);
|
|
712
|
+
POKE(VIC_SPR_COL(SLOT_P2), COLOR_YELLOW);
|
|
713
|
+
for (i = 0; i < MAX_BULLETS; i++) POKE(VIC_SPR_COL(SLOT_BULLET0 + i), COLOR_WHITE);
|
|
714
|
+
for (i = 0; i < MAX_ENEMIES; i++) POKE(VIC_SPR_COL(SLOT_ENEMY0 + i), COLOR_LIGHT_RED);
|
|
715
|
+
POKE(CIA1_DDRA, 0xFF); /* port A drives keyboard columns */
|
|
716
|
+
POKE(CIA1_DDRB, 0x00); /* port B reads rows / stick 1 */
|
|
143
717
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
718
|
+
sfx_init();
|
|
719
|
+
music_init();
|
|
720
|
+
hiscore = hiscore_load(); /* 0 until the core save round lands */
|
|
147
721
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
for (i = 0; i < MAX_ENEMIES; i++) SPRITE_POINTERS[SLOT_ENEMY0 + i] = 0x82;
|
|
722
|
+
field_d016 = D016_BAR;
|
|
723
|
+
paint_field(); /* the ONE full-field paint (boot) */
|
|
724
|
+
install_raster_irq(); /* the split + heartbeat go live */
|
|
725
|
+
paint_title();
|
|
153
726
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
for (i = 0; i < MAX_ENEMIES; i++) POKE(VIC_SPR_COL(SLOT_ENEMY0 + i), 0x02); /* red */
|
|
727
|
+
for (;;) {
|
|
728
|
+
wait_frame(); /* the line-251 IRQ paces everything */
|
|
157
729
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
730
|
+
/* Scroll bookkeeping FIRST: field_d016 must be settled long before the
|
|
731
|
+
* beam reaches SPLIT_LINE, and the coarse shift needs its head start on
|
|
732
|
+
* the beam (see scroll_field_left). */
|
|
733
|
+
if (state == ST_PLAY) {
|
|
734
|
+
++cam;
|
|
735
|
+
field_d016 = (uint8_t)(D016_BAR | (7 - (cam & 7)));
|
|
736
|
+
if ((cam & 7) == 0) scroll_field_left();
|
|
737
|
+
}
|
|
161
738
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
spawn_timer = 0;
|
|
167
|
-
prev_pad = 0;
|
|
168
|
-
sfx_init();
|
|
739
|
+
music_update();
|
|
740
|
+
sfx_update();
|
|
741
|
+
pad0 = read_stick_port2(); /* P1 — control port 2 (convention) */
|
|
742
|
+
pad1 = read_stick_port1(); /* P2 — control port 1 */
|
|
169
743
|
|
|
170
|
-
|
|
171
|
-
|
|
744
|
+
if (state == ST_TITLE) {
|
|
745
|
+
/* Mode select doubles as a controls demo: the stick that presses
|
|
746
|
+
* FIRE picks the mode — port 2 starts 1P, port 1 starts 2P co-op. */
|
|
747
|
+
if ((pad0 & JOY_FIRE) && !(prev0 & JOY_FIRE)) start_game(0);
|
|
748
|
+
else if ((pad1 & JOY_FIRE) && !(prev1 & JOY_FIRE)) start_game(1);
|
|
749
|
+
prev0 = pad0; prev1 = pad1;
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
172
752
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
753
|
+
if (state == ST_OVER) {
|
|
754
|
+
if (((pad0 & JOY_FIRE) && !(prev0 & JOY_FIRE)) ||
|
|
755
|
+
((pad1 & JOY_FIRE) && !(prev1 & JOY_FIRE))) paint_title();
|
|
756
|
+
prev0 = pad0; prev1 = pad1;
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
177
759
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if (
|
|
181
|
-
|
|
182
|
-
if ((pad & JOY_FIRE) && !(prev_pad & JOY_FIRE)) { fire(); sfx_tone(0, 0x80, 0x20, 4); }
|
|
183
|
-
prev_pad = pad;
|
|
760
|
+
/* ── ST_PLAY — GAME LOGIC (clay) from here down ─────────────────── */
|
|
761
|
+
update_ship(0, pad0);
|
|
762
|
+
if (two_player) update_ship(1, pad1);
|
|
763
|
+
prev0 = pad0; prev1 = pad1;
|
|
184
764
|
|
|
185
765
|
for (i = 0; i < MAX_BULLETS; i++) {
|
|
186
|
-
if (!
|
|
187
|
-
|
|
188
|
-
|
|
766
|
+
if (!bullet_on[i]) continue;
|
|
767
|
+
bullet_x[i] += 6;
|
|
768
|
+
if (bullet_x[i] > 344) bullet_on[i] = 0;
|
|
189
769
|
}
|
|
770
|
+
|
|
190
771
|
for (i = 0; i < MAX_ENEMIES; i++) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
772
|
+
uint8_t ty;
|
|
773
|
+
if (!enemy_on[i]) continue;
|
|
774
|
+
enemy_x[i] -= 1 + (score >= 300) + (score >= 800); /* speed up w/ score */
|
|
775
|
+
/* Seek the (alive) P1's altitude every other frame — pressure that
|
|
776
|
+
* also guarantees collisions actually happen. */
|
|
777
|
+
ty = ship_alive[0] ? ship_y[0] : ship_y[1];
|
|
778
|
+
if (frame_count & 1) {
|
|
779
|
+
if (enemy_y[i] < ty) ++enemy_y[i];
|
|
780
|
+
else if (enemy_y[i] > ty) --enemy_y[i];
|
|
781
|
+
}
|
|
782
|
+
if (enemy_x[i] < 4) enemy_on[i] = 0; /* slipped past */
|
|
194
783
|
}
|
|
195
|
-
spawn_timer++;
|
|
196
|
-
if (spawn_timer >= 32) { spawn_timer = 0; spawn(); }
|
|
197
784
|
|
|
785
|
+
++spawn_timer;
|
|
786
|
+
if (spawn_timer >= 40) { spawn_timer = 0; spawn_enemy(); }
|
|
787
|
+
|
|
788
|
+
/* Bullets ↔ enemies. */
|
|
198
789
|
for (i = 0; i < MAX_BULLETS; i++) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if (
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
790
|
+
uint8_t e;
|
|
791
|
+
if (!bullet_on[i]) continue;
|
|
792
|
+
for (e = 0; e < MAX_ENEMIES; e++) {
|
|
793
|
+
if (!enemy_on[e]) continue;
|
|
794
|
+
if (hits(bullet_x[i], bullet_y[i], enemy_x[e], enemy_y[e])) {
|
|
795
|
+
bullet_on[i] = 0;
|
|
796
|
+
enemy_on[e] = 0;
|
|
797
|
+
score += 10;
|
|
798
|
+
sfx_noise(6); /* boom */
|
|
799
|
+
draw_bar_stats();
|
|
207
800
|
break;
|
|
208
801
|
}
|
|
209
802
|
}
|
|
210
803
|
}
|
|
211
804
|
|
|
212
|
-
/*
|
|
213
|
-
POKE(VIC_SPRITE_X(SLOT_PLAYER), player.x);
|
|
214
|
-
POKE(VIC_SPRITE_Y(SLOT_PLAYER), player.y);
|
|
215
|
-
for (i = 0; i < MAX_BULLETS; i++) {
|
|
216
|
-
uint8_t sx = bullets[i].alive ? bullets[i].x : 0;
|
|
217
|
-
uint8_t sy = bullets[i].alive ? bullets[i].y : 0;
|
|
218
|
-
POKE(VIC_SPRITE_X(SLOT_BULLET0 + i), sx);
|
|
219
|
-
POKE(VIC_SPRITE_Y(SLOT_BULLET0 + i), sy);
|
|
220
|
-
}
|
|
805
|
+
/* Enemies ↔ ships: shared life pool (arcade co-op). */
|
|
221
806
|
for (i = 0; i < MAX_ENEMIES; i++) {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
807
|
+
if (!enemy_on[i]) continue;
|
|
808
|
+
for (p = 0; p < 2; p++) {
|
|
809
|
+
if (!ship_alive[p] || ship_inv[p]) continue;
|
|
810
|
+
if (hits(enemy_x[i], enemy_y[i], ship_x[p], ship_y[p])) {
|
|
811
|
+
enemy_on[i] = 0;
|
|
812
|
+
sfx_noise(16);
|
|
813
|
+
if (lives) --lives;
|
|
814
|
+
draw_bar_stats();
|
|
815
|
+
if (lives == 0) { game_over(); break; }
|
|
816
|
+
ship_x[p] = 50; /* knockback respawn + mercy frames */
|
|
817
|
+
ship_inv[p] = 90;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
if (state != ST_PLAY) break;
|
|
226
821
|
}
|
|
822
|
+
if (state != ST_PLAY) continue;
|
|
823
|
+
|
|
824
|
+
/* Stage all 8 sprite slots, then commit enable + X-MSB in one go.
|
|
825
|
+
* Invulnerable ships blink by skipping their slot every few frames. */
|
|
826
|
+
stage_begin();
|
|
827
|
+
for (p = 0; p < 2; p++)
|
|
828
|
+
if (ship_alive[p] && !(ship_inv[p] & 4))
|
|
829
|
+
stage_sprite(p ? SLOT_P2 : SLOT_P1, ship_x[p], ship_y[p]);
|
|
830
|
+
for (i = 0; i < MAX_BULLETS; i++)
|
|
831
|
+
if (bullet_on[i]) stage_sprite(SLOT_BULLET0 + i, bullet_x[i], bullet_y[i]);
|
|
832
|
+
for (i = 0; i < MAX_ENEMIES; i++)
|
|
833
|
+
if (enemy_on[i]) stage_sprite(SLOT_ENEMY0 + i, enemy_x[i], enemy_y[i]);
|
|
834
|
+
stage_commit();
|
|
227
835
|
}
|
|
228
836
|
}
|