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,46 +1,81 @@
|
|
|
1
|
-
; ── platformer.asm — Atari 2600
|
|
1
|
+
; ── platformer.asm — PERCH PATROL — Atari 2600 single-screen platformer ──────
|
|
2
2
|
;
|
|
3
|
-
;
|
|
4
|
-
;
|
|
5
|
-
;
|
|
6
|
-
;
|
|
7
|
-
;
|
|
8
|
-
;
|
|
9
|
-
;
|
|
3
|
+
; A COMPLETE, working game — drawn title screen, a single-SCREEN platformer
|
|
4
|
+
; (you hop a P0 hero across PLAYFIELD ledges, grab the bouncing coin, dodge
|
|
5
|
+
; the patrolling spike), score + in-session hi-score, TIA sound effects + a
|
|
6
|
+
; title jingle, game-over with auto-return to the title, and the 2600's
|
|
7
|
+
; signature feature: THE WHOLE MACHINE. There is no framebuffer, no tilemap,
|
|
8
|
+
; no OS — every visible scanline below is composed live by racing the beam,
|
|
9
|
+
; and this file teaches the platformer's load-bearing TIA tricks while doing
|
|
10
|
+
; it:
|
|
10
11
|
;
|
|
11
|
-
;
|
|
12
|
-
;
|
|
13
|
-
;
|
|
14
|
-
;
|
|
15
|
-
;
|
|
12
|
+
; 1. PLAYFIELD-AS-LEVEL (the ledges + floor + pit) — the 2600 has NO
|
|
13
|
+
; tilemap and NO hardware scroll, so the level IS the playfield. PF0/
|
|
14
|
+
; PF1/PF2 are reloaded per scanline band from a per-row table; a lit PF
|
|
15
|
+
; pixel is solid ground, a gap is a pit. This is exactly how the era's
|
|
16
|
+
; single-screen platformers drew their arenas: the honest 2600
|
|
17
|
+
; platformer is a FIXED screen (those games flip whole new screens at
|
|
18
|
+
; the edges — they never scroll), so this one is too.
|
|
19
|
+
; 2. CODE COLLISION, NOT TIA LATCHES (land-on-ledge) — a shooter reads the
|
|
20
|
+
; TIA's hardware overlap latch (FLAK FRENZY does), but a platformer must
|
|
21
|
+
; know WHICH surface it's standing on to stop the fall there. So ground
|
|
22
|
+
; contact is tested in CODE: sample the level's PF bit directly under
|
|
23
|
+
; the hero's column at his feet's Y. (TIA latches are still used — for
|
|
24
|
+
; the coin pickup and the spike death, where "did I touch it" is enough.)
|
|
25
|
+
; 3. RESP/HMOVE BEAM POSITIONING (the SBC-#15 idiom) — there is no sprite X
|
|
26
|
+
; register; you strobe RESPx/RESBL/RESM0 WHERE THE BEAM IS, then nudge
|
|
27
|
+
; ±7px with HMOVE. Hero P0, coin BL and spike M0 are positioned this way
|
|
28
|
+
; every frame, inside the timed VBLANK window.
|
|
29
|
+
; 4. TIM64T/INTIM FRAME TIMING — set the RIOT timer for VBLANK/overscan and
|
|
30
|
+
; let it absorb however much the game logic costs, instead of hand-
|
|
31
|
+
; counting WSYNCs (which rolls the picture the moment logic grows).
|
|
16
32
|
;
|
|
17
|
-
;
|
|
18
|
-
;
|
|
19
|
-
;
|
|
20
|
-
; (
|
|
21
|
-
;
|
|
22
|
-
; (
|
|
23
|
-
; WHICH surface to stand on and TIA only gives a yes/no overlap.
|
|
24
|
-
; * Walk left/right with the joystick; you can't walk off the screen.
|
|
33
|
+
; THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
|
|
34
|
+
; very different one. The markers tell you what's what:
|
|
35
|
+
; HARDWARE IDIOM (load-bearing) — cycle-counted / footgun-dodging code;
|
|
36
|
+
; reshape your gameplay around it (see TROUBLESHOOTING before changing).
|
|
37
|
+
; GAME LOGIC (clay) — physics, level layout, scoring, tuning, art: reshape
|
|
38
|
+
; freely (the LEVEL table near the bottom is pure clay — redraw it).
|
|
25
39
|
;
|
|
26
|
-
;
|
|
27
|
-
;
|
|
28
|
-
;
|
|
29
|
-
;
|
|
40
|
+
; GAME_TITLE: on the 2600 a title is DRAWN, not printed — there is no font
|
|
41
|
+
; hardware. The PERCH/PATROL banner bitmaps near the bottom of this file ARE
|
|
42
|
+
; the title; redraw them for your game (the comment above each table shows
|
|
43
|
+
; the 40-pixel artwork and the PF0/PF1/PF2 bit-order encoding).
|
|
30
44
|
;
|
|
31
|
-
;
|
|
32
|
-
;
|
|
33
|
-
;
|
|
34
|
-
;
|
|
45
|
+
; CONTROLS (documented for players and for the fork README):
|
|
46
|
+
; Title: fire on JOYSTICK 0 (or console RESET) starts the game
|
|
47
|
+
; Play: joystick 0 LEFT/RIGHT walks the hero; fire (or UP) JUMPS when
|
|
48
|
+
; standing on a ledge; console RESET returns to the title
|
|
49
|
+
; Grab the bouncing coin to score; touch the patrolling spike and it's
|
|
50
|
+
; game over. Your best SCORE this session is shown on the title screen.
|
|
51
|
+
;
|
|
52
|
+
; PLAYERS — 1P, honest. The 2600 has two joystick ports, but this single-
|
|
53
|
+
; screen kernel is already spending its scanline budget on the PLAYFIELD
|
|
54
|
+
; level (per-row PF reload), the hero (P0), the coin (BL) and the spike
|
|
55
|
+
; (M0). A second human hero would need its own positioned object competing
|
|
56
|
+
; for the SAME 76-cycle lines the level reload already fills. To add 2P
|
|
57
|
+
; alternating TURNS instead — cheap, no extra kernel objects — keep a second
|
|
58
|
+
; score/lives pair and swap on death; left as an exercise.
|
|
59
|
+
;
|
|
60
|
+
; HI-SCORE HONESTY: real 2600 cartridges had NO battery, NO SRAM, NO
|
|
61
|
+
; persistence of any kind. The hi-score here lives in RIOT RAM ($A0) and
|
|
62
|
+
; survives game → title cycles only WITHIN one power-on session — exactly
|
|
63
|
+
; like the arcade machines of the era. Power off and it is gone. Do not
|
|
64
|
+
; fake an EEPROM; state it honestly in your fork too.
|
|
65
|
+
;
|
|
66
|
+
; NTSC frame: 3 VSYNC + 37 VBLANK + 192 visible + 30 overscan = 262 lines.
|
|
35
67
|
|
|
36
68
|
processor 6502
|
|
37
69
|
org $F000
|
|
38
70
|
|
|
71
|
+
; ── TIA write registers ───────────────────────────────────────────────
|
|
39
72
|
VSYNC = $00
|
|
40
73
|
VBLANK = $01
|
|
41
74
|
WSYNC = $02
|
|
42
75
|
NUSIZ0 = $04
|
|
76
|
+
NUSIZ1 = $05
|
|
43
77
|
COLUP0 = $06
|
|
78
|
+
COLUP1 = $07
|
|
44
79
|
COLUPF = $08
|
|
45
80
|
COLUBK = $09
|
|
46
81
|
CTRLPF = $0A
|
|
@@ -48,49 +83,95 @@ PF0 = $0D
|
|
|
48
83
|
PF1 = $0E
|
|
49
84
|
PF2 = $0F
|
|
50
85
|
RESP0 = $10
|
|
86
|
+
RESBL = $14
|
|
87
|
+
RESM0 = $12
|
|
51
88
|
GRP0 = $1B
|
|
89
|
+
GRP1 = $1C
|
|
90
|
+
ENABL = $1F
|
|
91
|
+
ENAM0 = $1D
|
|
52
92
|
HMP0 = $20
|
|
93
|
+
HMBL = $24
|
|
94
|
+
HMM0 = $22
|
|
53
95
|
HMOVE = $2A
|
|
54
96
|
HMCLR = $2B
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
; TIA audio
|
|
97
|
+
CXCLR = $2C
|
|
98
|
+
; ── TIA audio ─────────────────────────────────────────────────────────
|
|
58
99
|
AUDC0 = $15
|
|
100
|
+
AUDC1 = $16
|
|
59
101
|
AUDF0 = $17
|
|
102
|
+
AUDF1 = $18
|
|
60
103
|
AUDV0 = $19
|
|
104
|
+
AUDV1 = $1A
|
|
105
|
+
; ── TIA READ registers (separate read map — the same addresses as some
|
|
106
|
+
; write strobes; e.g. CXP0FB reads $02 while STA $02 strobes WSYNC) ─────
|
|
107
|
+
CXP0FB = $02 ; bit6 = player0 / ball collision (latched)
|
|
108
|
+
CXM0P = $00 ; bit7 = missile0 / player0 collision (latched)
|
|
109
|
+
INPT4 = $0C ; joystick 0 fire (bit7, ACTIVE LOW)
|
|
110
|
+
; ── RIOT ──────────────────────────────────────────────────────────────
|
|
111
|
+
SWCHA = $280 ; joysticks: P0 = high nibble, P1 = LOW nibble
|
|
112
|
+
SWCHB = $282 ; console: bit0 RESET, bit1 SELECT (ACTIVE LOW)
|
|
113
|
+
INTIM = $284 ; timer read
|
|
114
|
+
TIM64T = $296 ; timer set, 64-cycle ticks
|
|
115
|
+
|
|
116
|
+
; ── Zero-page state (the 2600's ENTIRE RAM is $80-$FF — 128 bytes; in
|
|
117
|
+
; core memory dumps system_ram offset 0 = $80) ────────────────────────
|
|
118
|
+
STATE = $80 ; 0 = title, 1 = play, 2 = game over
|
|
119
|
+
P_X = $81 ; hero X column (visible 0..159; kept 12..148)
|
|
120
|
+
P_Y = $82 ; hero FEET scanline in level space (0=floor .. up)
|
|
121
|
+
P_VY = $83 ; vertical velocity, signed (jump up = +, fall = -)
|
|
122
|
+
ON_GND = $84 ; 1 = standing on a ledge/floor this frame, 0 = airborne
|
|
123
|
+
COIN_X = $85 ; coin (BL) X column
|
|
124
|
+
COIN_Y = $86 ; coin Y in level space
|
|
125
|
+
COIN_VY = $87 ; coin bounce velocity (signed)
|
|
126
|
+
SPK_X = $88 ; spike (M0) X column
|
|
127
|
+
SPK_Y = $89 ; spike Y in level space (which ledge it patrols)
|
|
128
|
+
SPK_DIR = $8A ; +1 marching right, $FF marching left
|
|
129
|
+
SCORE = $8B ; current score, BCD (digit nibbles fall out free)
|
|
130
|
+
SCORE_HI = $8C ; current score high byte, BCD
|
|
131
|
+
FRAME = $8D
|
|
132
|
+
SFX_LEFT = $8E ; frames remaining on the voice-0 sound effect
|
|
133
|
+
TUNE_SEL = $8F ; 0 = title jingle, 1 = game-over tune (voice 1)
|
|
134
|
+
TUNE_POS = $90
|
|
135
|
+
TUNE_LEFT = $91 ; frames left on current jingle note (0 = silent)
|
|
136
|
+
OVER_T = $92 ; game-over auto-return-to-title countdown
|
|
137
|
+
SWCHB_PRV = $93 ; previous SWCHB for RESET edge detect
|
|
138
|
+
FIRE_PRV = $94 ; previous fire level (bit7) for fire-edge detect
|
|
139
|
+
EDGEB = $95 ; this frame's RESET press-edge (bit0)
|
|
140
|
+
FIRE_EDG = $96 ; this frame's fire press-edge (bit7)
|
|
141
|
+
TMP = $97
|
|
142
|
+
TMP2 = $98
|
|
143
|
+
P_ROW = $99 ; hero's current level ROW (Y/16) — picked in logic,
|
|
144
|
+
; reused by the kernel to draw the hero band
|
|
145
|
+
COIN_ROW = $9A ; coin's level row (for the kernel)
|
|
146
|
+
SPK_ROW = $9B ; spike's level row (for the kernel)
|
|
147
|
+
GFXIDX = $9C ; hero sprite frame base (0 = idle, 6 = walk)
|
|
148
|
+
SCORE_HSV = $A0 ; SESSION hi-score (BCD low byte). RAM only — real
|
|
149
|
+
SCORE_HSH = $A1 ; 2600 carts have no battery; honest by design.
|
|
150
|
+
S0BUF = $A2 ; 6 rows: packed score digits for the play kernel
|
|
151
|
+
HSBUF = $A8 ; 6 rows: hi-score, packed (for the title kernel)
|
|
152
|
+
SCRATCH = $AE ; 6 bytes general kernel/packer scratch
|
|
153
|
+
|
|
154
|
+
; ── level geometry constants (clay — change to reshape the arena) ──────
|
|
155
|
+
; The level is NROWS bands of 16 scanlines. Each row carries a PF0/PF1/PF2
|
|
156
|
+
; triple (LEVEL table) = where the ground/ledges are on that band. P_Y is
|
|
157
|
+
; the hero's FEET measured 0 (bottom of the arena) upward; row = P_Y/16.
|
|
158
|
+
NROWS = 9 ; 9 rows × 16 = 144 visible level lines (+24 score bar)
|
|
159
|
+
ROWH = 16
|
|
160
|
+
GRAVITY = 1 ; downward pull per frame
|
|
161
|
+
JUMP_VY = 9 ; initial jump impulse
|
|
162
|
+
WALK_LO = 12 ; hero X clamp
|
|
163
|
+
WALK_HI = 148
|
|
61
164
|
|
|
62
|
-
;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
LANDY = $88 ; the Y we snap to when we land
|
|
73
|
-
; PFROW: 96-byte playfield row buffer (one entry per 2-line kernel row),
|
|
74
|
-
; built ONCE at boot from the platform table. $89..$E8. Stack ($FF down)
|
|
75
|
-
; has $E9..$FF free (23 bytes) — ample, since the only JSR is the one-shot
|
|
76
|
-
; build_pfrow and the kernel itself calls nothing.
|
|
77
|
-
PFROW = $89
|
|
78
|
-
|
|
79
|
-
; Player height (sprite rows).
|
|
80
|
-
PH = 8
|
|
81
|
-
|
|
82
|
-
; ── Platform table ────────────────────────────────────────────────────
|
|
83
|
-
; Each platform = (top-band scanline Y, x-left, x-right). The visuals are
|
|
84
|
-
; FULL-WIDTH horizontal bars (cheap + reads cleanly), so the x-spans below
|
|
85
|
-
; are the whole screen and the land-on-top test lets you stand anywhere on
|
|
86
|
-
; a platform. Beam Y counts 192(top)->1(bottom): a LARGER Y value sits
|
|
87
|
-
; HIGHER on the screen, so PLAT_Y=18 is the bottom FLOOR and PLAT_Y=150 is
|
|
88
|
-
; the top ledge.
|
|
89
|
-
; floor : Y=18 (bottom, full width)
|
|
90
|
-
; ledge : Y=70
|
|
91
|
-
; ledge : Y=110
|
|
92
|
-
; ledge : Y=150 (highest)
|
|
93
|
-
NUM_PLAT = 4
|
|
165
|
+
HEROH = 6 ; hero sprite height in scanlines
|
|
166
|
+
COIN_FLR = 8 ; coin's bounce floor (level Y)
|
|
167
|
+
COIN_CEIL = 124 ; coin's bounce ceiling
|
|
168
|
+
|
|
169
|
+
COL_SKY = $00 ; black space behind the arena
|
|
170
|
+
COL_HERO = $3A ; warm yellow hero
|
|
171
|
+
COL_LEDGE = $C6 ; green ledges/floor
|
|
172
|
+
COL_COIN = $1E ; bright yellow coin
|
|
173
|
+
COL_SPIKE = $44 ; red spike
|
|
174
|
+
COL_HUD = $0E ; white score digits
|
|
94
175
|
|
|
95
176
|
START:
|
|
96
177
|
SEI
|
|
@@ -99,370 +180,1182 @@ START:
|
|
|
99
180
|
TXS
|
|
100
181
|
LDA #0
|
|
101
182
|
.clr:
|
|
102
|
-
STA $00,X
|
|
103
|
-
DEX
|
|
104
|
-
BNE .clr
|
|
105
|
-
|
|
106
|
-
; Start the player standing on the floor.
|
|
107
|
-
LDA #76
|
|
108
|
-
STA P_X
|
|
109
|
-
LDA #26 ; just above the floor band (floor top = 18, +PH)
|
|
110
|
-
STA P_Y
|
|
111
|
-
LDA #1
|
|
112
|
-
STA ON_GND
|
|
183
|
+
STA $00,X ; clears ALL of $00-$FF: zero page RAM AND the TIA
|
|
184
|
+
DEX ; write registers (GRP/ENAxx/HMxx/audio all silenced
|
|
185
|
+
BNE .clr ; — the standard 2600 power-on hygiene)
|
|
113
186
|
|
|
114
|
-
;
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
LDA #$1E ; yellow player
|
|
187
|
+
; Fixed identity colors (the kernels rewrite COLUPF per band, but the
|
|
188
|
+
; object colors are constant all session).
|
|
189
|
+
LDA #COL_HERO
|
|
118
190
|
STA COLUP0
|
|
119
|
-
|
|
120
|
-
|
|
191
|
+
; single-width hero. NUSIZ left at 0 = single objects everywhere.
|
|
192
|
+
LDA #%00000000
|
|
193
|
+
STA NUSIZ0
|
|
194
|
+
STA NUSIZ1
|
|
121
195
|
|
|
122
|
-
|
|
123
|
-
; you switch ledges to partial patterns later they mirror symmetrically).
|
|
124
|
-
LDA #%00000001
|
|
125
|
-
STA CTRLPF
|
|
126
|
-
|
|
127
|
-
; Build the static playfield row buffer ONCE (platforms never move).
|
|
128
|
-
JSR build_pfrow
|
|
129
|
-
|
|
130
|
-
; Boot chime.
|
|
131
|
-
LDA #$04
|
|
132
|
-
STA AUDC0
|
|
133
|
-
LDA #$0C
|
|
134
|
-
STA AUDF0
|
|
135
|
-
LDA #$0F
|
|
136
|
-
STA AUDV0
|
|
137
|
-
LDA #15
|
|
138
|
-
STA SFX_LEFT
|
|
196
|
+
JSR enter_title
|
|
139
197
|
|
|
198
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
199
|
+
; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
200
|
+
; THE FRAME LOOP. 262 scanlines, every frame, forever. VBLANK and overscan
|
|
201
|
+
; are timed with the RIOT timer (TIM64T) instead of counted WSYNCs: set the
|
|
202
|
+
; timer, run however much game logic the state needs, then spin on INTIM.
|
|
203
|
+
; This is how shipped 2600 games did it, and it kills the classic homebrew
|
|
204
|
+
; bug class where adding one branch to the game logic emits a 263rd line
|
|
205
|
+
; and the TV loses vsync (rolling picture). The VISIBLE 192 lines are still
|
|
206
|
+
; counted exactly — every STA WSYNC below is one scanline, and each state's
|
|
207
|
+
; kernel accounts for all 192.
|
|
208
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
140
209
|
MAIN:
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
; ── VSYNC (3 lines) ──
|
|
210
|
+
; VSYNC: 3 lines
|
|
144
211
|
LDA #2
|
|
212
|
+
STA VBLANK
|
|
145
213
|
STA VSYNC
|
|
146
214
|
STA WSYNC
|
|
147
215
|
STA WSYNC
|
|
148
216
|
STA WSYNC
|
|
149
217
|
LDA #0
|
|
150
218
|
STA VSYNC
|
|
219
|
+
; 37 lines of VBLANK = 2812 cycles ≈ 43 × 64-cycle timer ticks.
|
|
220
|
+
LDA #43
|
|
221
|
+
STA TIM64T
|
|
151
222
|
|
|
152
|
-
;
|
|
223
|
+
JSR frame_logic ; all game thinking happens in the blanked region
|
|
224
|
+
|
|
225
|
+
; burn whatever VBLANK time the logic didn't use
|
|
226
|
+
.vbwait:
|
|
227
|
+
LDA INTIM
|
|
228
|
+
BNE .vbwait
|
|
229
|
+
STA WSYNC
|
|
230
|
+
|
|
231
|
+
; kernel dispatch — title has its own kernel; play and game-over share one
|
|
232
|
+
LDA STATE
|
|
233
|
+
BNE .ingame
|
|
234
|
+
JMP title_kernel
|
|
235
|
+
.ingame:
|
|
236
|
+
JMP play_kernel
|
|
237
|
+
|
|
238
|
+
kernel_done:
|
|
239
|
+
; overscan: 30 lines, timer-paced like VBLANK
|
|
153
240
|
LDA #2
|
|
154
241
|
STA VBLANK
|
|
155
|
-
|
|
156
|
-
|
|
242
|
+
LDA #35
|
|
243
|
+
STA TIM64T
|
|
244
|
+
.oswait:
|
|
245
|
+
LDA INTIM
|
|
246
|
+
BNE .oswait
|
|
157
247
|
STA WSYNC
|
|
158
|
-
|
|
159
|
-
BNE .vb
|
|
248
|
+
JMP MAIN
|
|
160
249
|
|
|
161
|
-
|
|
162
|
-
|
|
250
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
251
|
+
; Per-frame logic, dispatched by state. Runs entirely inside the timed
|
|
252
|
+
; VBLANK window (~2800 cycles — an eternity next to the kernel's 76/line).
|
|
253
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
254
|
+
frame_logic:
|
|
255
|
+
INC FRAME
|
|
256
|
+
JSR audio_tick
|
|
257
|
+
|
|
258
|
+
; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
259
|
+
; Console RESET + fire button are ACTIVE LOW and not debounced; a held
|
|
260
|
+
; RESET would restart every frame. Convert to press-EDGES once per frame:
|
|
261
|
+
; edge = was-released-last-frame AND pressed-now.
|
|
262
|
+
LDA SWCHB
|
|
263
|
+
TAX ; X = current switch levels
|
|
264
|
+
EOR #$FF ; A = pressed-now mask (1 = held)
|
|
265
|
+
AND SWCHB_PRV ; ...that were RELEASED (1) last frame
|
|
266
|
+
STA EDGEB ; bit0 = RESET edge
|
|
267
|
+
STX SWCHB_PRV
|
|
268
|
+
; fire button → same edge treatment in bit7
|
|
269
|
+
LDA #0
|
|
270
|
+
BIT INPT4
|
|
271
|
+
BMI .fup ; bit7 set = not pressed (active low)
|
|
272
|
+
ORA #$80
|
|
273
|
+
.fup:
|
|
274
|
+
TAY ; Y = pressed-now (bit7)
|
|
275
|
+
LDA FIRE_PRV
|
|
276
|
+
EOR #$FF
|
|
277
|
+
STA TMP ; released-last-frame mask
|
|
278
|
+
TYA
|
|
279
|
+
AND TMP
|
|
280
|
+
STA FIRE_EDG ; bit7 = fire press-edge
|
|
281
|
+
STY FIRE_PRV
|
|
282
|
+
|
|
283
|
+
LDA STATE
|
|
284
|
+
BEQ logic_title
|
|
285
|
+
CMP #1
|
|
286
|
+
BEQ logic_play_jmp
|
|
287
|
+
JMP logic_over
|
|
288
|
+
logic_play_jmp:
|
|
289
|
+
JMP logic_play
|
|
290
|
+
|
|
291
|
+
; ── GAME LOGIC (clay — reshape freely) ── title-screen behavior ────────
|
|
292
|
+
logic_title:
|
|
293
|
+
; fire 0 or console RESET starts the game.
|
|
294
|
+
LDA FIRE_EDG
|
|
295
|
+
BMI .start ; bit7 set = fire edge
|
|
296
|
+
LDA EDGEB
|
|
163
297
|
AND #$01
|
|
164
|
-
BNE .
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
298
|
+
BNE .start
|
|
299
|
+
JMP .packtitle
|
|
300
|
+
.start:
|
|
301
|
+
JMP start_game
|
|
302
|
+
.packtitle:
|
|
303
|
+
; Pack the hi-score into the title's display buffer (the kernel just
|
|
304
|
+
; streams bytes — all per-frame thinking happens HERE, in VBLANK, never
|
|
305
|
+
; inside a kernel). Two visible digits, packed two-per-PF1-row.
|
|
306
|
+
LDA SCORE_HSV
|
|
307
|
+
JSR pack_two_digits
|
|
308
|
+
LDY #0
|
|
309
|
+
.hst:
|
|
310
|
+
LDA SCRATCH,Y ; pack_two_digits left 6 rows in SCRATCH..SCRATCH+5
|
|
311
|
+
STA HSBUF,Y
|
|
312
|
+
INY
|
|
313
|
+
CPY #6
|
|
314
|
+
BNE .hst
|
|
315
|
+
|
|
316
|
+
; title shows no moving objects
|
|
317
|
+
LDA #0
|
|
318
|
+
STA GRP0
|
|
319
|
+
STA ENABL
|
|
320
|
+
STA ENAM0
|
|
321
|
+
RTS
|
|
322
|
+
|
|
323
|
+
; ── GAME LOGIC (clay — reshape freely) ── one frame of the platformer ──
|
|
324
|
+
logic_play:
|
|
325
|
+
LDA EDGEB
|
|
326
|
+
AND #$01 ; console RESET → back to title
|
|
327
|
+
BEQ .noquit
|
|
328
|
+
JMP enter_title
|
|
329
|
+
.noquit:
|
|
330
|
+
|
|
331
|
+
; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
332
|
+
; SWCHA is ACTIVE LOW (0 = pressed) and must be RE-LOADED for every
|
|
333
|
+
; direction check. The classic bug: caching it in A and chaining ASLs,
|
|
334
|
+
; then clobbering A with game state between shifts — "right works once,
|
|
335
|
+
; left never moves". Fresh LDA SWCHA + AND #mask per check is immune.
|
|
336
|
+
; Joystick 0 lives in the HIGH nibble: bit7 right, bit6 left, bit4 up.
|
|
168
337
|
LDA SWCHA
|
|
169
|
-
AND #$80 ;
|
|
338
|
+
AND #$80 ; joy0 right
|
|
170
339
|
BNE .nr
|
|
171
340
|
LDA P_X
|
|
172
|
-
CMP #
|
|
341
|
+
CMP #WALK_HI
|
|
173
342
|
BCS .nr
|
|
174
343
|
INC P_X
|
|
175
344
|
INC P_X
|
|
345
|
+
LDA #6
|
|
346
|
+
STA GFXIDX ; walk frame while moving
|
|
176
347
|
.nr:
|
|
177
|
-
LDA SWCHA
|
|
178
|
-
AND #$40 ;
|
|
348
|
+
LDA SWCHA ; RE-LOAD — never trust A to still hold SWCHA
|
|
349
|
+
AND #$40 ; joy0 left
|
|
179
350
|
BNE .nl
|
|
180
351
|
LDA P_X
|
|
181
|
-
CMP #
|
|
352
|
+
CMP #WALK_LO
|
|
182
353
|
BCC .nl
|
|
183
354
|
DEC P_X
|
|
184
355
|
DEC P_X
|
|
356
|
+
LDA #6
|
|
357
|
+
STA GFXIDX
|
|
185
358
|
.nl:
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
;
|
|
189
|
-
;
|
|
190
|
-
;
|
|
191
|
-
;
|
|
192
|
-
;
|
|
193
|
-
;
|
|
359
|
+
|
|
360
|
+
; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
361
|
+
; JUMP / GRAVITY — a fixed-point vertical-velocity counter, the heart of
|
|
362
|
+
; any platformer. P_VY is signed: positive = rising, negative = falling.
|
|
363
|
+
; JUMP is only allowed when ON_GND was true (set by the ground test BELOW,
|
|
364
|
+
; from last frame's footing) — the canonical "no mid-air double jump" gate.
|
|
365
|
+
; Beam-Y runs 192→1 top-to-bottom, but we keep P_Y in LEVEL space (0 =
|
|
366
|
+
; arena floor, growing UP) so the physics reads naturally; the kernel maps
|
|
367
|
+
; it back to a scanline.
|
|
194
368
|
LDA ON_GND
|
|
195
|
-
BEQ .
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
369
|
+
BEQ .noJump
|
|
370
|
+
; jump on fire edge OR joystick up
|
|
371
|
+
LDA FIRE_EDG
|
|
372
|
+
BMI .doJump
|
|
373
|
+
LDA SWCHA
|
|
374
|
+
AND #$10 ; joy0 up
|
|
375
|
+
BNE .noJump
|
|
376
|
+
.doJump:
|
|
377
|
+
LDA #JUMP_VY
|
|
199
378
|
STA P_VY
|
|
200
379
|
LDA #0
|
|
201
|
-
STA ON_GND
|
|
202
|
-
; jump sfx
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
LDA #$0F
|
|
208
|
-
STA AUDV0
|
|
209
|
-
LDA #6
|
|
210
|
-
STA SFX_LEFT
|
|
211
|
-
.nojump:
|
|
380
|
+
STA ON_GND ; we leave the ground this frame
|
|
381
|
+
LDA #$0C ; jump sfx
|
|
382
|
+
LDX #$04
|
|
383
|
+
LDY #6
|
|
384
|
+
JSR sfx_play
|
|
385
|
+
.noJump:
|
|
212
386
|
|
|
213
|
-
;
|
|
214
|
-
; Standing still on a platform we DON'T apply gravity (otherwise the
|
|
215
|
-
; player drops 1px every frame and the landing snap fights it → jitter).
|
|
216
|
-
LDA ON_GND
|
|
217
|
-
BNE .skipgrav
|
|
218
|
-
DEC P_VY ; gravity: velocity drifts toward falling each frame
|
|
219
|
-
; Clamp terminal FALL speed to -8 px/frame — but ONLY while falling.
|
|
220
|
-
; The old unsigned compare (CMP #$F8 / BCS keep) also caught every
|
|
221
|
-
; POSITIVE velocity (5 < $F8 unsigned!), so the instant you jumped the
|
|
222
|
-
; clamp slammed P_VY from +6 to -8: the whole "jump" rose 0 frames,
|
|
223
|
-
; fell 8px and re-landed within ONE frame — jump sfx played, screen
|
|
224
|
-
; blipped, player never left the ground.
|
|
387
|
+
; apply gravity to velocity, then velocity to position (signed integrate)
|
|
225
388
|
LDA P_VY
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
BCS .vyok ; -8..-1 → within terminal speed, keep
|
|
229
|
-
LDA #$F8 ; -128..-9 → clamp to -8
|
|
389
|
+
SEC
|
|
390
|
+
SBC #GRAVITY
|
|
230
391
|
STA P_VY
|
|
231
|
-
.vyok:
|
|
232
|
-
; P_Y += P_VY (signed add: sign-extend P_VY into the add)
|
|
233
|
-
LDA P_VY
|
|
234
392
|
CLC
|
|
235
|
-
|
|
393
|
+
LDA P_Y
|
|
394
|
+
ADC P_VY
|
|
236
395
|
STA P_Y
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
;
|
|
241
|
-
; each platform, the stand-line = PLAT_Y + PH (the player's feet rest
|
|
242
|
-
; just above the band's top edge). If the player's feet (P_Y) have
|
|
243
|
-
; reached or just dropped through that line from above, and X is within
|
|
244
|
-
; the platform's span, snap onto it.
|
|
396
|
+
; ceiling clamp (top of arena = NROWS*16 - HEROH)
|
|
397
|
+
CMP #(NROWS*ROWH - HEROH)
|
|
398
|
+
BCC .nceil
|
|
399
|
+
; only clamp when RISING into the ceiling (large value, not a fall-wrap)
|
|
245
400
|
LDA P_VY
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
.landloop:
|
|
249
|
-
LDA PLAT_Y,X
|
|
250
|
-
CLC
|
|
251
|
-
ADC #PH ; stand-line for this platform
|
|
252
|
-
STA LANDY
|
|
253
|
-
; player at/below the stand-line? (P_Y <= LANDY, i.e. NOT P_Y > LANDY)
|
|
254
|
-
LDA P_Y
|
|
255
|
-
CMP LANDY
|
|
256
|
-
BEQ .ydepth ; exactly on it
|
|
257
|
-
BCS .nextplat ; P_Y > LANDY → still above the surface → no land
|
|
258
|
-
.ydepth:
|
|
259
|
-
; not fallen WAY past it (avoid grabbing a platform from underneath):
|
|
260
|
-
; require LANDY - P_Y <= 12.
|
|
261
|
-
LDA LANDY
|
|
262
|
-
SEC
|
|
263
|
-
SBC P_Y
|
|
264
|
-
CMP #13
|
|
265
|
-
BCS .nextplat ; dropped >12px below → ignore
|
|
266
|
-
; x-span test: PLAT_XL <= P_X <= PLAT_XR
|
|
267
|
-
LDA P_X
|
|
268
|
-
CMP PLAT_XL,X
|
|
269
|
-
BCC .nextplat
|
|
270
|
-
CMP PLAT_XR,X
|
|
271
|
-
BCS .nextplat ; (XR=159 + the +1 makes the whole row standable)
|
|
272
|
-
; LAND!
|
|
273
|
-
LDA LANDY
|
|
401
|
+
BMI .nceil
|
|
402
|
+
LDA #(NROWS*ROWH - HEROH)
|
|
274
403
|
STA P_Y
|
|
275
404
|
LDA #0
|
|
405
|
+
STA P_VY ; bonk the head → stop rising
|
|
406
|
+
.nceil:
|
|
407
|
+
|
|
408
|
+
; floor / fall-through clamp FIRST: P_Y is unsigned; a downward step past 0
|
|
409
|
+
; wraps to a large value. Detect that and snap to the floor (row 0 top).
|
|
410
|
+
LDA P_Y
|
|
411
|
+
CMP #(NROWS*ROWH)
|
|
412
|
+
BCC .nfloor
|
|
413
|
+
LDA #0 ; wrapped (fell below the floor) → clamp to floor
|
|
414
|
+
STA P_Y
|
|
276
415
|
STA P_VY
|
|
277
416
|
LDA #1
|
|
278
417
|
STA ON_GND
|
|
279
|
-
JMP .
|
|
280
|
-
.
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
;
|
|
418
|
+
JMP .landDone
|
|
419
|
+
.nfloor:
|
|
420
|
+
|
|
421
|
+
; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
422
|
+
; LAND-ON-LEDGE collision — done in CODE, not via a TIA latch. A shooter
|
|
423
|
+
; can ask the TIA "did anything overlap?", but a platformer must know
|
|
424
|
+
; WHICH surface stopped the fall, to settle the hero exactly on top of it.
|
|
425
|
+
; Rule: only when FALLING (P_VY <= 0), look up the LEVEL row at the hero's
|
|
426
|
+
; FEET and test whether a solid PF pixel sits under his X column. If yes,
|
|
427
|
+
; snap his feet to that row's top, zero the velocity, and mark ON_GND for
|
|
428
|
+
; next frame's jump gate.
|
|
285
429
|
LDA #0
|
|
286
430
|
STA ON_GND
|
|
287
|
-
|
|
288
|
-
.
|
|
289
|
-
|
|
290
|
-
; Safety floor: never let the player fall off the bottom of the world.
|
|
431
|
+
LDA P_VY
|
|
432
|
+
BPL .landDone ; rising (P_VY > 0) → can't land
|
|
433
|
+
; falling. Which row are the feet in? row = P_Y / 16.
|
|
291
434
|
LDA P_Y
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
435
|
+
LSR
|
|
436
|
+
LSR
|
|
437
|
+
LSR
|
|
438
|
+
LSR
|
|
439
|
+
CMP #NROWS
|
|
440
|
+
BCC .rowok
|
|
441
|
+
LDA #(NROWS-1)
|
|
442
|
+
.rowok:
|
|
443
|
+
STA TMP ; TMP = row index
|
|
444
|
+
JSR ground_under_hero ; C=1 if solid PF bit under P_X in row TMP
|
|
445
|
+
BCC .landDone
|
|
446
|
+
; snap feet to the TOP of this row (row*16)
|
|
447
|
+
LDA TMP
|
|
448
|
+
ASL
|
|
449
|
+
ASL
|
|
450
|
+
ASL
|
|
451
|
+
ASL ; row*16
|
|
295
452
|
STA P_Y
|
|
296
453
|
LDA #0
|
|
297
454
|
STA P_VY
|
|
298
455
|
LDA #1
|
|
299
456
|
STA ON_GND
|
|
300
|
-
.
|
|
457
|
+
.landDone:
|
|
458
|
+
|
|
459
|
+
; ── GAME LOGIC (clay) — the COIN bounces in place; grab it to score ────
|
|
460
|
+
; The coin (TIA ball, BL) bounces vertically between COIN_FLR and
|
|
461
|
+
; COIN_CEIL. Touch it (P0/BL collision latch) → score + respawn it at a
|
|
462
|
+
; new pseudo-random column.
|
|
463
|
+
LDA COIN_Y
|
|
464
|
+
CLC
|
|
465
|
+
ADC COIN_VY
|
|
466
|
+
STA COIN_Y
|
|
467
|
+
CMP #COIN_CEIL
|
|
468
|
+
BCC .ncoinTop
|
|
469
|
+
LDA #$FF ; reverse to falling
|
|
470
|
+
STA COIN_VY
|
|
471
|
+
.ncoinTop:
|
|
472
|
+
LDA COIN_Y
|
|
473
|
+
CMP #COIN_FLR
|
|
474
|
+
BCS .ncoinBot
|
|
475
|
+
LDA #1 ; reverse to rising
|
|
476
|
+
STA COIN_VY
|
|
477
|
+
.ncoinBot:
|
|
301
478
|
|
|
302
|
-
; ──
|
|
479
|
+
; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
480
|
+
; Coin pickup AND spike death both use the TIA's hardware collision
|
|
481
|
+
; LATCHES (P0/ball, P0/missile0). The TIA detects pixel overlap in silicon
|
|
482
|
+
; as it draws; we read the latched result here, one frame later, free.
|
|
483
|
+
; Rules: latches accumulate until CXCLR — clear them EVERY frame (we do at
|
|
484
|
+
; the end of this block), or a stale hit fires phantom pickups/deaths long
|
|
485
|
+
; after the object moved.
|
|
486
|
+
BIT CXP0FB ; bit6 (V flag) = P0/ball (hero/coin) overlapped
|
|
487
|
+
BVC .noCoin
|
|
488
|
+
JSR add_score ; +10
|
|
489
|
+
JSR respawn_coin
|
|
490
|
+
LDA #$08 ; coin chime
|
|
491
|
+
LDX #$0C
|
|
492
|
+
LDY #8
|
|
493
|
+
JSR sfx_play
|
|
494
|
+
.noCoin:
|
|
495
|
+
|
|
496
|
+
; ── GAME LOGIC (clay) — the SPIKE patrols a ledge left/right ───────────
|
|
497
|
+
LDA SPK_X
|
|
498
|
+
CLC
|
|
499
|
+
ADC SPK_DIR
|
|
500
|
+
STA SPK_X
|
|
501
|
+
CMP #WALK_HI
|
|
502
|
+
BCC .nspkR
|
|
503
|
+
LDA #$FF
|
|
504
|
+
STA SPK_DIR
|
|
505
|
+
.nspkR:
|
|
506
|
+
LDA SPK_X
|
|
507
|
+
CMP #WALK_LO
|
|
508
|
+
BCS .nspkL
|
|
509
|
+
LDA #1
|
|
510
|
+
STA SPK_DIR
|
|
511
|
+
.nspkL:
|
|
512
|
+
|
|
513
|
+
BIT CXM0P ; bit7 (N flag) = missile0/P0 (spike/hero) overlapped
|
|
514
|
+
BPL .noDeath
|
|
515
|
+
JMP do_game_over
|
|
516
|
+
.noDeath:
|
|
517
|
+
STA CXCLR ; arm BOTH latches fresh for the frame we draw next
|
|
518
|
+
|
|
519
|
+
; pick the rows for the kernel (Y/16) so the kernel doesn't divide per line
|
|
520
|
+
LDA P_Y
|
|
521
|
+
LSR
|
|
522
|
+
LSR
|
|
523
|
+
LSR
|
|
524
|
+
LSR
|
|
525
|
+
STA P_ROW
|
|
526
|
+
LDA COIN_Y
|
|
527
|
+
LSR
|
|
528
|
+
LSR
|
|
529
|
+
LSR
|
|
530
|
+
LSR
|
|
531
|
+
STA COIN_ROW
|
|
532
|
+
LDA SPK_Y
|
|
533
|
+
LSR
|
|
534
|
+
LSR
|
|
535
|
+
LSR
|
|
536
|
+
LSR
|
|
537
|
+
STA SPK_ROW
|
|
538
|
+
|
|
539
|
+
; decay the walk-animation frame back to idle when not pressing
|
|
540
|
+
LDA GFXIDX
|
|
541
|
+
BEQ .pk
|
|
542
|
+
DEC GFXIDX
|
|
543
|
+
.pk:
|
|
544
|
+
JMP pack_score ; render SCORE into S0BUF (tail-RTS ends frame_logic)
|
|
545
|
+
|
|
546
|
+
; ── GAME LOGIC (clay — reshape freely) ── game-over freeze-frame ───────
|
|
547
|
+
logic_over:
|
|
548
|
+
LDA EDGEB
|
|
549
|
+
AND #$01
|
|
550
|
+
BNE .toTitle
|
|
551
|
+
LDA FIRE_EDG
|
|
552
|
+
BMI .toTitle
|
|
553
|
+
DEC OVER_T
|
|
554
|
+
BNE .stay
|
|
555
|
+
.toTitle:
|
|
556
|
+
JMP enter_title
|
|
557
|
+
.stay:
|
|
558
|
+
RTS
|
|
559
|
+
|
|
560
|
+
; ── GAME LOGIC (clay — reshape freely) ── helpers ──────────────────────
|
|
561
|
+
|
|
562
|
+
; ground_under_hero — is there a solid LEVEL pixel under the hero's column
|
|
563
|
+
; in row TMP? Returns C=1 if solid (can stand), C=0 if pit/gap.
|
|
564
|
+
; The hero stands at a coarse 1-of-40 column: PF pixel = P_X/4 across the 40
|
|
565
|
+
; playfield pixels of a (reflect-mode) symmetric arena. We fold the right
|
|
566
|
+
; half back, then test the right PF register+bit. Code reads the SAME LEVEL
|
|
567
|
+
; table the kernel draws, so picture and physics never disagree.
|
|
568
|
+
ground_under_hero:
|
|
569
|
+
LDA TMP
|
|
570
|
+
CMP #NROWS
|
|
571
|
+
BCC .lok
|
|
572
|
+
LDA #(NROWS-1)
|
|
573
|
+
.lok:
|
|
574
|
+
STA TMP2
|
|
575
|
+
ASL
|
|
576
|
+
CLC
|
|
577
|
+
ADC TMP2 ; row*3 = LEVEL byte offset (PF0,PF1,PF2 per row)
|
|
578
|
+
TAX ; X = LEVEL offset
|
|
579
|
+
; coarse playfield pixel index 0..39 from P_X (160 visible px / 4 = 40)
|
|
580
|
+
LDA P_X
|
|
581
|
+
LSR
|
|
582
|
+
LSR ; P_X/4
|
|
583
|
+
STA TMP2
|
|
584
|
+
; In REFLECT mode the right half (px 20..39) mirrors the left, so fold any
|
|
585
|
+
; index >= 20 down to 0..19 reading from the right edge inward.
|
|
586
|
+
CMP #20
|
|
587
|
+
BCC .left
|
|
588
|
+
LDA #39
|
|
589
|
+
SEC
|
|
590
|
+
SBC TMP2 ; 39 - idx → 0..19
|
|
591
|
+
STA TMP2
|
|
592
|
+
.left:
|
|
593
|
+
; TMP2 = 0..19 into the 20-pixel half. Which register/bit?
|
|
594
|
+
; px 0..3 → PF0 bits 4..7 (bit4 = leftmost)
|
|
595
|
+
; px 4..11 → PF1 bits 7..0 (bit7 = leftmost)
|
|
596
|
+
; px 12..19 → PF2 bits 0..7 (bit0 = leftmost)
|
|
597
|
+
LDA TMP2
|
|
598
|
+
CMP #4
|
|
599
|
+
BCS .notPF0
|
|
600
|
+
; PF0: pixel n (0..3) → bit (4+n)
|
|
601
|
+
CLC
|
|
602
|
+
ADC #4
|
|
603
|
+
TAY ; Y = bit index in PF0
|
|
604
|
+
LDA LEVEL,X ; PF0 byte
|
|
605
|
+
JMP .testBit
|
|
606
|
+
.notPF0:
|
|
607
|
+
CMP #12
|
|
608
|
+
BCS .pf2
|
|
609
|
+
; PF1: pixel n (4..11) → bit (11-n)
|
|
610
|
+
STA TMP2
|
|
611
|
+
LDA #11
|
|
612
|
+
SEC
|
|
613
|
+
SBC TMP2
|
|
614
|
+
TAY ; Y = bit index in PF1
|
|
615
|
+
LDA LEVEL+1,X ; PF1 byte
|
|
616
|
+
JMP .testBit
|
|
617
|
+
.pf2:
|
|
618
|
+
; PF2: pixel n (12..19) → bit (n-12) (bit0 = leftmost)
|
|
619
|
+
SEC
|
|
620
|
+
SBC #12
|
|
621
|
+
TAY ; Y = bit index in PF2
|
|
622
|
+
LDA LEVEL+2,X ; PF2 byte
|
|
623
|
+
.testBit:
|
|
624
|
+
; shift the chosen bit (Y) down to bit0, return C = that bit
|
|
625
|
+
CPY #0
|
|
626
|
+
BEQ .haveBit
|
|
627
|
+
.shloop:
|
|
628
|
+
LSR
|
|
629
|
+
DEY
|
|
630
|
+
BNE .shloop
|
|
631
|
+
.haveBit:
|
|
632
|
+
AND #$01
|
|
633
|
+
CMP #$01 ; sets C if the bit was 1 (solid ground)
|
|
634
|
+
RTS
|
|
635
|
+
|
|
636
|
+
respawn_coin: ; place the coin at a new pseudo-random column + reset Y
|
|
637
|
+
LDA FRAME
|
|
638
|
+
AND #$7F
|
|
639
|
+
CLC
|
|
640
|
+
ADC #20 ; 20..147 column
|
|
641
|
+
CMP #WALK_HI
|
|
642
|
+
BCC .cok
|
|
643
|
+
LDA #100
|
|
644
|
+
.cok:
|
|
645
|
+
STA COIN_X
|
|
646
|
+
LDA #COIN_CEIL
|
|
647
|
+
STA COIN_Y
|
|
648
|
+
LDA #$FF
|
|
649
|
+
STA COIN_VY ; start it falling
|
|
650
|
+
RTS
|
|
651
|
+
|
|
652
|
+
add_score: ; +10 points, BCD, capped at 9990, tracks session hi
|
|
653
|
+
SED
|
|
654
|
+
LDA SCORE
|
|
655
|
+
CLC
|
|
656
|
+
ADC #$10 ; tens place +1 → +10 points
|
|
657
|
+
STA SCORE
|
|
658
|
+
LDA SCORE_HI
|
|
659
|
+
ADC #0 ; carry into the high byte
|
|
660
|
+
STA SCORE_HI
|
|
661
|
+
CLD
|
|
662
|
+
; keep the running session hi-score
|
|
663
|
+
LDA SCORE_HI
|
|
664
|
+
CMP SCORE_HSH
|
|
665
|
+
BCC .nohs
|
|
666
|
+
BNE .seths
|
|
667
|
+
LDA SCORE
|
|
668
|
+
CMP SCORE_HSV
|
|
669
|
+
BCC .nohs
|
|
670
|
+
.seths:
|
|
671
|
+
LDA SCORE
|
|
672
|
+
STA SCORE_HSV
|
|
673
|
+
LDA SCORE_HI
|
|
674
|
+
STA SCORE_HSH
|
|
675
|
+
.nohs:
|
|
676
|
+
RTS
|
|
677
|
+
|
|
678
|
+
do_game_over:
|
|
679
|
+
LDA #2
|
|
680
|
+
STA STATE
|
|
681
|
+
LDA #200 ; ~3.3 s freeze, then auto-return to title
|
|
682
|
+
STA OVER_T
|
|
683
|
+
LDA #0
|
|
684
|
+
STA ENABL
|
|
685
|
+
STA ENAM0
|
|
686
|
+
STA GRP0
|
|
687
|
+
LDA #1
|
|
688
|
+
STA TUNE_SEL
|
|
689
|
+
JMP tune_start ; game-over tune on voice 1
|
|
690
|
+
|
|
691
|
+
start_game:
|
|
692
|
+
LDA #0
|
|
693
|
+
STA SCORE
|
|
694
|
+
STA SCORE_HI
|
|
695
|
+
STA TUNE_LEFT ; silence the title jingle
|
|
696
|
+
STA AUDV1
|
|
697
|
+
STA P_VY
|
|
698
|
+
STA GFXIDX
|
|
699
|
+
LDA #1
|
|
700
|
+
STA ON_GND
|
|
701
|
+
LDA #76
|
|
702
|
+
STA P_X
|
|
703
|
+
LDA #(2*ROWH) ; start standing on row 2's ledge
|
|
704
|
+
STA P_Y
|
|
705
|
+
; coin
|
|
706
|
+
LDA #110
|
|
707
|
+
STA COIN_X
|
|
708
|
+
LDA #COIN_CEIL
|
|
709
|
+
STA COIN_Y
|
|
710
|
+
LDA #$FF
|
|
711
|
+
STA COIN_VY
|
|
712
|
+
; spike patrols row 1
|
|
713
|
+
LDA #40
|
|
714
|
+
STA SPK_X
|
|
715
|
+
LDA #(1*ROWH)
|
|
716
|
+
STA SPK_Y
|
|
717
|
+
LDA #1
|
|
718
|
+
STA SPK_DIR
|
|
719
|
+
LDA #1
|
|
720
|
+
STA STATE
|
|
721
|
+
LDA #$06 ; start blip
|
|
722
|
+
LDX #$04
|
|
723
|
+
LDY #10
|
|
724
|
+
JMP sfx_play
|
|
725
|
+
|
|
726
|
+
enter_title:
|
|
727
|
+
LDA #0
|
|
728
|
+
STA STATE
|
|
729
|
+
STA GRP0
|
|
730
|
+
STA ENABL
|
|
731
|
+
STA ENAM0
|
|
732
|
+
STA AUDV0
|
|
733
|
+
STA SFX_LEFT
|
|
734
|
+
STA TUNE_SEL ; title jingle
|
|
735
|
+
JMP tune_start
|
|
736
|
+
|
|
737
|
+
digit_times6: ; A = digit 0-9 → A = digit*6 (DIGITS row index)
|
|
738
|
+
STA TMP
|
|
739
|
+
ASL
|
|
740
|
+
CLC
|
|
741
|
+
ADC TMP ; *3
|
|
742
|
+
ASL ; *6
|
|
743
|
+
RTS
|
|
744
|
+
|
|
745
|
+
; pack_two_digits — A = a BCD byte (two digits). Writes 6 rows into SCRATCH,
|
|
746
|
+
; left digit (high nibble) in PF1 high nibble, right digit (low nibble) in
|
|
747
|
+
; PF1 low nibble. In SCORE mode the byte draws twice (two colors) — the
|
|
748
|
+
; classic dual-score look — but here both halves carry the SAME packed pair.
|
|
749
|
+
pack_two_digits:
|
|
750
|
+
PHA
|
|
751
|
+
LSR
|
|
752
|
+
LSR
|
|
753
|
+
LSR
|
|
754
|
+
LSR ; high (tens) digit
|
|
755
|
+
JSR digit_times6
|
|
756
|
+
TAX
|
|
757
|
+
LDY #0
|
|
758
|
+
.pd0:
|
|
759
|
+
LDA DIGITS,X
|
|
760
|
+
STA SCRATCH,Y ; high nibble of font = left digit
|
|
761
|
+
INX
|
|
762
|
+
INY
|
|
763
|
+
CPY #6
|
|
764
|
+
BNE .pd0
|
|
765
|
+
PLA
|
|
766
|
+
AND #$0F ; low (ones) digit
|
|
767
|
+
JSR digit_times6
|
|
768
|
+
TAX
|
|
769
|
+
LDY #0
|
|
770
|
+
.pd1:
|
|
771
|
+
LDA DIGITS,X
|
|
772
|
+
LSR
|
|
773
|
+
LSR
|
|
774
|
+
LSR
|
|
775
|
+
LSR ; ones in the LOW nibble
|
|
776
|
+
ORA SCRATCH,Y
|
|
777
|
+
STA SCRATCH,Y
|
|
778
|
+
INX
|
|
779
|
+
INY
|
|
780
|
+
CPY #6
|
|
781
|
+
BNE .pd1
|
|
782
|
+
RTS
|
|
783
|
+
|
|
784
|
+
pack_score: ; render the low two SCORE digits into S0BUF
|
|
785
|
+
LDA SCORE
|
|
786
|
+
JSR pack_two_digits
|
|
787
|
+
LDY #0
|
|
788
|
+
.pks:
|
|
789
|
+
LDA SCRATCH,Y
|
|
790
|
+
STA S0BUF,Y
|
|
791
|
+
INY
|
|
792
|
+
CPY #6
|
|
793
|
+
BNE .pks
|
|
794
|
+
RTS
|
|
795
|
+
|
|
796
|
+
; ── GAME LOGIC (clay — reshape freely) ── TIA sound ────────────────────
|
|
797
|
+
; Voice 0 = one-shot sound effects; voice 1 = the jingle player. Keeping
|
|
798
|
+
; them on separate voices means a jump blip never cuts the tune off.
|
|
799
|
+
sfx_play: ; A = AUDF pitch, X = AUDC waveform, Y = frames
|
|
800
|
+
STA AUDF0
|
|
801
|
+
STX AUDC0
|
|
802
|
+
STY SFX_LEFT
|
|
803
|
+
LDA #$0C
|
|
804
|
+
STA AUDV0
|
|
805
|
+
RTS
|
|
806
|
+
|
|
807
|
+
tune_start: ; TUNE_SEL chosen by caller (0 title, 1 game over)
|
|
808
|
+
LDA #0
|
|
809
|
+
STA TUNE_POS
|
|
810
|
+
JSR tune_note
|
|
811
|
+
LDA #$04 ; pure square wave
|
|
812
|
+
STA AUDC1
|
|
813
|
+
LDA #$06
|
|
814
|
+
STA AUDV1
|
|
815
|
+
LDA #10
|
|
816
|
+
STA TUNE_LEFT
|
|
817
|
+
RTS
|
|
818
|
+
|
|
819
|
+
tune_note: ; load AUDF1 from the selected table at TUNE_POS;
|
|
820
|
+
LDX TUNE_POS ; returns Z set (A=0) on the $FF terminator
|
|
821
|
+
LDA TUNE_SEL
|
|
822
|
+
BNE .tn1
|
|
823
|
+
LDA TITLE_TUNE,X
|
|
824
|
+
JMP .tn2
|
|
825
|
+
.tn1:
|
|
826
|
+
LDA OVER_TUNE,X
|
|
827
|
+
.tn2:
|
|
828
|
+
CMP #$FF
|
|
829
|
+
BEQ .tnEnd
|
|
830
|
+
STA AUDF1
|
|
831
|
+
LDA #1
|
|
832
|
+
RTS
|
|
833
|
+
.tnEnd:
|
|
834
|
+
LDA #0
|
|
835
|
+
STA AUDV1
|
|
836
|
+
RTS
|
|
837
|
+
|
|
838
|
+
audio_tick: ; called once per frame, every state
|
|
303
839
|
LDA SFX_LEFT
|
|
304
|
-
BEQ .
|
|
840
|
+
BEQ .at1
|
|
305
841
|
DEC SFX_LEFT
|
|
306
|
-
BNE .
|
|
842
|
+
BNE .at1
|
|
307
843
|
LDA #0
|
|
308
|
-
STA AUDV0
|
|
309
|
-
.
|
|
844
|
+
STA AUDV0 ; sfx finished → silence voice 0
|
|
845
|
+
.at1:
|
|
846
|
+
LDA TUNE_LEFT
|
|
847
|
+
BEQ .at2
|
|
848
|
+
DEC TUNE_LEFT
|
|
849
|
+
BNE .at2
|
|
850
|
+
INC TUNE_POS
|
|
851
|
+
JSR tune_note
|
|
852
|
+
BEQ .at2 ; hit the terminator → tune stays off
|
|
853
|
+
LDA #10
|
|
854
|
+
STA TUNE_LEFT
|
|
855
|
+
.at2:
|
|
856
|
+
RTS
|
|
310
857
|
|
|
311
|
-
|
|
858
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
859
|
+
; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
860
|
+
; OBJECT POSITIONING — the canonical SBC-#15 beam-race. There is no "X
|
|
861
|
+
; register" for sprites: you strobe RESP0/RESBL/RESM0 and the object lands
|
|
862
|
+
; WHEREVER THE BEAM IS. Each SBC/BCS lap is 5 CPU cycles = 15 beam pixels,
|
|
863
|
+
; so when the subtraction underflows the beam has crossed x/15 coarse
|
|
864
|
+
; columns; the remainder, EOR #7 shifted to the high nibble, becomes the
|
|
865
|
+
; ±7px fine offset HMOVE applies on the next line. The naive "divide first,
|
|
866
|
+
; then burn a delay loop" version lands in the WRONG column. Three objects =
|
|
867
|
+
; three WSYNC lines + one shared HMOVE line, all inside timed VBLANK.
|
|
868
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
869
|
+
position_objects:
|
|
312
870
|
STA WSYNC
|
|
313
871
|
STA HMCLR
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
.p0pos:
|
|
317
|
-
CPX #15
|
|
318
|
-
BCC .p0done
|
|
872
|
+
LDA P_X ; hero → P0
|
|
873
|
+
STA WSYNC
|
|
319
874
|
SEC
|
|
875
|
+
.d0:
|
|
320
876
|
SBC #15
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
877
|
+
BCS .d0
|
|
878
|
+
EOR #7
|
|
879
|
+
ASL
|
|
880
|
+
ASL
|
|
881
|
+
ASL
|
|
882
|
+
ASL
|
|
324
883
|
STA RESP0
|
|
325
|
-
STA
|
|
884
|
+
STA HMP0
|
|
885
|
+
LDA COIN_X ; coin → BL
|
|
886
|
+
STA WSYNC
|
|
887
|
+
SEC
|
|
888
|
+
.d1:
|
|
889
|
+
SBC #15
|
|
890
|
+
BCS .d1
|
|
891
|
+
EOR #7
|
|
892
|
+
ASL
|
|
893
|
+
ASL
|
|
894
|
+
ASL
|
|
895
|
+
ASL
|
|
896
|
+
STA RESBL
|
|
897
|
+
STA HMBL
|
|
898
|
+
LDA SPK_X ; spike → M0
|
|
899
|
+
STA WSYNC
|
|
900
|
+
SEC
|
|
901
|
+
.d2:
|
|
902
|
+
SBC #15
|
|
903
|
+
BCS .d2
|
|
904
|
+
EOR #7
|
|
905
|
+
ASL
|
|
906
|
+
ASL
|
|
907
|
+
ASL
|
|
908
|
+
ASL
|
|
909
|
+
STA RESM0
|
|
910
|
+
STA HMM0
|
|
911
|
+
STA WSYNC
|
|
912
|
+
STA HMOVE ; one HMOVE applies ALL the fine offsets; it must
|
|
913
|
+
RTS ; come fresh after a WSYNC (mid-line HMOVE combs)
|
|
326
914
|
|
|
915
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
916
|
+
; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
917
|
+
; THE PLAY/GAME-OVER KERNEL — 192 visible lines, fully accounted:
|
|
918
|
+
; 24 = score bar + 144 = arena (9 rows × 16) + 24 = pad = 192
|
|
919
|
+
;
|
|
920
|
+
; SCORE BAR (SCORE mode): CTRLPF = $02 colors the LEFT playfield half with
|
|
921
|
+
; COLUP0 and the RIGHT half with COLUP1 — a two-color scoreboard with zero
|
|
922
|
+
; sprites. We stream the packed score digits into PF1, one font row / 4 lines.
|
|
923
|
+
;
|
|
924
|
+
; ARENA: the level is NROWS bands of 16 lines. Each band reloads PF0/PF1/PF2
|
|
925
|
+
; from the LEVEL table ONCE (the ledges/floor/pit for that row), in REFLECT
|
|
926
|
+
; mode so 20 stored pixels mirror into a symmetric 40-pixel arena. Per
|
|
927
|
+
; scanline we also test, from the row counter, whether the hero (P0), coin
|
|
928
|
+
; (BL) or spike (M0) band is here and enable/disable that object's graphic.
|
|
929
|
+
; Each test is a compare-and-store (no multiply) so the work fits 76 cycles.
|
|
930
|
+
; Beam Y counts DOWN; level rows are drawn TOP first (row NROWS-1 → 0), so a
|
|
931
|
+
; lit pixel in a HIGH row index draws nearer the top, matching level space.
|
|
932
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
933
|
+
play_kernel:
|
|
934
|
+
; positioning runs first, inside the still-blanked region
|
|
935
|
+
JSR position_objects
|
|
936
|
+
|
|
937
|
+
LDA #COL_SKY
|
|
938
|
+
STA COLUBK
|
|
327
939
|
LDA #0
|
|
328
|
-
STA
|
|
940
|
+
STA PF0
|
|
941
|
+
STA PF1
|
|
942
|
+
STA PF2
|
|
943
|
+
STA GRP0
|
|
944
|
+
STA ENABL
|
|
945
|
+
STA ENAM0
|
|
946
|
+
STA VBLANK ; beam on
|
|
947
|
+
LDA #COL_HUD
|
|
948
|
+
STA COLUPF ; score digits bright
|
|
949
|
+
LDA #$02
|
|
950
|
+
STA CTRLPF ; SCORE mode for the score bar
|
|
329
951
|
|
|
330
|
-
;
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
; the 76-cycle budget → frames grew to ~250 lines → no vsync lock →
|
|
334
|
-
; black rolling screen — the bug this kernel fixes). Instead PFROW[] is
|
|
335
|
-
; a 96-byte buffer (one entry per 2-line row) filled ONCE at boot from
|
|
336
|
-
; the platform table: $FF = platform here, $00 = open air. The kernel
|
|
337
|
-
; just LDA PFROW,X / STA PF1 / STA PF2 (cheap) + a single player-sprite
|
|
338
|
-
; test. That comfortably fits one scanline.
|
|
339
|
-
;
|
|
340
|
-
; X = row index 0..95 (top→bottom in buffer order). Y = beam scanline
|
|
341
|
-
; 192→1. We draw two scanlines per buffer row.
|
|
342
|
-
LDX #0 ; PFROW index
|
|
343
|
-
LDY #192
|
|
344
|
-
.draw:
|
|
952
|
+
; ---- score bar: 24 lines (6 font rows × 4) ----
|
|
953
|
+
LDX #0
|
|
954
|
+
.sbar:
|
|
345
955
|
STA WSYNC
|
|
346
|
-
|
|
347
|
-
|
|
956
|
+
TXA
|
|
957
|
+
LSR
|
|
958
|
+
LSR
|
|
959
|
+
TAY ; row = line/4
|
|
960
|
+
LDA S0BUF,Y
|
|
961
|
+
STA PF1
|
|
962
|
+
INX
|
|
963
|
+
CPX #24
|
|
964
|
+
BNE .sbar
|
|
965
|
+
|
|
966
|
+
; transition: clear the bar, switch the TIA to the arena (REFLECT mode so
|
|
967
|
+
; the 20-pixel level mirrors symmetric), ledges in green.
|
|
968
|
+
STA WSYNC
|
|
969
|
+
LDA #0
|
|
970
|
+
STA PF1
|
|
971
|
+
LDA #$11 ; REFLECT (bit0) + 2px ball (bits 4-5 = 01)
|
|
972
|
+
STA CTRLPF
|
|
973
|
+
LDA #COL_LEDGE
|
|
974
|
+
STA COLUPF
|
|
975
|
+
|
|
976
|
+
; ---- arena: NROWS rows × 16 lines, top row (NROWS-1) drawn first ----
|
|
977
|
+
LDX #(NROWS-1) ; X = current level row
|
|
978
|
+
.rowLoop:
|
|
979
|
+
; reload PF for this row (once per 16 lines). Row table offset = X*3.
|
|
980
|
+
TXA
|
|
981
|
+
STA TMP ; save row index for the per-line object tests
|
|
982
|
+
ASL
|
|
983
|
+
CLC
|
|
984
|
+
ADC TMP ; X*3
|
|
985
|
+
TAY
|
|
986
|
+
LDA LEVEL,Y
|
|
348
987
|
STA PF0
|
|
988
|
+
LDA LEVEL+1,Y
|
|
349
989
|
STA PF1
|
|
990
|
+
LDA LEVEL+2,Y
|
|
350
991
|
STA PF2
|
|
351
|
-
|
|
992
|
+
|
|
993
|
+
LDY #ROWH ; 16 scanlines for this row (Y = 16..1)
|
|
994
|
+
.lineLoop:
|
|
995
|
+
STA WSYNC
|
|
996
|
+
; sub-line within the row, 0 (top) .. 15 (bottom) = ROWH - Y
|
|
997
|
+
; hero: drawn if this row == P_ROW and sub < HEROH
|
|
998
|
+
LDA P_ROW
|
|
999
|
+
CMP TMP
|
|
1000
|
+
BNE .noHero
|
|
352
1001
|
TYA
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
1002
|
+
EOR #$FF
|
|
1003
|
+
CLC
|
|
1004
|
+
ADC #(ROWH+1) ; sub = ROWH - Y
|
|
1005
|
+
CMP #HEROH
|
|
1006
|
+
BCS .noHero
|
|
1007
|
+
CLC
|
|
1008
|
+
ADC GFXIDX ; pick idle (0) or walk (6) frame base
|
|
1009
|
+
TAX
|
|
1010
|
+
LDA HERO,X
|
|
360
1011
|
STA GRP0
|
|
361
|
-
|
|
362
|
-
JMP .
|
|
363
|
-
.
|
|
1012
|
+
LDX TMP ; restore row index
|
|
1013
|
+
JMP .heroDone
|
|
1014
|
+
.noHero:
|
|
364
1015
|
LDA #0
|
|
365
1016
|
STA GRP0
|
|
366
|
-
.
|
|
1017
|
+
.heroDone:
|
|
1018
|
+
|
|
1019
|
+
; coin (BL): enabled if this row == COIN_ROW and sub-line < 4
|
|
1020
|
+
LDA COIN_ROW
|
|
1021
|
+
CMP TMP
|
|
1022
|
+
BNE .noCoinK
|
|
1023
|
+
TYA
|
|
1024
|
+
EOR #$FF
|
|
1025
|
+
CLC
|
|
1026
|
+
ADC #(ROWH+1)
|
|
1027
|
+
CMP #4
|
|
1028
|
+
BCS .noCoinK
|
|
1029
|
+
LDA #2
|
|
1030
|
+
STA ENABL
|
|
1031
|
+
JMP .coinDone
|
|
1032
|
+
.noCoinK:
|
|
1033
|
+
LDA #0
|
|
1034
|
+
STA ENABL
|
|
1035
|
+
.coinDone:
|
|
1036
|
+
|
|
1037
|
+
; spike (M0): enabled if this row == SPK_ROW and sub-line < 6
|
|
1038
|
+
LDA SPK_ROW
|
|
1039
|
+
CMP TMP
|
|
1040
|
+
BNE .noSpikeK
|
|
1041
|
+
TYA
|
|
1042
|
+
EOR #$FF
|
|
1043
|
+
CLC
|
|
1044
|
+
ADC #(ROWH+1)
|
|
1045
|
+
CMP #6
|
|
1046
|
+
BCS .noSpikeK
|
|
1047
|
+
LDA #2
|
|
1048
|
+
STA ENAM0
|
|
1049
|
+
JMP .spikeDone
|
|
1050
|
+
.noSpikeK:
|
|
1051
|
+
LDA #0
|
|
1052
|
+
STA ENAM0
|
|
1053
|
+
.spikeDone:
|
|
1054
|
+
|
|
367
1055
|
DEY
|
|
368
|
-
|
|
1056
|
+
BNE .lineLoop
|
|
1057
|
+
|
|
1058
|
+
LDX TMP ; restore row counter (clobbered by the hero pick)
|
|
1059
|
+
DEX
|
|
1060
|
+
BPL .rowLoop
|
|
1061
|
+
|
|
1062
|
+
; pad to reach exactly 192 visible (24 bar + 144 arena = 168 → +24 pad)
|
|
1063
|
+
LDA #0
|
|
1064
|
+
STA GRP0
|
|
1065
|
+
STA ENABL
|
|
1066
|
+
STA ENAM0
|
|
1067
|
+
STA PF0
|
|
1068
|
+
STA PF1
|
|
1069
|
+
STA PF2
|
|
1070
|
+
LDX #24
|
|
1071
|
+
.pad:
|
|
369
1072
|
STA WSYNC
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
CPX #96
|
|
375
|
-
BNE .draw
|
|
1073
|
+
DEX
|
|
1074
|
+
BNE .pad
|
|
1075
|
+
|
|
1076
|
+
JMP kernel_done
|
|
376
1077
|
|
|
377
|
-
|
|
1078
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
1079
|
+
; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
1080
|
+
; THE TITLE KERNEL — 192 lines, banded:
|
|
1081
|
+
; 24 blank + 28 banner "PERCH" + 8 gap + 28 banner "PATROL" + 16 gap +
|
|
1082
|
+
; 24 hi-score + remainder pad = 192
|
|
1083
|
+
;
|
|
1084
|
+
; The banner is an ASYMMETRIC PLAYFIELD — the 2600's only way to draw
|
|
1085
|
+
; full-width artwork. The playfield registers hold just 20 pixels; the TIA
|
|
1086
|
+
; replays them for the right half of the line (CTRLPF bit0 chooses repeat
|
|
1087
|
+
; or mirror). For 40 INDEPENDENT pixels you rewrite all three registers
|
|
1088
|
+
; mid-line, each inside its window (CPU cycle = 3 color clocks; left copy
|
|
1089
|
+
; reads at clocks 68-147, right copy at 148-227):
|
|
1090
|
+
; PF0 again after cycle ~28 (left copy drawn) before ~49 (right copy reads)
|
|
1091
|
+
; PF1 again after cycle ~39 before ~54
|
|
1092
|
+
; PF2 again after cycle ~50 before ~65
|
|
1093
|
+
; The code below hits those windows by instruction order alone — count
|
|
1094
|
+
; cycles before you reorder ANYTHING between the WSYNC and the last STA.
|
|
1095
|
+
; REQUIRES: CTRLPF bit0 = 0 (repeat mode). In mirror mode the right half
|
|
1096
|
+
; reads the registers in REVERSE order and every window above is wrong.
|
|
1097
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
1098
|
+
title_kernel:
|
|
1099
|
+
LDA #$84 ; deep blue backdrop
|
|
1100
|
+
STA COLUBK
|
|
378
1101
|
LDA #0
|
|
379
1102
|
STA PF0
|
|
380
1103
|
STA PF1
|
|
381
1104
|
STA PF2
|
|
382
1105
|
STA GRP0
|
|
383
|
-
|
|
384
|
-
STA
|
|
385
|
-
|
|
386
|
-
|
|
1106
|
+
STA ENABL
|
|
1107
|
+
STA ENAM0
|
|
1108
|
+
STA CTRLPF ; REPEAT mode — required by the banner (see above)
|
|
1109
|
+
STA VBLANK ; beam on
|
|
1110
|
+
|
|
1111
|
+
LDX #24 ; band 1: 24 blank lines
|
|
1112
|
+
.tb1:
|
|
387
1113
|
STA WSYNC
|
|
388
1114
|
DEX
|
|
389
|
-
BNE .
|
|
1115
|
+
BNE .tb1
|
|
390
1116
|
|
|
391
|
-
|
|
1117
|
+
LDA #$3A ; word 1 in warm yellow
|
|
1118
|
+
STA COLUPF
|
|
1119
|
+
LDX #0 ; band 2: 28 banner lines (7 rows × 4)
|
|
1120
|
+
.ban1:
|
|
1121
|
+
STA WSYNC
|
|
1122
|
+
TXA ; row = line/4
|
|
1123
|
+
LSR
|
|
1124
|
+
LSR
|
|
1125
|
+
TAY
|
|
1126
|
+
LDA R1_PF0L,Y
|
|
1127
|
+
STA PF0
|
|
1128
|
+
LDA R1_PF1L,Y
|
|
1129
|
+
STA PF1
|
|
1130
|
+
LDA R1_PF2L,Y
|
|
1131
|
+
STA PF2
|
|
1132
|
+
LDA R1_PF0R,Y
|
|
1133
|
+
STA PF0
|
|
1134
|
+
LDA R1_PF1R,Y
|
|
1135
|
+
STA PF1
|
|
1136
|
+
NOP
|
|
1137
|
+
NOP
|
|
1138
|
+
LDA R1_PF2R,Y
|
|
1139
|
+
STA PF2
|
|
1140
|
+
INX
|
|
1141
|
+
CPX #28
|
|
1142
|
+
BNE .ban1
|
|
1143
|
+
|
|
1144
|
+
STA WSYNC ; band 3: clear + 7 gap lines
|
|
1145
|
+
LDA #0
|
|
1146
|
+
STA PF0
|
|
1147
|
+
STA PF1
|
|
1148
|
+
STA PF2
|
|
1149
|
+
LDX #7
|
|
1150
|
+
.tb3:
|
|
1151
|
+
STA WSYNC
|
|
1152
|
+
DEX
|
|
1153
|
+
BNE .tb3
|
|
392
1154
|
|
|
393
|
-
;
|
|
394
|
-
|
|
395
|
-
;
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
build_pfrow:
|
|
399
|
-
LDX #0 ; row index 0..95
|
|
400
|
-
.brow:
|
|
401
|
-
; beam scanline for this row = 192 - 2*X
|
|
1155
|
+
LDA #$C6 ; word 2 in green
|
|
1156
|
+
STA COLUPF
|
|
1157
|
+
LDX #0 ; band 4: 28 banner lines, word 2
|
|
1158
|
+
.ban2:
|
|
1159
|
+
STA WSYNC
|
|
402
1160
|
TXA
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
1161
|
+
LSR
|
|
1162
|
+
LSR
|
|
1163
|
+
TAY
|
|
1164
|
+
LDA R2_PF0L,Y
|
|
1165
|
+
STA PF0
|
|
1166
|
+
LDA R2_PF1L,Y
|
|
1167
|
+
STA PF1
|
|
1168
|
+
LDA R2_PF2L,Y
|
|
1169
|
+
STA PF2
|
|
1170
|
+
LDA R2_PF0R,Y
|
|
1171
|
+
STA PF0
|
|
1172
|
+
LDA R2_PF1R,Y
|
|
1173
|
+
STA PF1
|
|
1174
|
+
NOP
|
|
1175
|
+
NOP
|
|
1176
|
+
LDA R2_PF2R,Y
|
|
1177
|
+
STA PF2
|
|
1178
|
+
INX
|
|
1179
|
+
CPX #28
|
|
1180
|
+
BNE .ban2
|
|
1181
|
+
|
|
1182
|
+
STA WSYNC ; band 5: clear + 15 gap lines
|
|
411
1183
|
LDA #0
|
|
412
|
-
STA
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
1184
|
+
STA PF0
|
|
1185
|
+
STA PF1
|
|
1186
|
+
STA PF2
|
|
1187
|
+
LDA #$02
|
|
1188
|
+
STA CTRLPF ; SCORE mode for the hi-score band
|
|
1189
|
+
LDX #15
|
|
1190
|
+
.tb5:
|
|
1191
|
+
STA WSYNC
|
|
1192
|
+
DEX
|
|
1193
|
+
BNE .tb5
|
|
1194
|
+
|
|
1195
|
+
; band 6: hi-score, 24 lines (6 rows × 4). Packed digits stream into PF1;
|
|
1196
|
+
; SCORE mode draws them twice in the two player colors. In-session best;
|
|
1197
|
+
; honest: there is no battery — gone at power-off, like the arcades.
|
|
1198
|
+
LDA #COL_HUD
|
|
1199
|
+
STA COLUPF
|
|
1200
|
+
LDX #0
|
|
1201
|
+
.hsb:
|
|
1202
|
+
STA WSYNC
|
|
1203
|
+
TXA
|
|
1204
|
+
LSR
|
|
1205
|
+
LSR
|
|
1206
|
+
TAY
|
|
1207
|
+
LDA HSBUF,Y
|
|
1208
|
+
STA PF1
|
|
426
1209
|
INX
|
|
427
|
-
CPX #
|
|
428
|
-
BNE .
|
|
429
|
-
RTS
|
|
1210
|
+
CPX #24
|
|
1211
|
+
BNE .hsb
|
|
430
1212
|
|
|
431
|
-
;
|
|
432
|
-
|
|
433
|
-
|
|
1213
|
+
STA WSYNC ; band 7: clear + pad to exactly 192
|
|
1214
|
+
LDA #0
|
|
1215
|
+
STA PF1
|
|
1216
|
+
LDX #65
|
|
1217
|
+
.tb7:
|
|
1218
|
+
STA WSYNC
|
|
1219
|
+
DEX
|
|
1220
|
+
BNE .tb7
|
|
1221
|
+
|
|
1222
|
+
JMP kernel_done
|
|
1223
|
+
|
|
1224
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
1225
|
+
; ── GAME LOGIC (clay — reshape freely) ── data tables ──────────────────
|
|
1226
|
+
|
|
1227
|
+
; Digit font: 4 pixels wide × 6 rows, stored in the HIGH nibble (PF1 bit7
|
|
1228
|
+
; is the LEFTMOST pixel of the left playfield half — high nibble = left).
|
|
1229
|
+
DIGITS:
|
|
1230
|
+
.byte $60,$90,$90,$90,$90,$60 ; 0
|
|
1231
|
+
.byte $20,$60,$20,$20,$20,$70 ; 1
|
|
1232
|
+
.byte $60,$90,$10,$20,$40,$F0 ; 2
|
|
1233
|
+
.byte $E0,$10,$60,$10,$10,$E0 ; 3
|
|
1234
|
+
.byte $90,$90,$F0,$10,$10,$10 ; 4
|
|
1235
|
+
.byte $F0,$80,$E0,$10,$10,$E0 ; 5
|
|
1236
|
+
.byte $60,$80,$E0,$90,$90,$60 ; 6
|
|
1237
|
+
.byte $F0,$10,$20,$40,$40,$40 ; 7
|
|
1238
|
+
.byte $60,$90,$60,$90,$90,$60 ; 8
|
|
1239
|
+
.byte $60,$90,$90,$70,$10,$60 ; 9
|
|
1240
|
+
|
|
1241
|
+
; ── THE HERO SPRITE ───────────────────────────────────────────────────
|
|
1242
|
+
; 6 rows tall, P0. Two frames stacked: idle (base 0) and a walk pose
|
|
1243
|
+
; (base 6) selected by GFXIDX in the kernel — a cheap 1977-style 2-frame
|
|
1244
|
+
; animation. Drawn TOP-row first (the kernel scans the band downward).
|
|
1245
|
+
HERO:
|
|
1246
|
+
; idle (GFXIDX = 0)
|
|
434
1247
|
.byte %00111100
|
|
1248
|
+
.byte %01111110
|
|
435
1249
|
.byte %00011000
|
|
1250
|
+
.byte %00111100
|
|
1251
|
+
.byte %01100110
|
|
1252
|
+
.byte %01000010
|
|
1253
|
+
; walk (GFXIDX = 6)
|
|
1254
|
+
.byte %00111100
|
|
436
1255
|
.byte %01111110
|
|
437
|
-
.byte %
|
|
1256
|
+
.byte %00011000
|
|
438
1257
|
.byte %00111100
|
|
439
1258
|
.byte %00100100
|
|
440
|
-
.byte %
|
|
1259
|
+
.byte %01000010
|
|
1260
|
+
|
|
1261
|
+
; Title jingle (voice 1, AUDC $04 square; AUDF divider — LOWER = higher
|
|
1262
|
+
; pitch; 10 frames per note; $FF terminates). The table IS the song.
|
|
1263
|
+
TITLE_TUNE:
|
|
1264
|
+
.byte $1B,$17,$13,$0F,$13,$17,$13,$0F,$FF
|
|
1265
|
+
; Game-over tune: a falling figure.
|
|
1266
|
+
OVER_TUNE:
|
|
1267
|
+
.byte $0F,$13,$17,$1B,$1F,$23,$FF
|
|
1268
|
+
|
|
1269
|
+
; ── THE LEVEL ─────────────────────────────────────────────────────────
|
|
1270
|
+
; NROWS rows × (PF0, PF1, PF2). A lit pixel = solid ground; a gap = pit.
|
|
1271
|
+
; REFLECT mode mirrors the 20 stored pixels into a symmetric 40-px arena,
|
|
1272
|
+
; so you only author the LEFT half — the arena is naturally left/right
|
|
1273
|
+
; symmetric (for an asymmetric arena, switch the kernel to a per-line
|
|
1274
|
+
; asymmetric reload like the title banner). Row 0 = BOTTOM (the floor),
|
|
1275
|
+
; row NROWS-1 = TOP. PF bit order, as ever on the 2600:
|
|
1276
|
+
; PF0: bits 4..7 used, bit4 = leftmost (reversed)
|
|
1277
|
+
; PF1: bit7 = leftmost (normal)
|
|
1278
|
+
; PF2: bit0 = leftmost (reversed)
|
|
1279
|
+
; This is the workhorse "clay" of the file — every ledge, pit and the floor
|
|
1280
|
+
; lives here; ground_under_hero reads the SAME table so code and picture
|
|
1281
|
+
; never disagree. (The hero spawns on row 2; row 0 is a solid landing floor
|
|
1282
|
+
; so a fall always ends on ground.)
|
|
1283
|
+
LEVEL:
|
|
1284
|
+
; row 0 — solid floor (full left half → mirrored = full floor)
|
|
1285
|
+
.byte %11110000, %11111111, %11111111
|
|
1286
|
+
; row 1 — a low ledge on the left (the spike's patrol band)
|
|
1287
|
+
.byte %11110000, %11110000, %00000000
|
|
1288
|
+
; row 2 — a mid ledge (the hero's start perch)
|
|
1289
|
+
.byte %00000000, %00001111, %11110000
|
|
1290
|
+
; row 3 — open (air)
|
|
1291
|
+
.byte %00000000, %00000000, %00000000
|
|
1292
|
+
; row 4 — a high ledge near the wall
|
|
1293
|
+
.byte %11110000, %00000000, %00000000
|
|
1294
|
+
; row 5 — open
|
|
1295
|
+
.byte %00000000, %00000000, %00000000
|
|
1296
|
+
; row 6 — a small floating ledge centre-left
|
|
1297
|
+
.byte %00000000, %00111100, %00000000
|
|
1298
|
+
; row 7 — open
|
|
1299
|
+
.byte %00000000, %00000000, %00000000
|
|
1300
|
+
; row 8 — top cap ledge (a landing under the HUD)
|
|
1301
|
+
.byte %11110000, %00000011, %00000000
|
|
1302
|
+
|
|
1303
|
+
; ── THE TITLE BANNER ──────────────────────────────────────────────────
|
|
1304
|
+
; 40-pixel-wide artwork, 7 rows per word, drawn by the asymmetric-playfield
|
|
1305
|
+
; kernel above. Each row is six bytes across six tables (left PF0/PF1/PF2,
|
|
1306
|
+
; right PF0/PF1/PF2). PF bit order is the 2600's great prank — three
|
|
1307
|
+
; registers, three different orders:
|
|
1308
|
+
; PF0: only bits 4-7 used, bit 4 = LEFTMOST pixel (reversed)
|
|
1309
|
+
; PF1: bit 7 = leftmost (normal)
|
|
1310
|
+
; PF2: bit 0 = leftmost (reversed again)
|
|
1311
|
+
;
|
|
1312
|
+
; The 40-px art for each row is the comment ASCII above each table; the
|
|
1313
|
+
; bytes are mechanically encoded from it (left half = pixels 0..19 → PF0
|
|
1314
|
+
; bits4-7 / PF1 bits7-0 / PF2 bits0-7; right half = pixels 20..39 likewise).
|
|
1315
|
+
;
|
|
1316
|
+
; PERCH (P-E-R-C in the left copy, H in the right; +2px left pad to centre):
|
|
1317
|
+
; #### #### #### .### #..#
|
|
1318
|
+
; #..# #... #..# #... #..#
|
|
1319
|
+
; #..# #... #..# #... #..#
|
|
1320
|
+
; #### ###. ###. #... ####
|
|
1321
|
+
; #... #... #.#. #... #..#
|
|
1322
|
+
; #... #... #..# #... #..#
|
|
1323
|
+
; #... #### #..# .### #..#
|
|
1324
|
+
R1_PF0L:
|
|
1325
|
+
.byte %11000000, %01000000, %01000000, %11000000, %01000000, %01000000, %01000000
|
|
1326
|
+
R1_PF1L:
|
|
1327
|
+
.byte %11011110, %01010000, %01010000, %11011100, %00010000, %00010000, %00011110
|
|
1328
|
+
R1_PF2L:
|
|
1329
|
+
.byte %11001111, %00101001, %00101001, %00100111, %00100101, %00101001, %11001001
|
|
1330
|
+
R1_PF0R:
|
|
1331
|
+
.byte %01010000, %01000000, %01000000, %11000000, %01000000, %01000000, %01010000
|
|
1332
|
+
R1_PF1R:
|
|
1333
|
+
.byte %01000000, %01000000, %01000000, %11000000, %01000000, %01000000, %01000000
|
|
1334
|
+
R1_PF2R:
|
|
1335
|
+
.byte %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000
|
|
1336
|
+
|
|
1337
|
+
; PATROL (P-A-T-R in the left copy, O-L in the right):
|
|
1338
|
+
; #### .##. #### #### .##. #...
|
|
1339
|
+
; #..# #..# ..#. #..# #..# #...
|
|
1340
|
+
; #..# #..# ..#. #..# #..# #...
|
|
1341
|
+
; #### #### ..#. ###. #..# #...
|
|
1342
|
+
; #... #..# ..#. #.#. #..# #...
|
|
1343
|
+
; #... #..# ..#. #..# #..# #...
|
|
1344
|
+
; #... #..# ..#. #..# .##. ####
|
|
1345
|
+
R2_PF0L:
|
|
1346
|
+
.byte %11110000, %10010000, %10010000, %11110000, %00010000, %00010000, %00010000
|
|
1347
|
+
R2_PF1L:
|
|
1348
|
+
.byte %00110011, %01001000, %01001000, %01111000, %01001000, %01001000, %01001000
|
|
1349
|
+
R2_PF2L:
|
|
1350
|
+
.byte %01111011, %01001001, %01001001, %00111001, %00101001, %01001001, %01001001
|
|
1351
|
+
R2_PF0R:
|
|
1352
|
+
.byte %01100000, %10010000, %10010000, %10010000, %10010000, %10010000, %01100000
|
|
1353
|
+
R2_PF1R:
|
|
1354
|
+
.byte %01000000, %01000000, %01000000, %01000000, %01000000, %01000000, %01111000
|
|
1355
|
+
R2_PF2R:
|
|
1356
|
+
.byte %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000
|
|
441
1357
|
|
|
442
|
-
; ──
|
|
443
|
-
; Parallel arrays indexed 0..NUM_PLAT-1. Y = band top scanline (beam
|
|
444
|
-
; coords: bigger Y = higher on screen). XL/XR = the column span for the
|
|
445
|
-
; land-on-top test. The bars render FULL WIDTH, so every span is the whole
|
|
446
|
-
; screen (you can stand anywhere on a platform — visual == collision). To
|
|
447
|
-
; make narrower ledges, give a platform a partial PFROW pattern AND shrink
|
|
448
|
-
; its XL/XR here so the two stay in sync.
|
|
449
|
-
PLAT_Y:
|
|
450
|
-
.byte 18 ; floor (bottom)
|
|
451
|
-
.byte 70 ; ledge
|
|
452
|
-
.byte 110 ; ledge
|
|
453
|
-
.byte 150 ; ledge (top)
|
|
454
|
-
PLAT_XL:
|
|
455
|
-
.byte 0
|
|
456
|
-
.byte 0
|
|
457
|
-
.byte 0
|
|
458
|
-
.byte 0
|
|
459
|
-
PLAT_XR:
|
|
460
|
-
.byte 159
|
|
461
|
-
.byte 159
|
|
462
|
-
.byte 159
|
|
463
|
-
.byte 159
|
|
464
|
-
|
|
465
|
-
; ── Vector table ──
|
|
1358
|
+
; ── Vector table ──────────────────────────────────────────────────────
|
|
466
1359
|
org $FFFA
|
|
467
1360
|
.word START
|
|
468
1361
|
.word START
|