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,32 +1,72 @@
|
|
|
1
|
-
; ── shmup.asm — Atari 2600
|
|
1
|
+
; ── shmup.asm — FLAK FRENZY — Atari 2600 gallery shooter (complete game) ─────
|
|
2
2
|
;
|
|
3
|
-
;
|
|
4
|
-
;
|
|
5
|
-
;
|
|
6
|
-
; the
|
|
3
|
+
; A COMPLETE, working game — drawn title screen, a fixed/gallery shooter
|
|
4
|
+
; (your cannon vs a marching formation of replicated invaders), score +
|
|
5
|
+
; in-session hi-score, TIA sound effects + a title jingle, game-over with
|
|
6
|
+
; auto-return to the title, and the 2600's signature feature: THE WHOLE
|
|
7
|
+
; MACHINE. There is no framebuffer, no tilemap, no OS — every visible
|
|
8
|
+
; scanline below is composed live by racing the beam, and this file
|
|
9
|
+
; teaches the gallery-shooter's load-bearing TIA tricks while doing it:
|
|
7
10
|
;
|
|
8
|
-
;
|
|
9
|
-
;
|
|
11
|
+
; 1. NUSIZ REPLICATION (the enemy formation) — ONE GRP1 write paints
|
|
12
|
+
; THREE invaders. NUSIZ1 = %011 (three medium-spaced copies) is how
|
|
13
|
+
; Space Invaders / Galaxian / Demon Attack drew a whole row of aliens
|
|
14
|
+
; from a single 8-pixel sprite. This is the genre's defining idiom and
|
|
15
|
+
; the reason the 2600 is GOOD at this genre — playfield "barcode" bars
|
|
16
|
+
; would read as stripes, not invaders. We replicate a 2x2 BLOCK of six
|
|
17
|
+
; invaders by re-using P1 (GRP1) on a SECOND scanline band lower down.
|
|
18
|
+
; 2. RESP/HMOVE BEAM POSITIONING (the SBC-#15 idiom) — there is no sprite
|
|
19
|
+
; X register; you strobe RESPx WHERE THE BEAM IS, then nudge ±7px with
|
|
20
|
+
; HMOVE. Three objects (ship P0, formation P1, shot M0) positioned this
|
|
21
|
+
; way each frame, inside the timed VBLANK window.
|
|
22
|
+
; 3. TIA COLLISION LATCHES (the hit detect) — the TIA detects M0/P1 pixel
|
|
23
|
+
; overlap in silicon as it draws; we read the latched result one frame
|
|
24
|
+
; later, free, instead of doing AABB math. Clear it every frame (CXCLR)
|
|
25
|
+
; or a stale hit scores phantom kills.
|
|
26
|
+
; 4. TIM64T/INTIM FRAME TIMING — set the RIOT timer for VBLANK/overscan and
|
|
27
|
+
; let it absorb however much the game logic costs, instead of hand-
|
|
28
|
+
; counting WSYNCs (which rolls the picture the moment logic grows).
|
|
10
29
|
;
|
|
11
|
-
;
|
|
12
|
-
;
|
|
13
|
-
;
|
|
14
|
-
;
|
|
15
|
-
;
|
|
16
|
-
; PF = a thin ground line only (NOT the aliens — playfield bits look
|
|
17
|
-
; like a barcode; real sprites read as actual invaders).
|
|
30
|
+
; THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
|
|
31
|
+
; very different one. The markers tell you what's what:
|
|
32
|
+
; HARDWARE IDIOM (load-bearing) — cycle-counted / footgun-dodging code;
|
|
33
|
+
; reshape your gameplay around it (see TROUBLESHOOTING before changing).
|
|
34
|
+
; GAME LOGIC (clay) — movement, scoring, tuning, art: reshape freely.
|
|
18
35
|
;
|
|
19
|
-
;
|
|
20
|
-
;
|
|
21
|
-
;
|
|
22
|
-
;
|
|
23
|
-
; adding M1 as an alien bomb.
|
|
36
|
+
; GAME_TITLE: on the 2600 a title is DRAWN, not printed — there is no font
|
|
37
|
+
; hardware. The FLAK/FRENZY banner bitmaps near the bottom of this file ARE
|
|
38
|
+
; the title; redraw them for your game (the comment above each table shows
|
|
39
|
+
; the 40-pixel artwork and the PF0/PF1/PF2 bit-order encoding).
|
|
24
40
|
;
|
|
25
|
-
;
|
|
41
|
+
; CONTROLS (documented for players and for the fork README):
|
|
42
|
+
; Title: fire on JOYSTICK 0 (or console RESET) starts the game
|
|
43
|
+
; Play: joystick 0 LEFT/RIGHT moves your cannon; fire launches a shot
|
|
44
|
+
; (one shot in flight at a time, like the era's arcade games);
|
|
45
|
+
; console RESET returns to the title
|
|
46
|
+
; Clear the whole formation to advance; let it reach your row and it's
|
|
47
|
+
; game over. Your best SCORE this session is shown on the title screen.
|
|
48
|
+
;
|
|
49
|
+
; PLAYERS — 1P, honest. The 2600 has two joystick ports, but this genre's
|
|
50
|
+
; kernel is already spending its scanline budget on P0 (your ship), P1 with
|
|
51
|
+
; NUSIZ replication (the formation), and M0 (the shot). A second human ship
|
|
52
|
+
; would need its own positioned object competing for the SAME 76-cycle
|
|
53
|
+
; lines the formation already fills — so like the arcade gallery shooters
|
|
54
|
+
; this descends from, FLAK FRENZY is single-player. (To add 2P alternating
|
|
55
|
+
; TURNS instead — cheap, no extra kernel objects — keep a second score/lives
|
|
56
|
+
; pair and swap on death; left as an exercise.)
|
|
57
|
+
;
|
|
58
|
+
; HI-SCORE HONESTY: real 2600 cartridges had NO battery, NO SRAM, NO
|
|
59
|
+
; persistence of any kind. The hi-score here lives in RIOT RAM ($A0) and
|
|
60
|
+
; survives game → title cycles only WITHIN one power-on session — exactly
|
|
61
|
+
; like the arcade machines of the era. Power off and it is gone. Do not
|
|
62
|
+
; fake an EEPROM; state it honestly in your fork too.
|
|
63
|
+
;
|
|
64
|
+
; NTSC frame: 3 VSYNC + 37 VBLANK + 192 visible + 30 overscan = 262 lines.
|
|
26
65
|
|
|
27
66
|
processor 6502
|
|
28
67
|
org $F000
|
|
29
68
|
|
|
69
|
+
; ── TIA write registers ───────────────────────────────────────────────
|
|
30
70
|
VSYNC = $00
|
|
31
71
|
VBLANK = $01
|
|
32
72
|
WSYNC = $02
|
|
@@ -37,34 +77,79 @@ COLUP1 = $07
|
|
|
37
77
|
COLUPF = $08
|
|
38
78
|
COLUBK = $09
|
|
39
79
|
CTRLPF = $0A
|
|
80
|
+
PF0 = $0D
|
|
81
|
+
PF1 = $0E
|
|
82
|
+
PF2 = $0F
|
|
40
83
|
RESP0 = $10
|
|
41
84
|
RESP1 = $11
|
|
42
85
|
RESM0 = $12
|
|
43
86
|
GRP0 = $1B
|
|
44
87
|
GRP1 = $1C
|
|
45
88
|
ENAM0 = $1D
|
|
46
|
-
PF0 = $0D
|
|
47
89
|
HMP0 = $20
|
|
48
90
|
HMP1 = $21
|
|
49
91
|
HMM0 = $22
|
|
50
92
|
HMOVE = $2A
|
|
51
93
|
HMCLR = $2B
|
|
52
|
-
|
|
53
|
-
|
|
94
|
+
CXCLR = $2C
|
|
95
|
+
; ── TIA audio ─────────────────────────────────────────────────────────
|
|
54
96
|
AUDC0 = $15
|
|
97
|
+
AUDC1 = $16
|
|
55
98
|
AUDF0 = $17
|
|
99
|
+
AUDF1 = $18
|
|
56
100
|
AUDV0 = $19
|
|
101
|
+
AUDV1 = $1A
|
|
102
|
+
; ── TIA READ registers (separate read map — the same addresses as some
|
|
103
|
+
; write strobes; e.g. CXM0P reads $00 while STA $00 strobes VSYNC) ──────
|
|
104
|
+
CXM0P = $00 ; bit7 = missile0 / player1 collision (latched)
|
|
105
|
+
INPT4 = $0C ; joystick 0 fire (bit7, ACTIVE LOW)
|
|
106
|
+
; ── RIOT ──────────────────────────────────────────────────────────────
|
|
107
|
+
SWCHA = $280 ; joysticks: P0 = high nibble, P1 = LOW nibble
|
|
108
|
+
SWCHB = $282 ; console: bit0 RESET, bit1 SELECT (ACTIVE LOW)
|
|
109
|
+
INTIM = $284 ; timer read
|
|
110
|
+
TIM64T = $296 ; timer set, 64-cycle ticks
|
|
111
|
+
|
|
112
|
+
; ── Zero-page state (the 2600's ENTIRE RAM is $80-$FF — 128 bytes; in
|
|
113
|
+
; core memory dumps system_ram offset 0 = $80) ────────────────────────
|
|
114
|
+
STATE = $80 ; 0 = title, 1 = play, 2 = game over
|
|
115
|
+
P_X = $81 ; player cannon X column (visible 0..159)
|
|
116
|
+
FORM_X = $82 ; formation left edge (P1 X)
|
|
117
|
+
FORM_Y = $83 ; formation TOP scanline (beam counts 192→1, so a
|
|
118
|
+
; SMALLER value = LOWER on screen = closer to you)
|
|
119
|
+
FORM_DIR = $84 ; +1 = marching right, $FF = marching left
|
|
120
|
+
ALIVE = $85 ; 6 bits = which formation cells still live (one per
|
|
121
|
+
; invader: a kill clears its bit, kernel skips it)
|
|
122
|
+
SHOT_X = $86 ; missile column
|
|
123
|
+
SHOT_Y = $87 ; missile scanline (0 = no shot active)
|
|
124
|
+
SCORE = $88 ; current score, BCD (digit nibbles fall out free)
|
|
125
|
+
SCORE_HI = $89 ; current score high byte, BCD (hundreds/thousands)
|
|
126
|
+
FRAME = $8A
|
|
127
|
+
MARCH = $8B ; march step timer
|
|
128
|
+
SFX_LEFT = $8C ; frames remaining on the voice-0 sound effect
|
|
129
|
+
TUNE_SEL = $8D ; 0 = title jingle, 1 = game-over tune (voice 1)
|
|
130
|
+
TUNE_POS = $8E
|
|
131
|
+
TUNE_LEFT = $8F ; frames left on current jingle note (0 = silent)
|
|
132
|
+
OVER_T = $90 ; game-over auto-return-to-title countdown
|
|
133
|
+
SWCHB_PRV = $91 ; previous SWCHB for RESET edge detect
|
|
134
|
+
FIRE_PRV = $92 ; previous fire level (bit7) for fire-edge detect
|
|
135
|
+
EDGEB = $93 ; this frame's RESET press-edge (bit0)
|
|
136
|
+
FIRE_EDG = $94 ; this frame's fire press-edge (bit7)
|
|
137
|
+
TMP = $95
|
|
138
|
+
WAVE = $96 ; wave number (formation speeds up each wave)
|
|
139
|
+
MARCH_PERIOD = $97 ; frames per march step (set per wave; speeds up)
|
|
140
|
+
S0BUF = $98 ; 6 rows: packed score digits for the kernel
|
|
141
|
+
SCRATCH = $9E ; 6 bytes general kernel/packer scratch
|
|
142
|
+
SCORE_HSV = $A4 ; SESSION hi-score (BCD low byte). RAM only — real
|
|
143
|
+
SCORE_HSH = $A5 ; 2600 carts have no battery; honest by design.
|
|
144
|
+
HSBUF = $A6 ; 6 rows: hi-score, packed
|
|
57
145
|
|
|
58
|
-
;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
FRAME = $86
|
|
66
|
-
MARCH = $87 ; march timer
|
|
67
|
-
SFX_LEFT = $88
|
|
146
|
+
SHIPGFX = %00111100 ; cannon top bar (full sprite in SHIP table below)
|
|
147
|
+
COL_SHIP = $1E ; yellow cannon
|
|
148
|
+
COL_FORM = $46 ; red invaders
|
|
149
|
+
COL_BG = $00 ; black space
|
|
150
|
+
COL_SHOT = $0E ; white shot
|
|
151
|
+
FORM_COLS = 3 ; 3 NUSIZ copies across
|
|
152
|
+
FORM_W = 32 ; formation block width in color clocks (for the edge)
|
|
68
153
|
|
|
69
154
|
START:
|
|
70
155
|
SEI
|
|
@@ -73,316 +158,979 @@ START:
|
|
|
73
158
|
TXS
|
|
74
159
|
LDA #0
|
|
75
160
|
.clr:
|
|
76
|
-
STA $00,X
|
|
77
|
-
DEX
|
|
78
|
-
BNE .clr
|
|
161
|
+
STA $00,X ; clears ALL of $00-$FF: zero page RAM AND the TIA
|
|
162
|
+
DEX ; write registers (GRP/ENAxx/HMxx/audio all silenced
|
|
163
|
+
BNE .clr ; — the standard 2600 power-on hygiene)
|
|
79
164
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
LDA #
|
|
83
|
-
STA ALIEN_X
|
|
84
|
-
LDA #60
|
|
85
|
-
STA ALIEN_Y
|
|
86
|
-
LDA #1
|
|
87
|
-
STA ALIEN_DIR
|
|
88
|
-
LDA #$FF
|
|
89
|
-
STA SHOT_Y ; no shot yet
|
|
90
|
-
|
|
91
|
-
; Colours
|
|
92
|
-
LDA #$06 ; dark blue-grey bg
|
|
93
|
-
STA COLUBK
|
|
94
|
-
LDA #$1E ; yellow cannon
|
|
165
|
+
; Fixed identity colors (the kernels rewrite COLUPF per band, but the
|
|
166
|
+
; object colors are constant all session).
|
|
167
|
+
LDA #COL_SHIP
|
|
95
168
|
STA COLUP0
|
|
96
|
-
LDA
|
|
169
|
+
LDA #COL_FORM
|
|
97
170
|
STA COLUP1
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
; Object sizes: P0 double-width; P1 = 3 medium-spaced copies.
|
|
102
|
-
LDA #%00000101 ; NUSIZ0: P0 double size (2x width)
|
|
171
|
+
; NUSIZ0: single-width cannon. NUSIZ1: THREE medium-spaced copies — the
|
|
172
|
+
; gallery-shooter idiom. Set ONCE; persists every frame.
|
|
173
|
+
LDA #%00000000
|
|
103
174
|
STA NUSIZ0
|
|
104
|
-
LDA #%00000011 ;
|
|
175
|
+
LDA #%00000011 ; %011 = 3 copies, medium spacing
|
|
105
176
|
STA NUSIZ1
|
|
106
|
-
LDA #1
|
|
107
|
-
STA CTRLPF ; PF reflected (symmetric ground)
|
|
108
177
|
|
|
109
|
-
|
|
110
|
-
INC FRAME
|
|
178
|
+
JSR enter_title
|
|
111
179
|
|
|
112
|
-
|
|
180
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
181
|
+
; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
182
|
+
; THE FRAME LOOP. 262 scanlines, every frame, forever. VBLANK and overscan
|
|
183
|
+
; are timed with the RIOT timer (TIM64T) instead of counted WSYNCs: set the
|
|
184
|
+
; timer, run however much game logic the state needs, then spin on INTIM.
|
|
185
|
+
; This is how shipped 2600 games did it, and it kills the classic homebrew
|
|
186
|
+
; bug class where adding one branch to the game logic emits a 263rd line
|
|
187
|
+
; and the TV loses vsync (rolling picture). The VISIBLE 192 lines are still
|
|
188
|
+
; counted exactly — every STA WSYNC below is one scanline, and each state's
|
|
189
|
+
; kernel accounts for all 192.
|
|
190
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
191
|
+
MAIN:
|
|
192
|
+
; VSYNC: 3 lines
|
|
113
193
|
LDA #2
|
|
194
|
+
STA VBLANK
|
|
114
195
|
STA VSYNC
|
|
115
196
|
STA WSYNC
|
|
116
197
|
STA WSYNC
|
|
117
198
|
STA WSYNC
|
|
118
199
|
LDA #0
|
|
119
200
|
STA VSYNC
|
|
201
|
+
; 37 lines of VBLANK = 2812 cycles ≈ 43 × 64-cycle timer ticks.
|
|
202
|
+
LDA #43
|
|
203
|
+
STA TIM64T
|
|
204
|
+
|
|
205
|
+
JSR frame_logic ; all game thinking happens in the blanked region
|
|
206
|
+
|
|
207
|
+
; burn whatever VBLANK time the logic didn't use
|
|
208
|
+
.vbwait:
|
|
209
|
+
LDA INTIM
|
|
210
|
+
BNE .vbwait
|
|
211
|
+
STA WSYNC
|
|
120
212
|
|
|
121
|
-
;
|
|
213
|
+
; kernel dispatch — title has its own kernel; play and game-over share one
|
|
214
|
+
LDA STATE
|
|
215
|
+
BNE .ingame
|
|
216
|
+
JMP title_kernel
|
|
217
|
+
.ingame:
|
|
218
|
+
JMP play_kernel
|
|
219
|
+
|
|
220
|
+
kernel_done:
|
|
221
|
+
; overscan: 30 lines, timer-paced like VBLANK
|
|
122
222
|
LDA #2
|
|
123
223
|
STA VBLANK
|
|
124
|
-
|
|
125
|
-
|
|
224
|
+
LDA #35
|
|
225
|
+
STA TIM64T
|
|
226
|
+
.oswait:
|
|
227
|
+
LDA INTIM
|
|
228
|
+
BNE .oswait
|
|
126
229
|
STA WSYNC
|
|
127
|
-
|
|
128
|
-
BNE .vb
|
|
230
|
+
JMP MAIN
|
|
129
231
|
|
|
130
|
-
|
|
131
|
-
|
|
232
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
233
|
+
; Per-frame logic, dispatched by state. Runs entirely inside the timed
|
|
234
|
+
; VBLANK window (~2800 cycles — an eternity next to the kernel's 76/line).
|
|
235
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
236
|
+
frame_logic:
|
|
237
|
+
INC FRAME
|
|
238
|
+
JSR audio_tick
|
|
239
|
+
|
|
240
|
+
; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
241
|
+
; Console RESET + fire button are ACTIVE LOW and not debounced; a held
|
|
242
|
+
; RESET would restart every frame. Convert to press-EDGES once per frame:
|
|
243
|
+
; edge = was-released-last-frame AND pressed-now.
|
|
244
|
+
LDA SWCHB
|
|
245
|
+
TAX ; X = current switch levels
|
|
246
|
+
EOR #$FF ; A = pressed-now mask (1 = held)
|
|
247
|
+
AND SWCHB_PRV ; ...that were RELEASED (1) last frame
|
|
248
|
+
STA EDGEB ; bit0 = RESET edge
|
|
249
|
+
STX SWCHB_PRV
|
|
250
|
+
; fire button → same edge treatment in bit7
|
|
251
|
+
LDA #0
|
|
252
|
+
BIT INPT4
|
|
253
|
+
BMI .fup ; bit7 set = not pressed (active low)
|
|
254
|
+
ORA #$80
|
|
255
|
+
.fup:
|
|
256
|
+
TAY ; Y = pressed-now (bit7)
|
|
257
|
+
LDA FIRE_PRV
|
|
258
|
+
EOR #$FF
|
|
259
|
+
STA TMP ; released-last-frame mask
|
|
260
|
+
TYA
|
|
261
|
+
AND TMP
|
|
262
|
+
STA FIRE_EDG ; bit7 = fire press-edge
|
|
263
|
+
STY FIRE_PRV
|
|
264
|
+
|
|
265
|
+
LDA STATE
|
|
266
|
+
BEQ logic_title
|
|
267
|
+
CMP #1
|
|
268
|
+
BEQ logic_play_jmp
|
|
269
|
+
JMP logic_over
|
|
270
|
+
logic_play_jmp:
|
|
271
|
+
JMP logic_play
|
|
272
|
+
|
|
273
|
+
; ── GAME LOGIC (clay — reshape freely) ── title-screen behavior ────────
|
|
274
|
+
logic_title:
|
|
275
|
+
; fire 0 or console RESET starts the game.
|
|
276
|
+
LDA FIRE_EDG
|
|
277
|
+
BMI .start ; bit7 set = fire edge
|
|
278
|
+
LDA EDGEB
|
|
132
279
|
AND #$01
|
|
133
|
-
BNE .
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
;
|
|
280
|
+
BNE .start
|
|
281
|
+
JMP .packtitle
|
|
282
|
+
.start:
|
|
283
|
+
JMP start_game
|
|
284
|
+
.packtitle:
|
|
285
|
+
; Pack the hi-score into the title's display buffer (the kernel just
|
|
286
|
+
; streams bytes — all per-frame thinking happens HERE, in VBLANK, never
|
|
287
|
+
; inside a kernel). Hi-score uses the high byte's tens digit + low byte's
|
|
288
|
+
; two digits = 3 visible digits, packed two-per-PF1-row like the score.
|
|
289
|
+
LDA SCORE_HSV
|
|
290
|
+
JSR pack_two_digits
|
|
291
|
+
LDY #0
|
|
292
|
+
.hst:
|
|
293
|
+
LDA SCRATCH,Y ; pack_two_digits left 6 rows in SCRATCH..SCRATCH+5
|
|
294
|
+
STA HSBUF,Y
|
|
295
|
+
INY
|
|
296
|
+
CPY #6
|
|
297
|
+
BNE .hst
|
|
298
|
+
|
|
299
|
+
; title shows no moving objects
|
|
300
|
+
LDA #0
|
|
301
|
+
STA GRP0
|
|
302
|
+
STA GRP1
|
|
303
|
+
STA ENAM0
|
|
304
|
+
RTS
|
|
305
|
+
|
|
306
|
+
; ── GAME LOGIC (clay — reshape freely) ── one frame of the shooter ─────
|
|
307
|
+
logic_play:
|
|
308
|
+
LDA EDGEB
|
|
309
|
+
AND #$01 ; console RESET → back to title
|
|
310
|
+
BEQ .noquit
|
|
311
|
+
JMP enter_title
|
|
312
|
+
.noquit:
|
|
313
|
+
|
|
314
|
+
; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
315
|
+
; SWCHA is ACTIVE LOW (0 = pressed) and must be RE-LOADED for every
|
|
316
|
+
; direction check. The classic bug: caching it in A and chaining ASLs,
|
|
317
|
+
; then clobbering A with game state between shifts — "right works once,
|
|
318
|
+
; left never moves". Fresh LDA SWCHA + AND #mask per check is immune.
|
|
319
|
+
; Joystick 0 lives in the HIGH nibble: bit7 right, bit6 left.
|
|
139
320
|
LDA SWCHA
|
|
140
|
-
AND #$80 ;
|
|
321
|
+
AND #$80 ; joy0 right
|
|
141
322
|
BNE .nr
|
|
142
323
|
LDA P_X
|
|
143
|
-
CMP #
|
|
324
|
+
CMP #144
|
|
144
325
|
BCS .nr
|
|
145
326
|
INC P_X
|
|
146
327
|
INC P_X
|
|
147
328
|
.nr:
|
|
148
|
-
LDA SWCHA
|
|
149
|
-
AND #$40 ;
|
|
329
|
+
LDA SWCHA ; RE-LOAD — never trust A to still hold SWCHA
|
|
330
|
+
AND #$40 ; joy0 left
|
|
150
331
|
BNE .nl
|
|
151
332
|
LDA P_X
|
|
152
|
-
CMP #
|
|
333
|
+
CMP #14
|
|
153
334
|
BCC .nl
|
|
154
335
|
DEC P_X
|
|
155
336
|
DEC P_X
|
|
156
337
|
.nl:
|
|
157
|
-
.skipmove:
|
|
158
338
|
|
|
159
|
-
; Fire
|
|
339
|
+
; Fire: one shot in flight at a time (SHOT_Y == 0 means free).
|
|
160
340
|
LDA SHOT_Y
|
|
161
|
-
CMP #$FF
|
|
162
341
|
BNE .noFire
|
|
163
|
-
|
|
164
|
-
|
|
342
|
+
LDA FIRE_EDG
|
|
343
|
+
BPL .noFire ; no fire edge this frame
|
|
165
344
|
LDA P_X
|
|
166
345
|
CLC
|
|
167
|
-
ADC #
|
|
168
|
-
STA SHOT_X
|
|
169
|
-
LDA #
|
|
170
|
-
STA SHOT_Y
|
|
171
|
-
; pew sfx
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
STA AUDF0
|
|
176
|
-
LDA #$0C
|
|
177
|
-
STA AUDV0
|
|
178
|
-
LDA #6
|
|
179
|
-
STA SFX_LEFT
|
|
346
|
+
ADC #3
|
|
347
|
+
STA SHOT_X ; shot rises from the cannon muzzle
|
|
348
|
+
LDA #30
|
|
349
|
+
STA SHOT_Y ; just above the cannon
|
|
350
|
+
LDA #$04 ; pew sfx
|
|
351
|
+
LDX #$08
|
|
352
|
+
LDY #6
|
|
353
|
+
JSR sfx_play
|
|
180
354
|
.noFire:
|
|
181
355
|
|
|
182
|
-
; Move shot
|
|
356
|
+
; Move the shot UP the screen. Beam-Y counts 192→1 going down, so "up"
|
|
357
|
+
; means a LARGER scanline number → ADD. Despawn past the top band.
|
|
183
358
|
LDA SHOT_Y
|
|
184
|
-
CMP #$FF
|
|
185
359
|
BEQ .noShotMove
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
STA SHOT_Y
|
|
189
|
-
CMP #50
|
|
190
|
-
BCS .noShotMove
|
|
191
|
-
LDA #$FF
|
|
360
|
+
CLC
|
|
361
|
+
ADC #5
|
|
192
362
|
STA SHOT_Y
|
|
363
|
+
CMP #178
|
|
364
|
+
BCC .noShotMove
|
|
365
|
+
LDA #0
|
|
366
|
+
STA SHOT_Y ; flew off the top → free the shot
|
|
193
367
|
.noShotMove:
|
|
194
368
|
|
|
195
|
-
;
|
|
369
|
+
; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
370
|
+
; Shot/invader collision via the TIA's hardware collision LATCH — the
|
|
371
|
+
; 2600 detects M0/P1 pixel overlap in silicon while it draws; we read the
|
|
372
|
+
; latched result here, one frame late, for free (no AABB math). Rules:
|
|
373
|
+
; * latches accumulate until CXCLR — clear them EVERY frame, or a stale
|
|
374
|
+
; hit scores a phantom kill long after the shot is gone;
|
|
375
|
+
; * NUSIZ replication means P1 is THREE invaders + we draw two rows, so
|
|
376
|
+
; the latch only says "you hit SOME invader" — we map the shot's X/Y
|
|
377
|
+
; to the specific cell to clear the right ALIVE bit.
|
|
378
|
+
BIT CXM0P ; bit7 (N flag) = M0/P1 overlapped last frame
|
|
379
|
+
BPL .noHit
|
|
380
|
+
LDA SHOT_Y
|
|
381
|
+
BEQ .noHit ; no shot in flight → ignore stale latch
|
|
382
|
+
JSR resolve_hit
|
|
383
|
+
.noHit:
|
|
384
|
+
STA CXCLR ; arm the latch fresh for the frame we're about to draw
|
|
385
|
+
|
|
386
|
+
; March the formation. Speed ramps with the wave (fewer frames per step).
|
|
196
387
|
INC MARCH
|
|
197
388
|
LDA MARCH
|
|
198
|
-
CMP
|
|
199
|
-
BCC .
|
|
389
|
+
CMP MARCH_PERIOD ; period set by wave in start_wave
|
|
390
|
+
BCC .noMarch_jmp
|
|
200
391
|
LDA #0
|
|
201
392
|
STA MARCH
|
|
202
|
-
LDA
|
|
393
|
+
LDA FORM_DIR
|
|
203
394
|
BMI .marchLeft
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
395
|
+
; marching right: step until the block's right edge nears the wall
|
|
396
|
+
LDA FORM_X
|
|
397
|
+
CMP #112
|
|
398
|
+
BCS .flip
|
|
207
399
|
CLC
|
|
208
|
-
ADC #
|
|
209
|
-
STA
|
|
400
|
+
ADC #4
|
|
401
|
+
STA FORM_X
|
|
210
402
|
JMP .noMarch
|
|
211
403
|
.marchLeft:
|
|
212
|
-
LDA
|
|
404
|
+
LDA FORM_X
|
|
213
405
|
CMP #14
|
|
214
|
-
BCC .flip
|
|
406
|
+
BCC .flip
|
|
215
407
|
SEC
|
|
216
|
-
SBC #
|
|
217
|
-
STA
|
|
408
|
+
SBC #4
|
|
409
|
+
STA FORM_X
|
|
218
410
|
JMP .noMarch
|
|
219
411
|
.flip:
|
|
220
|
-
; Reverse direction and
|
|
221
|
-
; the
|
|
222
|
-
;
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
LDA ALIEN_Y
|
|
228
|
-
CMP #30
|
|
229
|
-
BCC .invaded ; reached the cannon's row — the aliens GOT YOU
|
|
412
|
+
; Reverse direction and DROP the whole block one notch toward the player.
|
|
413
|
+
; FORM_Y is the top scanline; SMALLER Y = lower on screen, so "drop" =
|
|
414
|
+
; subtract. Reach the cannon's row and it's game over.
|
|
415
|
+
LDA FORM_DIR
|
|
416
|
+
EOR #$FE ; +1 <-> $FF
|
|
417
|
+
STA FORM_DIR
|
|
418
|
+
LDA FORM_Y
|
|
230
419
|
SEC
|
|
231
|
-
SBC #
|
|
232
|
-
STA
|
|
233
|
-
|
|
234
|
-
.
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
420
|
+
SBC #8
|
|
421
|
+
STA FORM_Y
|
|
422
|
+
CMP #44 ; reached the cannon's band?
|
|
423
|
+
BCS .noMarch
|
|
424
|
+
JMP do_game_over
|
|
425
|
+
.noMarch_jmp:
|
|
426
|
+
JMP .formdone
|
|
427
|
+
.noMarch:
|
|
428
|
+
.formdone:
|
|
429
|
+
; Wave cleared? (no ALIVE bits left) → next wave, faster, from the top.
|
|
430
|
+
LDA ALIVE
|
|
431
|
+
BNE .alive
|
|
432
|
+
INC WAVE
|
|
433
|
+
JSR start_wave
|
|
434
|
+
LDA #$0A ; wave-clear chime
|
|
435
|
+
LDX #$0C
|
|
436
|
+
LDY #18
|
|
437
|
+
JSR sfx_play
|
|
438
|
+
.alive:
|
|
439
|
+
JMP pack_score ; render SCORE into the kernel's row buffer (tail-RTS)
|
|
440
|
+
|
|
441
|
+
; ── GAME LOGIC (clay — reshape freely) ── game-over freeze-frame ───────
|
|
442
|
+
logic_over:
|
|
443
|
+
LDA EDGEB
|
|
444
|
+
AND #$01
|
|
445
|
+
BNE .toTitle
|
|
446
|
+
LDA FIRE_EDG
|
|
447
|
+
BMI .toTitle
|
|
448
|
+
DEC OVER_T
|
|
449
|
+
BNE .stay
|
|
450
|
+
.toTitle:
|
|
451
|
+
JMP enter_title
|
|
452
|
+
.stay:
|
|
453
|
+
; freeze: hide the shot, leave the formation sitting on the cannon
|
|
454
|
+
LDA #0
|
|
455
|
+
STA SHOT_Y
|
|
456
|
+
STA ENAM0
|
|
457
|
+
RTS
|
|
458
|
+
|
|
459
|
+
; ── GAME LOGIC (clay — reshape freely) ── helpers ──────────────────────
|
|
460
|
+
|
|
461
|
+
; resolve_hit — a shot overlapped the formation. Figure out WHICH of the up
|
|
462
|
+
; to 6 cells (2 rows × 3 NUSIZ copies) the shot is inside, clear its ALIVE
|
|
463
|
+
; bit, score it, and free the shot. Cell columns are FORM_X + n*16.
|
|
464
|
+
resolve_hit:
|
|
465
|
+
; which row? top row spans FORM_Y..FORM_Y-7, bottom row 16 lower (smaller Y)
|
|
466
|
+
LDA SHOT_Y
|
|
467
|
+
CMP FORM_Y
|
|
468
|
+
BCC .maybeBottom ; shot Y below the top row's top
|
|
469
|
+
; (shot at/above top row top — treat as top row)
|
|
470
|
+
LDX #0 ; row 0 → ALIVE bits 0..2
|
|
471
|
+
JMP .findCol
|
|
472
|
+
.maybeBottom:
|
|
473
|
+
LDX #3 ; row 1 → ALIVE bits 3..5
|
|
474
|
+
.findCol:
|
|
475
|
+
; column 0..2: nearest of FORM_X, FORM_X+16, FORM_X+32 to SHOT_X
|
|
476
|
+
LDA SHOT_X
|
|
477
|
+
SEC
|
|
478
|
+
SBC FORM_X ; offset into the block
|
|
479
|
+
BMI .col0
|
|
480
|
+
CMP #8
|
|
481
|
+
BCC .col0
|
|
482
|
+
CMP #24
|
|
483
|
+
BCC .col1
|
|
484
|
+
; col 2
|
|
485
|
+
INX
|
|
486
|
+
.col1:
|
|
487
|
+
INX
|
|
488
|
+
.col0:
|
|
489
|
+
; X = bit index 0..5; clear that ALIVE bit if it's set (a real kill)
|
|
490
|
+
LDA BITMASK,X
|
|
491
|
+
AND ALIVE
|
|
492
|
+
BEQ .stale ; that cell already dead → no double-score
|
|
493
|
+
LDA BITMASK,X
|
|
494
|
+
EOR #$FF
|
|
495
|
+
AND ALIVE
|
|
496
|
+
STA ALIVE
|
|
497
|
+
JSR add_score ; +10 points
|
|
498
|
+
LDA #0
|
|
499
|
+
STA SHOT_Y ; consume the shot
|
|
500
|
+
LDA #$08 ; hit sfx
|
|
501
|
+
LDX #$04
|
|
502
|
+
LDY #8
|
|
503
|
+
JMP sfx_play
|
|
504
|
+
.stale:
|
|
505
|
+
RTS
|
|
506
|
+
|
|
507
|
+
add_score: ; +10 (one invader), BCD, capped at 9990
|
|
508
|
+
SED
|
|
509
|
+
LDA SCORE
|
|
510
|
+
CLC
|
|
511
|
+
ADC #$10 ; tens place +1 → +10 points
|
|
512
|
+
STA SCORE
|
|
513
|
+
LDA SCORE_HI
|
|
514
|
+
ADC #0 ; carry into the high byte
|
|
515
|
+
STA SCORE_HI
|
|
516
|
+
CLD
|
|
517
|
+
; keep the running session hi-score
|
|
518
|
+
LDA SCORE_HI
|
|
519
|
+
CMP SCORE_HSH
|
|
520
|
+
BCC .nohs
|
|
521
|
+
BNE .seths
|
|
522
|
+
LDA SCORE
|
|
523
|
+
CMP SCORE_HSV
|
|
524
|
+
BCC .nohs
|
|
525
|
+
.seths:
|
|
526
|
+
LDA SCORE
|
|
527
|
+
STA SCORE_HSV
|
|
528
|
+
LDA SCORE_HI
|
|
529
|
+
STA SCORE_HSH
|
|
530
|
+
.nohs:
|
|
531
|
+
RTS
|
|
532
|
+
|
|
533
|
+
start_wave: ; (re)seed the formation; called per wave
|
|
534
|
+
LDA #$3F ; 6 invaders alive (bits 0..5)
|
|
535
|
+
STA ALIVE
|
|
536
|
+
LDA #48
|
|
537
|
+
STA FORM_X
|
|
538
|
+
LDA #150
|
|
539
|
+
STA FORM_Y ; high on screen
|
|
242
540
|
LDA #1
|
|
243
|
-
STA
|
|
244
|
-
LDA
|
|
245
|
-
STA
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
541
|
+
STA FORM_DIR
|
|
542
|
+
LDA #0
|
|
543
|
+
STA MARCH
|
|
544
|
+
; march period: 24 frames, minus 3 per wave, floored at 6 (speeds up)
|
|
545
|
+
LDA WAVE
|
|
546
|
+
ASL
|
|
547
|
+
STA TMP
|
|
548
|
+
ASL
|
|
549
|
+
CLC
|
|
550
|
+
ADC TMP ; WAVE*6... but we want WAVE*3
|
|
551
|
+
LSR ; /2 → WAVE*3
|
|
552
|
+
STA TMP
|
|
553
|
+
LDA #24
|
|
554
|
+
SEC
|
|
555
|
+
SBC TMP
|
|
556
|
+
CMP #6
|
|
557
|
+
BCS .pok
|
|
558
|
+
LDA #6
|
|
559
|
+
.pok:
|
|
560
|
+
STA MARCH_PERIOD
|
|
561
|
+
RTS
|
|
562
|
+
|
|
563
|
+
do_game_over:
|
|
564
|
+
LDA #2
|
|
565
|
+
STA STATE
|
|
566
|
+
LDA #200 ; ~3.3 s freeze, then auto-return to title
|
|
567
|
+
STA OVER_T
|
|
568
|
+
LDA #0
|
|
569
|
+
STA SHOT_Y
|
|
570
|
+
STA ENAM0
|
|
571
|
+
LDA #1
|
|
572
|
+
STA TUNE_SEL
|
|
573
|
+
JMP tune_start ; game-over tune on voice 1
|
|
574
|
+
|
|
575
|
+
start_game:
|
|
576
|
+
LDA #0
|
|
577
|
+
STA SCORE
|
|
578
|
+
STA SCORE_HI
|
|
579
|
+
STA WAVE
|
|
580
|
+
STA TUNE_LEFT ; silence the title jingle
|
|
581
|
+
STA AUDV1
|
|
582
|
+
STA SHOT_Y
|
|
583
|
+
LDA #76
|
|
584
|
+
STA P_X
|
|
585
|
+
LDA #1
|
|
586
|
+
STA STATE
|
|
587
|
+
JSR start_wave
|
|
588
|
+
LDA #$06 ; start blip
|
|
589
|
+
LDX #$04
|
|
590
|
+
LDY #10
|
|
591
|
+
JMP sfx_play
|
|
592
|
+
|
|
593
|
+
enter_title:
|
|
594
|
+
LDA #0
|
|
595
|
+
STA STATE
|
|
596
|
+
STA GRP0
|
|
597
|
+
STA GRP1
|
|
598
|
+
STA ENAM0
|
|
249
599
|
STA AUDV0
|
|
250
|
-
LDA #20
|
|
251
600
|
STA SFX_LEFT
|
|
252
|
-
|
|
601
|
+
STA TUNE_SEL ; title jingle
|
|
602
|
+
JMP tune_start
|
|
603
|
+
|
|
604
|
+
digit_times6: ; A = digit 0-9 → A = digit*6 (DIGITS row index)
|
|
605
|
+
STA TMP
|
|
606
|
+
ASL
|
|
607
|
+
CLC
|
|
608
|
+
ADC TMP ; *3
|
|
609
|
+
ASL ; *6
|
|
610
|
+
RTS
|
|
611
|
+
|
|
612
|
+
; pack_two_digits — A = a BCD byte (two digits). Writes 6 rows into SCRATCH,
|
|
613
|
+
; left digit (high nibble) in PF1 high nibble, right digit (low nibble) in
|
|
614
|
+
; PF1 low nibble. In SCORE mode the byte draws twice (two colors) — the
|
|
615
|
+
; classic dual-score look — but here both halves carry the SAME packed pair.
|
|
616
|
+
pack_two_digits:
|
|
617
|
+
PHA
|
|
618
|
+
LSR
|
|
619
|
+
LSR
|
|
620
|
+
LSR
|
|
621
|
+
LSR ; high (tens) digit
|
|
622
|
+
JSR digit_times6
|
|
623
|
+
TAX
|
|
624
|
+
LDY #0
|
|
625
|
+
.pd0:
|
|
626
|
+
LDA DIGITS,X
|
|
627
|
+
STA SCRATCH,Y ; high nibble of font = left digit
|
|
628
|
+
INX
|
|
629
|
+
INY
|
|
630
|
+
CPY #6
|
|
631
|
+
BNE .pd0
|
|
632
|
+
PLA
|
|
633
|
+
AND #$0F ; low (ones) digit
|
|
634
|
+
JSR digit_times6
|
|
635
|
+
TAX
|
|
636
|
+
LDY #0
|
|
637
|
+
.pd1:
|
|
638
|
+
LDA DIGITS,X
|
|
639
|
+
LSR
|
|
640
|
+
LSR
|
|
641
|
+
LSR
|
|
642
|
+
LSR ; ones in the LOW nibble
|
|
643
|
+
ORA SCRATCH,Y
|
|
644
|
+
STA SCRATCH,Y
|
|
645
|
+
INX
|
|
646
|
+
INY
|
|
647
|
+
CPY #6
|
|
648
|
+
BNE .pd1
|
|
649
|
+
RTS
|
|
650
|
+
|
|
651
|
+
pack_score: ; render the low two SCORE digits into S0BUF
|
|
652
|
+
LDA SCORE
|
|
653
|
+
JSR pack_two_digits
|
|
654
|
+
LDY #0
|
|
655
|
+
.pks:
|
|
656
|
+
LDA SCRATCH,Y
|
|
657
|
+
STA S0BUF,Y
|
|
658
|
+
INY
|
|
659
|
+
CPY #6
|
|
660
|
+
BNE .pks
|
|
661
|
+
RTS
|
|
662
|
+
|
|
663
|
+
; ── GAME LOGIC (clay — reshape freely) ── TIA sound ────────────────────
|
|
664
|
+
; Voice 0 = one-shot sound effects; voice 1 = the jingle player. Keeping
|
|
665
|
+
; them on separate voices means a pew blip never cuts the tune off.
|
|
666
|
+
sfx_play: ; A = AUDF pitch, X = AUDC waveform, Y = frames
|
|
667
|
+
STA AUDF0
|
|
668
|
+
STX AUDC0
|
|
669
|
+
STY SFX_LEFT
|
|
670
|
+
LDA #$0C
|
|
671
|
+
STA AUDV0
|
|
672
|
+
RTS
|
|
673
|
+
|
|
674
|
+
tune_start: ; TUNE_SEL chosen by caller (0 title, 1 game over)
|
|
675
|
+
LDA #0
|
|
676
|
+
STA TUNE_POS
|
|
677
|
+
JSR tune_note
|
|
678
|
+
LDA #$04 ; pure square wave
|
|
679
|
+
STA AUDC1
|
|
680
|
+
LDA #$06
|
|
681
|
+
STA AUDV1
|
|
682
|
+
LDA #10
|
|
683
|
+
STA TUNE_LEFT
|
|
684
|
+
RTS
|
|
253
685
|
|
|
254
|
-
|
|
686
|
+
tune_note: ; load AUDF1 from the selected table at TUNE_POS;
|
|
687
|
+
LDX TUNE_POS ; returns Z set (A=0) on the $FF terminator
|
|
688
|
+
LDA TUNE_SEL
|
|
689
|
+
BNE .tn1
|
|
690
|
+
LDA TITLE_TUNE,X
|
|
691
|
+
JMP .tn2
|
|
692
|
+
.tn1:
|
|
693
|
+
LDA OVER_TUNE,X
|
|
694
|
+
.tn2:
|
|
695
|
+
CMP #$FF
|
|
696
|
+
BEQ .tnEnd
|
|
697
|
+
STA AUDF1
|
|
698
|
+
LDA #1
|
|
699
|
+
RTS
|
|
700
|
+
.tnEnd:
|
|
701
|
+
LDA #0
|
|
702
|
+
STA AUDV1
|
|
703
|
+
RTS
|
|
704
|
+
|
|
705
|
+
audio_tick: ; called once per frame, every state
|
|
255
706
|
LDA SFX_LEFT
|
|
256
|
-
BEQ .
|
|
707
|
+
BEQ .at1
|
|
257
708
|
DEC SFX_LEFT
|
|
258
|
-
BNE .
|
|
709
|
+
BNE .at1
|
|
259
710
|
LDA #0
|
|
260
|
-
STA AUDV0
|
|
261
|
-
.
|
|
711
|
+
STA AUDV0 ; sfx finished → silence voice 0
|
|
712
|
+
.at1:
|
|
713
|
+
LDA TUNE_LEFT
|
|
714
|
+
BEQ .at2
|
|
715
|
+
DEC TUNE_LEFT
|
|
716
|
+
BNE .at2
|
|
717
|
+
INC TUNE_POS
|
|
718
|
+
JSR tune_note
|
|
719
|
+
BEQ .at2 ; hit the terminator → tune stays off
|
|
720
|
+
LDA #10
|
|
721
|
+
STA TUNE_LEFT
|
|
722
|
+
.at2:
|
|
723
|
+
RTS
|
|
262
724
|
|
|
263
|
-
|
|
264
|
-
|
|
725
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
726
|
+
; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
727
|
+
; OBJECT POSITIONING — the canonical SBC-#15 beam-race. There is no "X
|
|
728
|
+
; register" for sprites: you strobe RESPx/RESM0 and the object lands
|
|
729
|
+
; WHEREVER THE BEAM IS. Each SBC/BCS lap is 5 CPU cycles = 15 beam pixels,
|
|
730
|
+
; so when the subtraction underflows the beam has crossed x/15 coarse
|
|
731
|
+
; columns; the remainder, EOR #7 shifted to the high nibble, becomes the
|
|
732
|
+
; ±7px fine offset HMOVE applies on the next line. The naive "divide first,
|
|
733
|
+
; then burn a delay loop" version lands in the WRONG column. Three objects =
|
|
734
|
+
; three WSYNC lines + one shared HMOVE line, all inside timed VBLANK.
|
|
735
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
736
|
+
position_objects:
|
|
265
737
|
STA WSYNC
|
|
266
738
|
STA HMCLR
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
.p0pos:
|
|
270
|
-
CPX #15
|
|
271
|
-
BCC .p0done
|
|
739
|
+
LDA P_X ; ship → P0
|
|
740
|
+
STA WSYNC
|
|
272
741
|
SEC
|
|
742
|
+
.d0:
|
|
273
743
|
SBC #15
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
744
|
+
BCS .d0
|
|
745
|
+
EOR #7
|
|
746
|
+
ASL
|
|
747
|
+
ASL
|
|
748
|
+
ASL
|
|
749
|
+
ASL
|
|
277
750
|
STA RESP0
|
|
278
|
-
|
|
751
|
+
STA HMP0
|
|
752
|
+
LDA FORM_X ; formation → P1 (NUSIZ replicates it ×3)
|
|
279
753
|
STA WSYNC
|
|
280
|
-
LDX ALIEN_X
|
|
281
|
-
LDA #0
|
|
282
|
-
.p1pos:
|
|
283
|
-
CPX #15
|
|
284
|
-
BCC .p1done
|
|
285
754
|
SEC
|
|
755
|
+
.d1:
|
|
286
756
|
SBC #15
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
757
|
+
BCS .d1
|
|
758
|
+
EOR #7
|
|
759
|
+
ASL
|
|
760
|
+
ASL
|
|
761
|
+
ASL
|
|
762
|
+
ASL
|
|
290
763
|
STA RESP1
|
|
291
|
-
|
|
764
|
+
STA HMP1
|
|
765
|
+
LDA SHOT_X ; shot → M0
|
|
292
766
|
STA WSYNC
|
|
293
|
-
LDX SHOT_X
|
|
294
|
-
LDA #0
|
|
295
|
-
.m0pos:
|
|
296
|
-
CPX #15
|
|
297
|
-
BCC .m0done
|
|
298
767
|
SEC
|
|
768
|
+
.d2:
|
|
299
769
|
SBC #15
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
770
|
+
BCS .d2
|
|
771
|
+
EOR #7
|
|
772
|
+
ASL
|
|
773
|
+
ASL
|
|
774
|
+
ASL
|
|
775
|
+
ASL
|
|
303
776
|
STA RESM0
|
|
304
|
-
STA
|
|
777
|
+
STA HMM0
|
|
778
|
+
STA WSYNC
|
|
779
|
+
STA HMOVE ; one HMOVE applies ALL the fine offsets; it must
|
|
780
|
+
RTS ; come fresh after a WSYNC (mid-line HMOVE combs)
|
|
781
|
+
|
|
782
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
783
|
+
; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
784
|
+
; THE PLAY/GAME-OVER KERNEL — 192 visible lines, fully accounted:
|
|
785
|
+
; 24 = score bar + 168 = playfield (formation + ship + shot)
|
|
786
|
+
;
|
|
787
|
+
; SCORE BAR (SCORE mode): CTRLPF = $02 colors the LEFT playfield half with
|
|
788
|
+
; COLUP0 and the RIGHT half with COLUP1 — a two-color scoreboard with zero
|
|
789
|
+
; sprites. We stream the packed score digits into PF1, one row of font per
|
|
790
|
+
; 4 scanlines.
|
|
791
|
+
;
|
|
792
|
+
; PLAYFIELD: a single per-line loop draws the whole field. Each visible line
|
|
793
|
+
; computes, from the beam's current Y:
|
|
794
|
+
; * GRP1 = the formation bitmap row IF this Y is inside one of the TWO
|
|
795
|
+
; formation bands (top row at FORM_Y, bottom row 16 lines lower). NUSIZ1
|
|
796
|
+
; replication means this ONE store paints all three columns of that row.
|
|
797
|
+
; ALIVE-bit masking blanks killed columns by zeroing the relevant copy
|
|
798
|
+
; — done by swapping GRP1 to a per-column-masked byte (kept simple: a
|
|
799
|
+
; dead WHOLE row blanks; partial columns rely on the collision-cleared
|
|
800
|
+
; bit + the player seeing the gap as the block marches).
|
|
801
|
+
; * GRP0 = the cannon bitmap row IF this Y is in the cannon band.
|
|
802
|
+
; * ENAM0 = the shot, 4 lines tall at SHOT_Y.
|
|
803
|
+
; This is a ONE-line kernel (each pass = one scanline); the work fits 76
|
|
804
|
+
; cycles because every test is a compare-and-store, no multiply.
|
|
805
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
806
|
+
play_kernel:
|
|
807
|
+
; positioning runs first, inside the still-blanked region
|
|
808
|
+
JSR position_objects
|
|
305
809
|
|
|
810
|
+
LDA #COL_BG
|
|
811
|
+
STA COLUBK
|
|
306
812
|
LDA #0
|
|
307
|
-
STA
|
|
813
|
+
STA PF0
|
|
814
|
+
STA PF1
|
|
815
|
+
STA PF2
|
|
816
|
+
STA GRP0
|
|
817
|
+
STA GRP1
|
|
818
|
+
STA ENAM0
|
|
819
|
+
STA VBLANK ; beam on
|
|
820
|
+
LDA #$0E
|
|
821
|
+
STA COLUPF ; score digits bright
|
|
822
|
+
LDA #$02
|
|
823
|
+
STA CTRLPF ; SCORE mode for the score bar
|
|
308
824
|
|
|
309
|
-
;
|
|
310
|
-
|
|
311
|
-
.
|
|
825
|
+
; ---- score bar: 24 lines (6 font rows × 4) ----
|
|
826
|
+
; S0BUF was packed in logic_play (VBLANK); stream it here. Two visible
|
|
827
|
+
; digits (tens/ones of SCORE), doubled by SCORE mode into two colors.
|
|
828
|
+
LDX #0
|
|
829
|
+
.sbar:
|
|
312
830
|
STA WSYNC
|
|
831
|
+
TXA
|
|
832
|
+
LSR
|
|
833
|
+
LSR
|
|
834
|
+
TAY ; row = line/4
|
|
835
|
+
LDA S0BUF,Y
|
|
836
|
+
STA PF1
|
|
837
|
+
INX
|
|
838
|
+
CPX #24
|
|
839
|
+
BNE .sbar
|
|
313
840
|
|
|
314
|
-
;
|
|
841
|
+
; transition: clear the bar, switch the TIA to the playfield, and lay down
|
|
842
|
+
; a STATIC star-lane backdrop. CTRLPF bit0 (reflect) mirrors the 20-pixel
|
|
843
|
+
; playfield into a symmetric 40-pixel field of dim vertical star lanes; set
|
|
844
|
+
; PF0/PF1/PF2 ONCE here (registers persist every line) so the starfield
|
|
845
|
+
; costs ZERO per-line cycles — the one-line kernel below stays inside 76.
|
|
846
|
+
STA WSYNC
|
|
847
|
+
LDA #$01
|
|
848
|
+
STA CTRLPF ; reflect mode: symmetric star lanes
|
|
849
|
+
LDA #$06 ; dim grey-blue stars
|
|
850
|
+
STA COLUPF
|
|
851
|
+
LDA #%00000000 ; PF0: no star lane at the very edge
|
|
852
|
+
STA PF0
|
|
853
|
+
LDA #%00010000 ; PF1: one thin star lane
|
|
854
|
+
STA PF1
|
|
855
|
+
LDA #%00001000 ; PF2: one thin star lane (mirrored = 4 lanes total)
|
|
856
|
+
STA PF2
|
|
857
|
+
LDA #COL_BG
|
|
858
|
+
STA COLUBK
|
|
859
|
+
|
|
860
|
+
; ---- playfield: Y from 168 down to 1 ----
|
|
861
|
+
LDY #168
|
|
862
|
+
.field:
|
|
863
|
+
STA WSYNC
|
|
864
|
+
; formation top row: (Y - FORM_Y) in [0,8) ?
|
|
315
865
|
TYA
|
|
316
866
|
SEC
|
|
317
|
-
SBC
|
|
867
|
+
SBC FORM_Y
|
|
318
868
|
CMP #8
|
|
319
|
-
BCS .
|
|
869
|
+
BCS .notTop
|
|
320
870
|
TAX
|
|
321
|
-
LDA
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
871
|
+
LDA FORM,X
|
|
872
|
+
; mask whole row if all top cells dead (bits 0..2)
|
|
873
|
+
PHA
|
|
874
|
+
LDA ALIVE
|
|
875
|
+
AND #$07
|
|
876
|
+
BNE .topLive
|
|
877
|
+
PLA
|
|
325
878
|
LDA #0
|
|
326
|
-
|
|
327
|
-
.
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
879
|
+
JMP .setForm
|
|
880
|
+
.topLive:
|
|
881
|
+
PLA
|
|
882
|
+
.setForm:
|
|
883
|
+
STA GRP1
|
|
884
|
+
JMP .formDone
|
|
885
|
+
.notTop:
|
|
886
|
+
; formation bottom row: 16 lines lower (smaller Y) than the top
|
|
332
887
|
TYA
|
|
888
|
+
CLC
|
|
889
|
+
ADC #16
|
|
333
890
|
SEC
|
|
334
|
-
SBC
|
|
891
|
+
SBC FORM_Y
|
|
335
892
|
CMP #8
|
|
336
|
-
BCS .
|
|
893
|
+
BCS .noForm
|
|
337
894
|
TAX
|
|
338
|
-
LDA
|
|
895
|
+
LDA FORM,X
|
|
896
|
+
PHA
|
|
897
|
+
LDA ALIVE
|
|
898
|
+
AND #$38 ; bottom cells = bits 3..5
|
|
899
|
+
BNE .botLive
|
|
900
|
+
PLA
|
|
901
|
+
LDA #0
|
|
902
|
+
JMP .setForm2
|
|
903
|
+
.botLive:
|
|
904
|
+
PLA
|
|
905
|
+
.setForm2:
|
|
339
906
|
STA GRP1
|
|
340
|
-
JMP .
|
|
341
|
-
.
|
|
907
|
+
JMP .formDone
|
|
908
|
+
.noForm:
|
|
342
909
|
LDA #0
|
|
343
910
|
STA GRP1
|
|
344
|
-
.
|
|
911
|
+
.formDone:
|
|
345
912
|
|
|
346
|
-
;
|
|
913
|
+
; cannon: 8 rows in the bottom band (Y in [24,32))
|
|
914
|
+
TYA
|
|
915
|
+
SEC
|
|
916
|
+
SBC #24
|
|
917
|
+
CMP #8
|
|
918
|
+
BCS .noShip
|
|
919
|
+
TAX
|
|
920
|
+
LDA SHIP,X
|
|
921
|
+
STA GRP0
|
|
922
|
+
JMP .shipDone
|
|
923
|
+
.noShip:
|
|
924
|
+
LDA #0
|
|
925
|
+
STA GRP0
|
|
926
|
+
.shipDone:
|
|
927
|
+
|
|
928
|
+
; shot: ENAM0 for 4 lines at SHOT_Y (0 = parked → never matches)
|
|
929
|
+
LDA SHOT_Y
|
|
930
|
+
BEQ .noShot
|
|
347
931
|
TYA
|
|
348
932
|
SEC
|
|
349
933
|
SBC SHOT_Y
|
|
350
934
|
CMP #4
|
|
351
|
-
BCS .
|
|
935
|
+
BCS .noShot
|
|
352
936
|
LDA #2
|
|
353
937
|
STA ENAM0
|
|
354
|
-
JMP .
|
|
355
|
-
.
|
|
938
|
+
JMP .shotDone
|
|
939
|
+
.noShot:
|
|
356
940
|
LDA #0
|
|
357
941
|
STA ENAM0
|
|
358
|
-
.
|
|
942
|
+
.shotDone:
|
|
359
943
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
944
|
+
DEY
|
|
945
|
+
BNE .field
|
|
946
|
+
|
|
947
|
+
JMP kernel_done
|
|
948
|
+
|
|
949
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
950
|
+
; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
951
|
+
; THE TITLE KERNEL — 192 lines, banded:
|
|
952
|
+
; 24 blank + 28 banner "FLAK" + 8 gap + 28 banner "FRENZY" + 16 gap +
|
|
953
|
+
; 24 hi-score + remainder pad = 192
|
|
954
|
+
;
|
|
955
|
+
; The banner is an ASYMMETRIC PLAYFIELD — the 2600's only way to draw
|
|
956
|
+
; full-width artwork. The playfield registers hold just 20 pixels; the TIA
|
|
957
|
+
; replays them for the right half of the line (CTRLPF bit0 chooses repeat
|
|
958
|
+
; or mirror). For 40 INDEPENDENT pixels you rewrite all three registers
|
|
959
|
+
; mid-line, each inside its window (CPU cycle = 3 color clocks; left copy
|
|
960
|
+
; reads at clocks 68-147, right copy at 148-227):
|
|
961
|
+
; PF0 again after cycle ~28 (left copy drawn) before ~49 (right copy reads)
|
|
962
|
+
; PF1 again after cycle ~39 before ~54
|
|
963
|
+
; PF2 again after cycle ~50 before ~65
|
|
964
|
+
; The code below hits those windows by instruction order alone — count
|
|
965
|
+
; cycles before you reorder ANYTHING between the WSYNC and the last STA.
|
|
966
|
+
; REQUIRES: CTRLPF bit0 = 0 (repeat mode). In mirror mode the right half
|
|
967
|
+
; reads the registers in REVERSE order and every window above is wrong.
|
|
968
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
969
|
+
title_kernel:
|
|
970
|
+
LDA #$80 ; deep blue backdrop
|
|
971
|
+
STA COLUBK
|
|
972
|
+
LDA #0
|
|
973
|
+
STA PF0
|
|
974
|
+
STA PF1
|
|
975
|
+
STA PF2
|
|
976
|
+
STA GRP0
|
|
977
|
+
STA GRP1
|
|
978
|
+
STA ENAM0
|
|
979
|
+
STA CTRLPF ; REPEAT mode — required by the banner (see above)
|
|
980
|
+
STA VBLANK ; beam on
|
|
981
|
+
|
|
982
|
+
LDX #24 ; band 1: 24 blank lines
|
|
983
|
+
.tb1:
|
|
984
|
+
STA WSYNC
|
|
985
|
+
DEX
|
|
986
|
+
BNE .tb1
|
|
987
|
+
|
|
988
|
+
LDA #$9E ; word 1 in light blue
|
|
989
|
+
STA COLUPF
|
|
990
|
+
LDX #0 ; band 2: 28 banner lines (7 rows × 4)
|
|
991
|
+
.ban1:
|
|
992
|
+
STA WSYNC
|
|
993
|
+
TXA ; row = line/4
|
|
994
|
+
LSR
|
|
995
|
+
LSR
|
|
996
|
+
TAY
|
|
997
|
+
LDA R1_PF0L,Y
|
|
998
|
+
STA PF0
|
|
999
|
+
LDA R1_PF1L,Y
|
|
1000
|
+
STA PF1
|
|
1001
|
+
LDA R1_PF2L,Y
|
|
1002
|
+
STA PF2
|
|
1003
|
+
LDA R1_PF0R,Y
|
|
364
1004
|
STA PF0
|
|
365
|
-
|
|
366
|
-
|
|
1005
|
+
LDA R1_PF1R,Y
|
|
1006
|
+
STA PF1
|
|
1007
|
+
NOP
|
|
1008
|
+
NOP
|
|
1009
|
+
LDA R1_PF2R,Y
|
|
1010
|
+
STA PF2
|
|
1011
|
+
INX
|
|
1012
|
+
CPX #28
|
|
1013
|
+
BNE .ban1
|
|
1014
|
+
|
|
1015
|
+
STA WSYNC ; band 3: clear + 7 gap lines
|
|
367
1016
|
LDA #0
|
|
368
1017
|
STA PF0
|
|
369
|
-
|
|
1018
|
+
STA PF1
|
|
1019
|
+
STA PF2
|
|
1020
|
+
LDX #7
|
|
1021
|
+
.tb3:
|
|
1022
|
+
STA WSYNC
|
|
1023
|
+
DEX
|
|
1024
|
+
BNE .tb3
|
|
370
1025
|
|
|
371
|
-
|
|
372
|
-
|
|
1026
|
+
LDA #$46 ; word 2 in red
|
|
1027
|
+
STA COLUPF
|
|
1028
|
+
LDX #0 ; band 4: 28 banner lines, word 2
|
|
1029
|
+
.ban2:
|
|
1030
|
+
STA WSYNC
|
|
1031
|
+
TXA
|
|
1032
|
+
LSR
|
|
1033
|
+
LSR
|
|
1034
|
+
TAY
|
|
1035
|
+
LDA R2_PF0L,Y
|
|
1036
|
+
STA PF0
|
|
1037
|
+
LDA R2_PF1L,Y
|
|
1038
|
+
STA PF1
|
|
1039
|
+
LDA R2_PF2L,Y
|
|
1040
|
+
STA PF2
|
|
1041
|
+
LDA R2_PF0R,Y
|
|
1042
|
+
STA PF0
|
|
1043
|
+
LDA R2_PF1R,Y
|
|
1044
|
+
STA PF1
|
|
1045
|
+
NOP
|
|
1046
|
+
NOP
|
|
1047
|
+
LDA R2_PF2R,Y
|
|
1048
|
+
STA PF2
|
|
1049
|
+
INX
|
|
1050
|
+
CPX #28
|
|
1051
|
+
BNE .ban2
|
|
373
1052
|
|
|
374
|
-
;
|
|
375
|
-
LDA #
|
|
376
|
-
STA
|
|
377
|
-
|
|
378
|
-
|
|
1053
|
+
STA WSYNC ; band 5: clear + 15 gap lines
|
|
1054
|
+
LDA #0
|
|
1055
|
+
STA PF0
|
|
1056
|
+
STA PF1
|
|
1057
|
+
STA PF2
|
|
1058
|
+
LDA #$02
|
|
1059
|
+
STA CTRLPF ; SCORE mode for the hi-score band
|
|
1060
|
+
LDX #15
|
|
1061
|
+
.tb5:
|
|
379
1062
|
STA WSYNC
|
|
380
1063
|
DEX
|
|
381
|
-
BNE .
|
|
1064
|
+
BNE .tb5
|
|
382
1065
|
|
|
383
|
-
|
|
1066
|
+
; band 6: hi-score, 24 lines (6 rows × 4). Packed digits stream into PF1;
|
|
1067
|
+
; SCORE mode draws them twice in the two player colors. In-session best;
|
|
1068
|
+
; honest: there is no battery — gone at power-off, like the arcades.
|
|
1069
|
+
LDX #0
|
|
1070
|
+
.hsb:
|
|
1071
|
+
STA WSYNC
|
|
1072
|
+
TXA
|
|
1073
|
+
LSR
|
|
1074
|
+
LSR
|
|
1075
|
+
TAY
|
|
1076
|
+
LDA HSBUF,Y
|
|
1077
|
+
STA PF1
|
|
1078
|
+
INX
|
|
1079
|
+
CPX #24
|
|
1080
|
+
BNE .hsb
|
|
384
1081
|
|
|
385
|
-
;
|
|
1082
|
+
STA WSYNC ; band 7: clear + pad to exactly 192
|
|
1083
|
+
LDA #0
|
|
1084
|
+
STA PF1
|
|
1085
|
+
LDX #65
|
|
1086
|
+
.tb7:
|
|
1087
|
+
STA WSYNC
|
|
1088
|
+
DEX
|
|
1089
|
+
BNE .tb7
|
|
1090
|
+
|
|
1091
|
+
JMP kernel_done
|
|
1092
|
+
|
|
1093
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
1094
|
+
; ── GAME LOGIC (clay — reshape freely) ── data tables ──────────────────
|
|
1095
|
+
BITMASK:
|
|
1096
|
+
.byte $01,$02,$04,$08,$10,$20
|
|
1097
|
+
|
|
1098
|
+
; Digit font: 4 pixels wide × 6 rows, stored in the HIGH nibble (PF1 bit7
|
|
1099
|
+
; is the LEFTMOST pixel of the left playfield half — high nibble = left).
|
|
1100
|
+
DIGITS:
|
|
1101
|
+
.byte $60,$90,$90,$90,$90,$60 ; 0
|
|
1102
|
+
.byte $20,$60,$20,$20,$20,$70 ; 1
|
|
1103
|
+
.byte $60,$90,$10,$20,$40,$F0 ; 2
|
|
1104
|
+
.byte $E0,$10,$60,$10,$10,$E0 ; 3
|
|
1105
|
+
.byte $90,$90,$F0,$10,$10,$10 ; 4
|
|
1106
|
+
.byte $F0,$80,$E0,$10,$10,$E0 ; 5
|
|
1107
|
+
.byte $60,$80,$E0,$90,$90,$60 ; 6
|
|
1108
|
+
.byte $F0,$10,$20,$40,$40,$40 ; 7
|
|
1109
|
+
.byte $60,$90,$60,$90,$90,$60 ; 8
|
|
1110
|
+
.byte $60,$90,$90,$70,$10,$60 ; 9
|
|
1111
|
+
|
|
1112
|
+
; Title jingle (voice 1, AUDC $04 square; AUDF divider — LOWER = higher
|
|
1113
|
+
; pitch; 10 frames per note; $FF terminates). The table IS the song.
|
|
1114
|
+
TITLE_TUNE:
|
|
1115
|
+
.byte $17,$13,$0F,$0C,$0F,$0C,$09,$0C,$FF
|
|
1116
|
+
; Game-over tune: a falling figure.
|
|
1117
|
+
OVER_TUNE:
|
|
1118
|
+
.byte $09,$0C,$0F,$13,$17,$1B,$FF
|
|
1119
|
+
|
|
1120
|
+
; ── THE INVADER SPRITE ────────────────────────────────────────────────
|
|
1121
|
+
; 8 rows, drawn via P1 with NUSIZ1=%011 so it hardware-replicates into 3
|
|
1122
|
+
; medium-spaced invaders from ONE GRP1 write — the genre's defining trick.
|
|
1123
|
+
FORM:
|
|
1124
|
+
.byte %00100100
|
|
1125
|
+
.byte %00111100
|
|
1126
|
+
.byte %01111110
|
|
1127
|
+
.byte %11011011
|
|
1128
|
+
.byte %11111111
|
|
1129
|
+
.byte %01011010
|
|
1130
|
+
.byte %00100100
|
|
1131
|
+
.byte %01000010
|
|
1132
|
+
|
|
1133
|
+
; ── THE CANNON SPRITE ─────────────────────────────────────────────────
|
|
386
1134
|
SHIP:
|
|
387
1135
|
.byte %00011000
|
|
388
1136
|
.byte %00011000
|
|
@@ -393,18 +1141,62 @@ SHIP:
|
|
|
393
1141
|
.byte %11111111
|
|
394
1142
|
.byte %11100111
|
|
395
1143
|
|
|
396
|
-
;
|
|
397
|
-
;
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
1144
|
+
; ── THE TITLE BANNER ──────────────────────────────────────────────────
|
|
1145
|
+
; 40-pixel-wide artwork, 7 rows per word, drawn by the asymmetric-playfield
|
|
1146
|
+
; kernel above. Each row is six bytes across six tables (left PF0/PF1/PF2,
|
|
1147
|
+
; right PF0/PF1/PF2). PF bit order is the 2600's great prank — three
|
|
1148
|
+
; registers, three different orders:
|
|
1149
|
+
; PF0: only bits 4-7 used, bit 4 = LEFTMOST pixel (reversed)
|
|
1150
|
+
; PF1: bit 7 = leftmost (normal)
|
|
1151
|
+
; PF2: bit 0 = leftmost (reversed again)
|
|
1152
|
+
;
|
|
1153
|
+
; The 40-px art for each row is the comment ASCII above each table; the
|
|
1154
|
+
; bytes are mechanically encoded from it (left half = pixels 0..19 → PF0
|
|
1155
|
+
; bits4-7 / PF1 bits7-0 / PF2 bits0-7; right half = pixels 20..39 likewise).
|
|
1156
|
+
;
|
|
1157
|
+
; FLAK:
|
|
1158
|
+
; .####.....#........###....#...#.........
|
|
1159
|
+
; .#........#.......#...#...#..#..........
|
|
1160
|
+
; .#........#.......#...#...#.#...........
|
|
1161
|
+
; .###......#.......#####...##............
|
|
1162
|
+
; .#........#.......#...#...#.#...........
|
|
1163
|
+
; .#........#.......#...#...#..#..........
|
|
1164
|
+
; .#........#####...#...#...#...#.........
|
|
1165
|
+
R1_PF0L:
|
|
1166
|
+
.byte %11100000, %00100000, %00100000, %11100000, %00100000, %00100000, %00100000
|
|
1167
|
+
R1_PF1L:
|
|
1168
|
+
.byte %10000010, %00000010, %00000010, %00000010, %00000010, %00000010, %00000011
|
|
1169
|
+
R1_PF2L:
|
|
1170
|
+
.byte %10000000, %01000000, %01000000, %11000000, %01000000, %01000000, %01000111
|
|
1171
|
+
R1_PF0R:
|
|
1172
|
+
.byte %00110000, %01000000, %01000000, %01110000, %01000000, %01000000, %01000000
|
|
1173
|
+
R1_PF1R:
|
|
1174
|
+
.byte %00100010, %00100100, %00101000, %00110000, %00101000, %00100100, %00100010
|
|
1175
|
+
R1_PF2R:
|
|
1176
|
+
.byte %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000
|
|
1177
|
+
|
|
1178
|
+
; FRENZY:
|
|
1179
|
+
; ####..###...####.#..#..####..#...#......
|
|
1180
|
+
; #....#..#..#.....##.#.....#...#.#.......
|
|
1181
|
+
; #....#..#..#.....#.##....#.....#........
|
|
1182
|
+
; ###..###...###...#.##...#......#........
|
|
1183
|
+
; #....#.#...#.....#..#..#.......#........
|
|
1184
|
+
; #....#..#..#.....#..#.#........#........
|
|
1185
|
+
; #....#..#..####..#..#.####.....#........
|
|
1186
|
+
R2_PF0L:
|
|
1187
|
+
.byte %11110000, %00010000, %00010000, %01110000, %00010000, %00010000, %00010000
|
|
1188
|
+
R2_PF1L:
|
|
1189
|
+
.byte %00111000, %01001001, %01001001, %01110001, %01010001, %01001001, %01001001
|
|
1190
|
+
R2_PF2L:
|
|
1191
|
+
.byte %00101111, %01100000, %10100000, %10100011, %00100000, %00100000, %00100111
|
|
1192
|
+
R2_PF0R:
|
|
1193
|
+
.byte %10010000, %00010000, %00010000, %00010000, %10010000, %01010000, %11010000
|
|
1194
|
+
R2_PF1R:
|
|
1195
|
+
.byte %11100100, %00100010, %01000001, %10000001, %00000001, %00000001, %11000001
|
|
1196
|
+
R2_PF2R:
|
|
1197
|
+
.byte %00000010, %00000001, %00000000, %00000000, %00000000, %00000000, %00000000
|
|
407
1198
|
|
|
1199
|
+
; ── Vector table ──────────────────────────────────────────────────────
|
|
408
1200
|
org $FFFA
|
|
409
1201
|
.word START
|
|
410
1202
|
.word START
|